Entity Framework Lazy Loading (בעברית – טעינה עצלנית של מסגרת ישויות)

July 21, 2008

2 comments

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

כשתיכננו את הארכיטקטורה של EF, הוחלט שטעינת הישויות תהיה תוך כדי ריצה (JIT) ע”י שימוש בטכניקת Lazy Load – רק כאשר פונים לאוסף ישוית מבוצעת פניה ל-DB עבור טעינתם.

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

  1. על-מנת לבצע את הטעינה, יש להפעיל את מתודת Load של ה-EntityCollection או ה-EntityReference באופן יזום (Explicit), מה שאומר שצריך לזכור לשים פקודת Load לפני כל פניה ל-Navigation Property (אחרת עלולים לקבל NullReferenceException או לא פחות גרוע מזה – שגיאה בלוגיקה של התוכנית)
  2. הפעלה עוקבת של Load על אותו Navigation Property תגרום לפנייה חוזרת ל-DB, על כן צריך לשים לב טוב טוב בזמן Code Review אם המפתחים שלכם זכרו לעטוף כל פקודה כזו בבדיקה של IsLoaded (מאפיין שיש לכל EntityCollection ו-EntityReference).
  3. במידה ובנינו קוד שמבצע איטרציה על אוסף ישויות ומבצע פעולות על כל אחת, והישויות עליהן עוברים מכילות Navigation Properties, יש לבצע Load לפני כל פניה למאפיין, מה שאומר שכמות הפניות ל-DB תהיה מספר המאפיינים הנבדקים בישות X מספר הישויות – לא סימפטי מבחינת יעילות

 

הסעיף הראשון די מעצבן, בגלל שתי השורות שצריכות לחזור על עצמן לפני כל פניה ל-Navigation Property (בדיקת IsLoaded וביצוע Load). קיים כיום פתרון של Transparent Lazy Load אבל הוא לא חלק מה-EF עצמו עדין.

לגבי סעיף 3 – קיים פתרון ברמת ה-EF ע”י שימוש במתודה Include שניתן להפעיל על אובייקטי ObjectQuery (כל EntityType שניתן לתשאול). למתודה מעבירים פרמטר מחרוזתי שמציין את שם ה-Navigation Property אליו נרצה לגשת בהמשך, כאשר ניתן לשרשר מספר פקודות Include על-מנת לקבל תוצאת שאילתא המכילה נתונים של כמה ישויות.

מה בעצם קורה מאחורי הקלעים? כאשר מפעילים פעולת Include, השאילתא שמורצת ב-DB מכילה את השליפה של הישות המקורית וכן שליפה עבור כל אחת מישויות הבנים שהוספנו.

לדוגמה, נתון המודל הבא:

image 

ומבנה ה-DB הבא:

 image

בהנתן קוד שנראה כך:

TestModel.TestEntities model = new TestModel.TestEntities();

var all = from a in model.Person.Include("Pets").Include("Address")
          select a;

foreach (var person in all.ToList())
{
    if (!person.Pets.IsLoaded)
        person.Pets.Load();
    if (!person.Address.IsLoaded)
        person.Address.Load();
    
    Console.WriteLine(
        string.Format ("{0} {1}\n{2}\n{3}",
            person.FirstName, 
            person.LastName,
            String.Join("\n", (from ad in person.Address
             select string.Format("{0} {1} {2}", ad.City, ad.Street, ad.House)).ToArray()),
             String.Join("\n", (from p in person.Pets
             select string.Format("{0} {1}", p.Name, p.Species)).ToArray())));
    Console.WriteLine();
}

כאשר תבוצע הפקודה ToList, תורץ השאילתא הבאה:

 

 

