מדריך למושב הפתוח הפתוח באביב

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

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

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

2. הצגת מושב פתוח בתצוגה

כדי להבין טוב יותר את התפקיד של Open Session in View (OSIV), נניח שיש לנו בקשה נכנסת:

  1. האביב פותח תרדמת שינה חדשה מוֹשָׁב בתחילת הבקשה. אלה מושבים אינם מחוברים בהכרח למסד הנתונים.
  2. בכל פעם שהיישום זקוק ל- מוֹשָׁב, זה ישתמש בזה הקיים כבר.
  3. בסוף הבקשה, אותו מיירט סוגר זאת מוֹשָׁב.

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

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

2.1. מגף אביב

כברירת מחדל, OSIV פעיל ביישומי Spring Boot. למרות זאת, החל באביב אתחול 2.0, זה מזהיר אותנו מהעובדה שהוא מופעל בעת אתחול היישום אם לא הגדרנו זאת במפורש:

spring.jpa.open-in-view מופעל כברירת מחדל. לכן, ניתן לבצע שאילתות בסיס נתונים במהלך עיבוד התצוגה. הגדר באופן מפורש את spring.jpa.open-in-view כדי להשבית את האזהרה הזו.

בכל מקרה, אנו יכולים להשבית את OSIV באמצעות spring.jpa.open-in-view מאפיין תצורה:

spring.jpa.open-in-view = false

2.2. דפוס או אנטי דפוס?

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

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

3. גיבור אתחול עצלן

מאז OSIV מחייב את מוֹשָׁב מחזור חיים לכל בקשה, תרדמת שינה יכולה לפתור אסוציאציות עצלות גם לאחר חזרה ממפורש @ Transactional שֵׁרוּת.

כדי להבין טוב יותר את זה, נניח שאנחנו מדגמנים את המשתמשים שלנו ואת הרשאות האבטחה שלהם:

@Entity @Table (name = "משתמשים") משתמש בכיתה ציבורית {@Id @GeneratedValue פרטי מזהה ארוך; שם משתמש פרטי מחרוזת; הרשאות קבוצות פרטיות של @ElementCollection; // גטרים וקובעים}

בדומה למערכות יחסים אחרות, רבות-רבות, אחרות הרשאות הנכס הוא אוסף עצלן.

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

המחלקה הציבורית @Service SimpleUserService מיישמת את UserService {UserRepository userRepository הסופי; ציבורי SimpleUserService (UserRepository userRepository) {this.userRepository = userRepository; } @Override @Transactional (readOnly = true) ציבורי findOne (שם משתמש מחרוזת) אופציונלי {return userRepository.findByUsername (שם משתמש); }}

3.1. הציפייה

הנה מה שאנחנו מצפים שיקרה כאשר הקוד שלנו קורא ל- findOne שיטה:

  1. בהתחלה, ה- proxy של Spring מיירט את השיחה ומקבל את העסקה הנוכחית או יוצר אחת אם לא קיימת.
  2. לאחר מכן הוא מאציל את קריאת השיטה ליישום שלנו.
  3. לבסוף, מיופה הכוח מבצע את העסקה וכתוצאה מכך סוגר את הבסיס מוֹשָׁב. אחרי הכל, אנחנו רק צריכים את זה מוֹשָׁב בשכבת השירות שלנו.

בתוך ה findOne יישום השיטה, לא אתחול הרשאות אוסף. לכן, אנחנו לא צריכים להיות מסוגלים להשתמש ב- הרשאות לאחר השיטה חוזרת. אם אנו חוזרים על נכס זה, אנחנו צריכים לקבל א LazyInitializationException.

3.2. ברוך הבא לעולם האמיתי

בוא נכתוב בקר REST פשוט כדי לראות אם נוכל להשתמש ב- הרשאות תכונה:

@RestController @RequestMapping ("/ משתמשים") מחלקה ציבורית UserController {UserService userService פרטי פרטי; UserController ציבורי (UserService userService) {this.userService = userService; } @GetMapping ("/ {username}") ResponseEntity public findOne (@PathVariable String name us) {return userService .findOne (username) .map (DetailedUserDto :: fromEntity) .map (ResponseEntity :: ok) .orElse (ResponseEntity.notFound ().לִבנוֹת()); }}

הנה, אנחנו חוזרים הרשאות במהלך המרת ישות ל- DTO. מכיוון שאנחנו מצפים שההמרה הזו תיכשל עם א עצלן יוזמת יוצא מן הכלל, המבחן הבא לא אמור לעבור:

מחלקה @SpringBootTest @ AutoConfigureMockMvc @ ActiveProfiles ("test") UserControllerIntegrationTest {@ UserRepository UserRepository פרטי; @ MockMvc פרטית אוטומטית mockMvc; @ לפני כל setUp ריק () {משתמש משתמש = משתמש חדש (); user.setUsername ("שורש"); user.setPermissions (HashSet חדש (Arrays.asList ("PERM_READ", "PERM_WRITE"))); userRepository.save (user); } @Test בטל שניתןTheUserExists_WhenOsivIsEnabled_ThenLazyInitWorksEverywhere () זורק חריג {mockMvc.perform (get ("/ users / root")). AndExpect (status (). IsOk ()). AndExpect (jsonPath ("$." Username "). root ")) .andExpect (jsonPath (" $. הרשאות ", מכילInAnyOrder (" PERM_READ "," PERM_WRITE "))); }}

עם זאת, מבחן זה אינו זורק חריגים, והוא עובר.

מכיוון ש OSIV יוצר א מוֹשָׁב בתחילת הבקשה, פרוקסי העסקהמשתמש בזרם הזמין מוֹשָׁב במקום ליצור חדש לגמרי.

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

3.3. על פריון למפתחים

אם OSIV לא הופעל, נצטרך לאתחל ידנית את כל האסוציאציות העצלות הדרושות בהקשר עסקי. הדרך הכי ראשונית (ובדרך כלל שגויה) היא להשתמש ב- Hibernate.initialize () שיטה:

@Override @Transactional (readOnly = true) פומבי אופציונלי findOne (שם משתמש מחרוזת) {משתמש אופציונלי = userRepository.findByUsername (שם משתמש); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); משתמש חוזר; }

