ניהול עסקאות תכנות באביב

1. סקירה כללית

האביב @ Transactional ביאור מספק ממשק API הצהרתי נחמד לסימון גבולות העסקה.

מאחורי הקלעים, היבט דואג ליצור ולתחזק עסקאות כפי שהן מוגדרות בכל התרחשות של @ Transactional ביאור. גישה זו מקלה על ניתוק לוגיקת הליבה העסקית שלנו מדאגות רוחביות כמו ניהול עסקאות.

במדריך זה נראה שזו לא תמיד הגישה הטובה ביותר. נבדוק אילו אלטרנטיבות תכנותיות שמציעה אביב TransactionTemplate, והסיבות שלנו לשימוש בהן.

2. צרות בגן עדן

נניח שאנחנו מערבבים שני סוגים שונים של קלט / פלט בשירות פשוט:

@ Transactional בטל ציבורי initialPayment (בקשת PaymentRequest) {savePaymentRequest (בקשה); // שיחת DBThePaymentProviderApi (בקשה); // API updatePaymentState (בקשה); // DB saveHistoryForAuditing (בקשה); // DB}

הנה, יש לנו כמה שיחות בסיס נתונים לצד שיחת REST API יקרה אולי. במבט ראשון, אולי זה הגיוני להפוך את כל השיטה לעסקה, מכיוון שאולי נרצה להשתמש בשיטה כזו EntityManager לבצע את כל הפעולה באופן אטומי.

עם זאת, אם לממשק ה- API החיצוני זה לוקח יותר זמן מהרגיל להגיב, מכל סיבה שהיא, אנו עשויים להיגמר מחיבורי מסדי נתונים!

2.1. הטבע הקשה של המציאות

הנה מה שקורה כשאנחנו קוראים תשלום ראשוני שיטה:

  1. ההיבט העסקי יוצר חדש EntityManager ומתחיל בעסקה חדשה - כך, היא לווה עסקה חיבור ממאגר החיבורים
  2. לאחר שיחת מסד הנתונים הראשונה, הוא מתקשר לממשק ה- API החיצוני תוך שמירה על ההשאלה חיבור
  3. לבסוף, הוא משתמש בזה חיבור לביצוע שיחות מסד הנתונים הנותרות

אם שיחת ה- API מגיבה לאט מאוד לזמן מה, שיטה זו תחזיר את המושאל חיבור בזמן ההמתנה לתגובה.

דמיין שבמהלך תקופה זו, אנו מקבלים פרץ של שיחות אל תשלום ראשוני שיטה. ואז, הכל חיבורים עשוי לחכות לתגובה משיחת ה- API. זו הסיבה שאולי נגמר לנו חיבורי בסיס הנתונים - בגלל שירות אחורי איטי!

ערבוב ה- I / O של מסד הנתונים עם סוגים אחרים של I / O בהקשר עסקי הוא ריח רע. לכן, הפיתרון הראשון לבעיות מסוג זה הוא להפריד בין סוגים אלה של קלט / פלט לחלוטין. אם מסיבה כלשהי איננו יכולים להפריד ביניהם, אנו עדיין יכולים להשתמש ב- API של Spring כדי לנהל עסקאות באופן ידני.

3. שימוש TransactionTemplate

TransactionTemplate מספק קבוצה של ממשקי API המבוססים על החזרה לניהול עסקאות ידנית. כדי להשתמש בו, ראשית, עלינו לאתחל אותו עם PlatformTransactionManager.

לדוגמה, אנו יכולים להגדיר תבנית זו באמצעות הזרקת תלות:

// כיתת הערות הבדיקה ManualTransactionIntegrationTest {@Autowired PlatformTransactionManager PrivateManager; TransactionTemplate פרטי TransTemplate; @BeforeEach ריק ריק SetUp () {transactionTemplate = תבנית TransactionTemplate חדשה (transactionManager); } // הושמט }

ה PlatformTransactionManager עוזר לתבנית ליצור, לבצע או להחזיר עסקאות.

בעת שימוש באביב מגף, שעועית מתאימה מהסוג PlatformTransactionManager יירשם באופן אוטומטי, אז אנחנו פשוט צריכים פשוט להזריק אותו. אחרת, עלינו לרשום ידנית א PlatformTransactionManager אפונה.

3.1. דגם דומיין לדוגמא

מעתה ואילך, לצורך ההדגמה, אנו נשתמש במודל תחום תשלומים פשוט יותר. בתחום פשוט זה, יש לנו תַשְׁלוּם ישות שתכלול את פרטי כל תשלום:

@Etity בכיתה ציבורית תשלום {@Id @GeneratedValue פרטי מזהה ארוך; סכום פרטי פרטי; @Column (ייחודי = אמיתי) פרטי מחרוזת referenceNumber; מדינה מדינה פרטית (EnumType.STRING); // גטרים וקובעים ציבור ציבורי מדינה {STARTED, FAILED, SUCCESSFUL}}

כמו כן, נפעיל את כל הבדיקות בכיתת מבחן, תוך שימוש בספריית Testcontainers להפעלת מופע PostgreSQL לפני כל מקרה מבחן:

@DataJpaTest @ Testcontainers @ ActiveProfiles ("מבחן") @ AutoConfigureTestDatabase (החלף = NONE) @ Transactional (propagation = NOT_SUPPORTED) // אנו הולכים לטפל בעסקאות בכיתה ידנית באופן ידני TransactionIntegrationTest {@Autowired Private PlatformTransactionManager Transaction Manager. @ EntityManager entityManager פרטי; @ Container פרטי סטטי PostgreSQLContainer pg = initPostgres (); TransactionTemplate פרטי TransactionTemplate; @ לפני כל SetUp () ריק ציבורי () {transactionTemplate = חדש TransactionTemplate (transactionManager); } // בודק סטטי פרטי PostgreSQLContainer initPostgres () {PostgreSQLContainer pg = new PostgreSQLContainer ("postgres: 11.1") .withDatabaseName ("baeldung") .withUsername ("test") .withPassword ("test"); pg.setPortBindings (singletonList ("54320: 5432")); להחזיר pg; }}

3.2. עסקאות עם תוצאות

ה TransactionTemplate מציע שיטה שנקראת לבצע, שיכול להריץ כל גוש קוד נתון בתוך עסקה ואז להחזיר תוצאה כלשהי:

@Test void givenAPayment_WhenNotDuplicate_ThenShouldCommit () {Long id = transactionTemplate.execute (status -> {Payment payment = New Payment (); payment.setAmount (1000L); payment.setReferenceNumber ("Ref-1"); payment.setState (תשלום. State.SUCCESSFUL); entityManager.persist (תשלום); return return.getId ();}); תשלום תשלום = entityManager.find (Payment.class, id); assertThat (תשלום) .isNotNull (); }

הנה, אנחנו ממשיכים חדש תַשְׁלוּם למשל במסד הנתונים ואז להחזיר את המזהה שנוצר אוטומטית.

בדומה לגישה ההצהרתית, התבנית יכולה להבטיח אטומיות בשבילנו. כלומר, אם אחת הפעולות בתוך עסקה לא מצליחה להשלים, זהמחזיר את כולם לאחור:

@Test בטל שניתןTwoPayments_WhenRefIsDuplicate_ThenShouldRollback () {נסה {transactionTemplate.execute (סטטוס -> {תשלום ראשון = תשלום חדש (); first.setAmount (1000L); first.setReferenceNumber ("Ref-1"); first.setState (Payment.State .SUCCESSFUL); תשלום שני = תשלום חדש (); second.setAmount (2000L); second.setReferenceNumber ("Ref-1"); // אותו מספר התייחסות second.setState (Payment.State.SUCCESSFUL); entityManager.persist ( ראשון); // ok entityManager.persist (השני); // נכשלת החזרת "Ref-1";}); } לתפוס (התעלם מחריגה) {} assertThat (entityManager.createQuery ("בחר p מ- Payment p"). getResultList ()). isEmpty (); }

מאז השנייה referenceNumber הוא כפילות, מסד הנתונים דוחה את פעולת ההתמדה השנייה, מה שגורם להחזרת העסקה כולה. לכן, המאגר אינו מכיל תשלומים לאחר העסקה. אפשר גם להפעיל באופן ידני החזר על ידי התקשרות ל setRollbackOnly () עַל TransactionStatus:

@Test בטל givenAPayment_WhenMarkAsRollback_ThenShouldRollback () {transactionTemplate.execute (סטטוס -> {תשלום תשלום = תשלום חדש (); payment.setAmount (1000 ליטר); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL ); entityManager.persist (תשלום); status.setRollbackOnly (); החזר תשלום.getId ();}); assertThat (entityManager.createQuery ("בחר p מתוך תשלום p"). getResultList ()). isEmpty (); }

3.3. עסקאות ללא תוצאות

אם איננו מתכוונים להחזיר דבר מהעסקה, נוכל להשתמש ב TransactionCallbackWithoutResult מחזור החזרה:

@Test בטל givenAPayment_WhenNotExpectingAnyResult_ThenShouldCommit () {transactionTemplate.execute (TransactionCallbackWithoutResult חדש) {@Override בטל doInTransactionWithoutResult (מצב TransactionStatus) {תשלום תשלום = תשלום חדש (); .State.SUCCESSFUL); entityManager.persist (תשלום);}}); assertThat (entityManager.createQuery ("בחר p מתוך תשלום p"). getResultList ()). hasSize (1); }

3.4. תצורות עסקאות מותאמות אישית

עד עכשיו השתמשנו ב- TransactionTemplate עם תצורת ברירת המחדל שלה. למרות שברירת מחדל זו מספיקה רוב הזמן, עדיין ניתן לשנות את הגדרות התצורה.

לדוגמה, אנו יכולים להגדיר את רמת בידוד העסקה:

transactionTemplate = חדש TransactionTemplate (transactionManager); transactionTemplate.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ);

באופן דומה, אנו יכולים לשנות את התנהגות התפשטות העסקאות:

transactionTemplate.setPropagationBehavior (TransactionDefinition.PROPAGATION_REQUIRES_NEW);

או שנוכל לקבוע פסק זמן, בשניות, לעסקה:

transactionTemplate.setTimeout (1000);

אפשר אפילו ליהנות מאופטימיזציות לעסקאות לקריאה בלבד:

transactionTemplate.setReadOnly (נכון);

בכל מקרה, ברגע שניצור a TransactionTemplate עם תצורה, כל העסקאות ישתמשו בתצורה זו כדי לבצע. כך, אם אנו זקוקים למספר תצורות, עלינו ליצור מספר מופעי תבנית.

4. שימוש PlatformTransactionManager

בנוסף ל TransactionTemplate, אנו יכולים להשתמש ב- API ברמה נמוכה עוד יותר כמו PlatformTransactionManager לנהל עסקאות באופן ידני. באופן די מעניין, שניהם @ Transactional ו TransactionTemplate השתמש ב- API זה לניהול העסקאות שלהם באופן פנימי.

4.1. קביעת תצורה של עסקאות

לפני השימוש ב- API זה, עלינו להגדיר כיצד העסקה שלנו תיראה. לדוגמא, אנו יכולים להגדיר פסק זמן של שלוש שניות עם רמת בידוד העסקה הקריאה החוזרת על עצמה:

הגדרת DefaultTransactionDefinition = חדשה DefaultTransactionDefinition (); definition.setIsolationLevel (TransactionDefinition.ISOLATION_REPEATABLE_READ); definition.setTimeout (3); 

הגדרות עסקה דומות ל TransactionTemplate תצורות. למרות זאת, אנו יכולים להשתמש במספר הגדרות עם אחת בלבד PlatformTransactionManager.

4.2. שמירה על עסקאות

לאחר קביעת התצורה של העסקה שלנו, נוכל לנהל עסקאות בתכנות:

@Test בטל givenAPayment_WhenUsingTxManager_ThenShouldCommit () {// הגדרת עסקה TransactionStatus status = transactionManager.getTransaction (הגדרה); נסה {תשלום תשלום = תשלום חדש (); payment.setReferenceNumber ("Ref-1"); payment.setState (Payment.State.SUCCESSFUL); entityManager.persist (תשלום); transactionManager.commit (סטטוס); } לתפוס (Exception ex) {transactionManager.rollback (סטטוס); } assertThat (entityManager.createQuery ("בחר p מתוך תשלום p"). getResultList ()). hasSize (1); }

5. מסקנה

במדריך זה, ראשית, ראינו מתי צריך לבחור בניהול עסקאות פרוגרמטיות על פני הגישה ההצהרתית. לאחר מכן, על ידי הצגת שני ממשקי API שונים, למדנו כיצד ליצור, לבצע או להחזיר ידנית כל עסקה נתונה.

כרגיל, קוד הדוגמה זמין ב- GitHub.