SELECT 
[UnionAll1].[Id] AS [C1], 
[UnionAll1].[FirstName] AS [C2], 
[UnionAll1].[LastName] AS [C3], 
[UnionAll1].[C2] AS [C4], 
[UnionAll1].[C1] AS [C5], 
[UnionAll1].[Id1] AS [C6], 
[UnionAll1].[Name] AS [C7], 
[UnionAll1].[Species] AS [C8], 
[UnionAll1].[PersonId] AS [C9], 
[UnionAll1].[C3] AS [C10], 
[UnionAll1].[C4] AS [C11], 
[UnionAll1].[C5] AS [C12], 
[UnionAll1].[C6] AS [C13], 
[UnionAll1].[C7] AS [C14]
FROM  (SELECT 
    CASE WHEN ([Extent2].[Id] IS NULL) THEN CAST(NULL AS int) ELSE 1 END AS [C1], 
    [Extent1].[Id] AS [Id], 
    [Extent1].[FirstName] AS [FirstName], 
    [Extent1].[LastName] AS [LastName], 
    1 AS [C2], 
    [Extent2].[Id] AS [Id1], 
    [Extent2].[Name] AS [Name], 
    [Extent2].[Species] AS [Species], 
    [Extent2].[PersonId] AS [PersonId], 
    CAST(NULL AS int) AS [C3], 
    CAST(NULL AS varchar(1)) AS [C4], 
    CAST(NULL AS varchar(1)) AS [C5], 
    CAST(NULL AS varchar(1)) AS [C6], 
    CAST(NULL AS int) AS [C7]
    FROM  [dbo].[Person] AS [Extent1]
    LEFT OUTER JOIN [dbo].[Pets] AS [Extent2] ON [Extent1].[Id] = [Extent2].[PersonId]
UNION ALL
    SELECT 
    2 AS [C1], 
    [Extent3].[Id] AS [Id], 
    [Extent3].[FirstName] AS [FirstName], 
    [Extent3].[LastName] AS [LastName], 
    1 AS [C2], 
    CAST(NULL AS int) AS [C3], 
    CAST(NULL AS varchar(1)) AS [C4], 
    CAST(NULL AS varchar(1)) AS [C5], 
    CAST(NULL AS int) AS [C6], 
    [Extent4].[Id] AS [Id1], 
    [Extent4].[City] AS [City], 
    [Extent4].[Street] AS [Street], 
    [Extent4].[House] AS [House], 
    [Extent4].[PersonId] AS [PersonId]
    FROM  [dbo].[Person] AS [Extent3]
    INNER JOIN [dbo].[Address] AS [Extent4] ON [Extent3].[Id] = [Extent4].[PersonId]) AS [UnionAll1]

ארוך, לא? אבל עבור מבנה זה של טבלאות השליפה יחסית יעילה.

אזהרה: מומלץ מאוד להזהר בכמות ה-Include-ים שמבצעים ובמספר הרמות שנכנסים פנימה לתוך ה-Navigations – ככל שתנסו באמצעות Include לשלוף יותר Navigation Properties פנימיים של ישויות, כך השאילתא תגדל באופן מפלצתי ותהפוך לפחות יעילה (ניסיתם פעם לעשות Left Join על 4 טבלאות גדולות? לא ממש טוב בביצועים).

 

דבר נוסף שיש לשים לב אליו – הפקודה Include מנסה לזהות את ה-Navigation Property והמיפוי שלה (על-מנת לבנות את שאילתת ה-Union) עוד טרם טעינת אוסף הישויות הראשי, מה שאומר שאם אוסף הישויות הראשי שלכם הוא פולימורפי (נניח אם בנוסף ל-Person היתה מחלקה יורשת נוספת באותו EntitySet) תוכלו לבצע Include רק ל-Navigation Properties של ה-Base Class, כלומר אין לנו אפשרות אמיתית לבצע Eager Load (טעינה Implicit של כל מבנה הישויות).

אם כן תרצו לטעון רשימה פולימורפית, תצטרכו לבצע מספר טעינות, כל פעם עבור עץ ירושה בודד (עם ה-Include-ים עבורו).

אבל לכל פתרון יש את ה-catch – מה קורה אם מחלקת בן כלשהי מכילה Navigation Property שגם הוא ממחלקת Base כלשהי (נניח Person שמכיל מאפיין Cars שהוא רשימה של אוסף פולימורפי של Car וסוגי רכבים שיורשים ממנו)? אתם מבינים לאן זה הולך…

 

אז לסיכום, Lazy Loading קיים אך בעייתי, Include קיים אך בעייתי, Eager Load לא קיים אך לא פחות בעייתי – פשוט צריך למצוא את הפתרון המתאים בהתאם לסיטואציה בה אתם נמצאים.

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

2 comments

  1. ronjtJuly 22, 2008 ב 1:53 am

    I wish I could read your comments. Any chance of seeing it in English. Thank you.

    Reply
  2. Ido FlatowJuly 22, 2008 ב 12:54 pm

    Hi ronjt,
    I didn’t quite understand from your comment if you wish that the post will be translated into english or do you wish further comments to be written in english.

    Reply