נכון לעכשיו, ההשפעה של OSIV על פרודוקטיביות המפתחים ברורה. עם זאת, לא תמיד מדובר בתפוקה של מפתחים.

4. נבל ביצועים

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

@Override public אופציונלי findOne (שם משתמש מחרוזת) {משתמש אופציונלי = userRepository.findByUsername (שם משתמש); אם (user.isPresent ()) {// שיחה מרחוק} משתמש חוזר; }

הנה, אנו מסירים את @ Transactional ביאור מכיוון שברור שלא נרצה לשמור על קשר מוֹשָׁב בזמן ההמתנה לשירות מרחוק.

4.1. הימנעות מ- IO מעורב

הבה נבהיר מה קורה אם לא נסיר את @ Transactional ביאור. נניח והשירות המרוחק החדש מגיב לאט מהרגיל:

  1. בהתחלה, פרוקסי האביב מקבל את הזרם מוֹשָׁב או יוצר אחד חדש. כך או כך, זה מוֹשָׁב עדיין לא מחובר. כלומר, הוא אינו משתמש בחיבור כלשהו מהבריכה.
  2. ברגע שאנחנו מבצעים את השאילתה כדי למצוא משתמש, ה- מוֹשָׁב הופך מחובר ולווה א חיבור מהבריכה.
  3. אם השיטה כולה היא עסקה, השיטה ממשיכה להתקשר לשירות מרחוק איטי תוך שמירה על ההשאלה חיבור.

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

ערבוב IOs של מסדי נתונים עם סוגים אחרים של IO בהקשר עסקי הוא ריח רע, ועלינו להימנע מכך בכל מחיר.

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

4.2. מיצוי בריכת החיבורים

כאשר OSIV פעיל, תמיד יש מוֹשָׁב בהיקף הבקשה הנוכחי, גם אם נסיר @ Transactional. למרות זאת מוֹשָׁב אינו מחובר בתחילה, לאחר IO מסד הנתונים הראשון שלנו, הוא מתחבר ונשאר כך עד סוף הבקשה.

לכן, יישום השירות התמים למראה שלנו ומותאם לאחרונה הוא מתכון לאסון בנוכחות OSIV:

@Override public אופציונלי findOne (שם משתמש מחרוזת) {משתמש אופציונלי = userRepository.findByUsername (שם משתמש); אם (user.isPresent ()) {// שיחה מרחוק} משתמש חוזר; }

הנה מה שקורה כאשר OSIV מופעל:

  1. בתחילת הבקשה, המסנן המתאים יוצר חדש מוֹשָׁב.
  2. כשאנחנו קוראים findByUsername שיטה, זאת מוֹשָׁב לווה א חיבור מהבריכה.
  3. ה מוֹשָׁב נשאר מחובר עד סוף הבקשה.

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

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

4.3. שאילתות מיותרות

למרבה הצער, מיצוי מאגר החיבורים אינו נושא הביצועים היחיד הקשור ל- OSIV.

מאז מוֹשָׁב פתוח לכל מחזור חיי הבקשה, כמה ניווט בנכסים עשויים לעורר עוד כמה שאילתות שאינן רצויות מחוץ להקשר העסקה. אפשר אפילו להסתיים בבעיה נבחרת n + 1, והחדשות הגרועות ביותר הן שאולי לא נבחין בכך עד הייצור.

הוספת עלבון לפציעה, מוֹשָׁב מבצע את כל אותן שאילתות נוספות במצב התחייבות אוטומטית. במצב התחייבות אוטומטית, כל משפט SQL מטופל כעסקה ומחויב אוטומטית מיד לאחר ביצועו. זה, בתורו, מפעיל לחץ רב על בסיס הנתונים.

5. בחרו בחוכמה

האם OSIV הוא דפוס או דפוס אנטי רלוונטי. הדבר החשוב ביותר כאן הוא המציאות בה אנו חיים.

אם אנו מפתחים שירות CRUD פשוט, ייתכן שיהיה הגיוני להשתמש ב- OSIV, מכיוון שאולי לעולם לא ניתקל בבעיות ביצוע אלה.

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

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

השורה התחתונה היא שעלינו להיות מודעים לפיזויים בעת שימוש או התעלמות מ- OSIV.

6. חלופות

אם נשבית את OSIV, עלינו איכשהו למנוע פוטנציאל LazyInitializationExceptions כאשר מתמודדים עם עמותות עצלות. בין קומץ גישות להתמודדות עם עמותות עצלות, נמנה שתיים מהן כאן.

6.1. גרפי ישויות

כאשר אנו מגדירים שיטות שאילתה ב- Spring Data JPA, אנו יכולים להוסיף הערה לשיטת שאילתה @EntityGraph להביא בשקיקה חלק כלשהו מהישות:

ממשק ציבורי UserRepository מרחיב את JpaRepository {@EntityGraph (attributePaths = "הרשאות") findByUsername אופציונלי (שם משתמש מחרוזת); }

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

אם עלינו להחזיר מספר תחזיות מאותה שאילתה, עלינו להגדיר מספר שאילתות עם תצורות שונות של גרפים של ישויות:

ממשק ציבורי UserRepository מרחיב את JpaRepository {@EntityGraph (attributePaths = "permissions") אופציונלי findDetailedByUsername (שם משתמש מחרוזת); אופציונלי findSummaryByUsername (שם משתמש מחרוזת); }

6.2. אזהרות בעת השימוש Hibernate.initialize ()

אפשר לטעון שבמקום להשתמש בגרפים של ישויות, נוכל להשתמש בשמצה Hibernate.initialize () להביא עמותות עצלות בכל מקום שאנחנו צריכים לעשות זאת:

@Override @Transactional (readOnly = true) פומבי אופציונלי findOne (שם משתמש מחרוזת) {משתמש אופציונלי = userRepository.findByUsername (שם משתמש); user.ifPresent (u -> Hibernate.initialize (u.getPermissions ())); משתמש חוזר; }

הם עשויים להיות חכמים בנושא וגם מציעים להתקשר ל getPermissions () שיטה להפעלת תהליך האחזור:

משתמש אופציונלי = userRepository.findByUsername (שם משתמש); user.ifPresent (u -> {Set permissions = u.getPermissions (); System.out.println ("הרשאות טעונות:" + permissions.size ());});

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

> בחר u.id, u.username מהמשתמשים u where u.username =? > בחר p.user_id, p.permissions מ user_permissions p איפה p.user_id =? 

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

מצד שני, אם אנו משתמשים בגרפים של ישויות או אפילו Fetch Joins, ה- Hibernate יביא את כל הנתונים הדרושים בשאילתה אחת בלבד:

> בחר u.id, u.username, p.user_id, p. הרשאות ממשתמשים u שמאל חיצוני הצטרף user_permissions p ב- u.id = p.user_id איפה u.username =?

7. מסקנה

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

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


$config[zx-auto] not found$config[zx-overlay] not found