DCSIMG
July 2008 - Posts - Ido Flatow's Blog Veni Vidi Scripsi

Ido Flatow's Blog

Veni Vidi Scripsi

News

Have you heard me speak?
Powered
<style type='text/css' media='screen' id='sm_css'> #smix {overflow: visible;height: auto;border-radius: 10px;max-width: 250px;background-color: #323232;text-align: left;font-size: 12px;line-height: 16px;font-family:'Lucida Sans Unicode','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;-webkit-border-radius: 10px;-moz-border-radius: 10px;border-radius: 10px;} #smix a {color: #0056CC;text-decoration: none;} #smix .sm_head {color: #fff; line-height: 1em;font-size: 1.4em;padding: 10px;color: #fff;} #smix .sm_lanyard_wrapper {background-color: #fff;;clear: both;width: 97%;margin: 0 auto;margin-bottom: 0px;} #smix .sm_lanyard_content {padding: 7px;}#smix button.sm_rec, #smix a.sm_rec, #smix input[type=submit].sm_rec { padding: 6px 10px; -webkit-border-radius: 2px 2px;-moz-border-radius: 2px; border-radius: 2px; border: solid 1px rgb(153, 153, 153); background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 255, 255)), to(rgb(221, 221, 221))); color: #333; text-decoration: none; cursor: pointer; display: inline-block; text-align: center; text-shadow: 0px 1px 1px rgba(255,255,255,1); line-height: 1; }#smix .sm_rec:hover { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(248, 248, 248)), to(rgb(221, 221, 221))); }#smix .sm_rec:active { background: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(204, 204, 204)), to(rgb(221, 221, 221))); }#smix .sm_rec.medium { padding: 3px 7px; font-size: 13px; }#smix .sm_rec span.icon.thumbs_up {background-position: 0px 36px;vertical-align: text-top;display: inline-block;margin-right: 4px;height: 18px;width: 16px;background-image: url(http://speakermix.com/images/new/thumbsold.png);}#smix .sm_rec:hover span.icon.thumbs_up {background-position: 0px 18px;} #smix .sm_events {padding:2px 0px 4px 0px;} #smix .sm_section {font-size: 10px; border-bottom: 1px solid silver; margin-bottom: 6px;} #smix .sm_subline {font-size:120%;margin-top:4px;font-weight:bold} #smix .powered {text-align: right} #smix .powered img {margin: 7px} </style>
Sela Technology Center

Advertisement

July 2008 - Posts

שיתוף קבצי קוד בין פרויקטים

לפני שבועיים סשה פרסם פוסט על שיתוף קבצי Contract בין Service ל-Client שלו.

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

1. שיטה זו של קישור ע"י Link עובדת כל עוד היחס בין המסלולים (Path) נשמר בכל מחשבי הפיתוח - ברוב המקרים זה כך, אך יש לשים לב לכך בשביל שלא יווצרו broken links.

2. פתרון אפשרי אחר, למי שעדין עובד עם ה-VSS הישן של MS הוא לבצע Share של קובץ הקוד בין תיקיות הפרויקטים השונות ובכך לייצר עותק מקושר המנוהל ברמת ה-Source Control ולא ב-Studio. כך אפשר להתגבר על הרלטיוויות.
למי שמשתמש עם TFS ומעוניין באפשרות זו - תמשיכו לחפש. משום מה החליטו ב-MS שתכונה זו של VSS לא תמשיך הלאה ל-TFS והיא קיימת רק בתוספות 3rd Party.
לצערי אני מכיר רק את המוצרים של MS וקצת את Clear Case ששם נראה לי שאפשרות זו לא נתמכת. אם אתם מכירים כלי Source Control אחרים שמאפשרים זאת, אשמח לשמוע.

3. השימוש בקובץ קוד בפרויקט אחר מזה שבו הוא נוצר מעלה בעיה קטנה אחת - ה-Namespace של המחלקות. במצב זו ה-NS של פרויקט א' לפעמים שונה מזה של פרויקט ב' והיינו רוצים שהקוד המשוכפל "יתלבש" על מחלקות Partial שקיימות לנו בפרויקט החדש. הפתרון - שימוש ב-Preprocessor directives של IF.

 

א. מגדירים IF על ה-Namespace - ראו דוגמה בתמונות בהמשך

ב. בפרויקט א', הולכים להגדרות הפרויקט ותחת Build->Conditional compilation symbols מוסיפים את הסימבול המתאים ל-NS של פרויקט א'

ג. בפרויקט ב', הולכים להגדרות הפרויקט ותחת Build->Conditional compilation symbols מוסיפים את הסימבול המתאים ל-NS של פרויקט ב'

ככה זה נראה כאשר פותחים את הקובץ מתוך פרויקט A :

image

וככה זה נראה כאשר פותחים את הקובץ מתוך פרויקט B :

image

שימו לב שה-IDE צובע באפור את ה-NS שלא עונה ל-IF, על-מנת לציין שהוא לא רלוונטי - כך אפשר להבחין בעין איזה קוד הוא זה שעומד להתקמפל.

דרך אגב, יכולת זו, כולל התכונה של ההצגה באפור ב-IDE, קיימת גם בגרסת VS 2005

כמה צוותי פיתוח צריך בשביל להחליף נורה?

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

למי שיצא לקרוא את ה-Post הקודם שלי לגבי בעיות עם EF ו-JSON והציץ בתכתובת שהיתה בפורום של EF ב-MSDN שם אולי לב לכך שנרשם שם שהבעיה תועבר לצוות של Ado.Net Data Services (אסטוריה) לטיפולם.

אני חשבתי לתומי שהם כנראה לקחו אחריות על ה-Serializer של JSON (או בשמו הידוע DataContractJsonSerializer) זאת מאחר ו-JavascriptSerializer הוצא לגמלאות (או בעגה הדוטנטית made obselete) ושיערתי שאין סיבה שיפתחו שני סריאליזרים שונים לאותה מטרה - טעיתי !

הצצה חפוזה ב-Ado.Net Data Services מעלה שיש לסביבה זו סריאליזר נפרד לחלוטין (מחלקת JsonSerializer החבויה תחת System.Data.Services.Serializers), יחד עם אחיו ואחיותיו הסריאליזרים - סריאליזר חדש ל-Binary, ל-Atom ואפילו סריאליזר חדש ל-POX.

זה מזכיר לי ששנה שעברה, שמעתי הרצאה של אלון פליס בנושא Media Center וכשהוא הגיע לתאר את סביבות הפיתוח לפלטפורמה זו הוא ציין שבנוסף ל-HTML ו-XBAP (הרצת WPF תחת דפדפן) ישנה שפה נוספת לבניית אפליקציות שנקראת MCML (קיצור של Media Center Markup Language). הרבה שאלו את עצמם בסוף ההרצאה למה באמת צריך 2 סביבות פיתוח מתקדמות (XBAP ו-MCML) ולראיה, לאחר כמה חודשים אלון פרסם בבלוג על כך ש-XBAP הוצאה לגמלאות מ-Media Center.

 

אז אני מניח שנצטרך לחכות ולראות מי ינצח DataContractSerializer ומשפחתו או משפחת ה-Serializers של Data Services.

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

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

כשתיכננו את הארכיטקטורה של 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 לא קיים אך לא פחות בעייתי - פשוט צריך למצוא את הפתרון המתאים בהתאם לסיטואציה בה אתם נמצאים.

Entity Framework ו-JSON, שילוב בעייתי

בחודשים האחרונים אני עובד על בניית תשתית המבוססת על Ado.Net Entity Framework (ראו פוסט קודם בנושא), כרגע בגרסה שהופצה עם Visual Studio 2008 SP1 Beta 1 ולפני כן עם גרסת ה-Beta 3.

מאחר והפרויקט הוא WEB-י, מבוסס ASP.NET Ajax (עם שדרוג קטן שלנו), אחד השלבים שהיינו צריכים לעבור בדרך הוא איך להעביר אובייקטי Entity Framework לצד קליינט כאשר הם מסורלזים ל-JSON

לאחר כמה נסיונות, התברר שבגרסת ה-Beta 3 של ה-Entity Framework קיימת בעיה לבצע סריאליזציה לכל טיפוס המכיל Association (בין אם Reference או Collection):

אוסף האובייקטים המיוצגים ע"י EF, כמו כל אוסף אובייקטים שניתן לייצג ב-NET., יכול להוות גרף ולא דווקא עץ, לדוגמה יחס "אח של" שבו אובייקט A קשור לאובייקט B אבל גם אובייקט B קשור לאובייקט A. כאשר אוסף אובייקטים זה עובר סריאליזציה שאינה בינארית (XML/JSON) הקישור הגרפי נאבד (באופן תיאורטי) והרכבתו מחדש היא בעייתית ואמורה להסתמך על המזהה (ה-KEY) שניתן לכל אובייקט בגרף. מסיבה זו, בחרו ב-Beta 3 לעזוב את זה לכרגע ולכן כל קשר בין ישויות ב-EF לא עבר סריאליזציה, לא באמצעות XmlSerializer ולא באמצעות DataContractSerializer.

אם תנסו בגרסה זו לבצע סריאליזציה ודה-סריאליזציה תקבלו אובייקטים בלי קשרים ביניהם.

 

לאחר פרסום הודעה בבלוגים של מיקרוסופט, המליצו לנו להתקין את ה-VS 2008 SP1 Beta 1 שבו כביכול ניתן פתרון לבעיה (ראו קישור). שמחים וטובי לבב התקנו את הגרסה - דבר שלכשעצמו מסתבר כבעייתי, במיוחד כאשר אין למחשבים חיבור לאינטרנט.

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

 

אבל ... מתברר שהפעולה הזו עובדת רק בסריאליזציה ל-XML (מתווסף Attribute תחת xmlns אחר שמחזיק את ה-KEY), אבל מעיפה Exception כאשר מנסים לבצע סריאליזציה ל-JSON עם DataContractJsonSerializer (הגדרה של DataMember באופן שלא מאפשר סריאליזציה של האובייקטים).

אז כרגע אנחנו עובדים כמו חוראנים, מבצעים סריאליזציה עם JavaScriptSerializer של AJAX (שכמובן מתעלם מ-DataMemberAttribute) ודה-סריאליזציה עם DataContractJsonSerializer. אנחנו משתמשים באחרון כי הוא מאפשר לנו שליטה יותר טובה עם הדה-סריאזליזציה מאשר זה של AJAX.

על הבעיות בשילוב שני ה-Serializer-ים - בפעם הבאה ...

התקנת VS 2008 SP1 Beta 1 על מחשבים ללא חיבור לאינטרנט

למי שתוהה, ההתקנה של SP1 שניתנת להורדה מכאן לא באמת שוקלת "רק" 451 KB אלא למעשה היא רק Bootstrap להתקנה ששוקלת 350 מגה (תלוי בסוג המעבד) שיורדים מהאינטרנט אוטומטית בהתחלת ההתקנה.

מי שירצה להתקין את ה-SP1 על מחשב שאינו מחובר לאינטרנט, בוודאי יחפש כאן הסבר כיצד לעשות זאת (תחת הנושא "Administrator deployment of Visual Studio 2008 SP1 Beta") וישים לב שיורדים לו 555 מגה של קבצי התקנה (לכל הגרסאות האפשריות של מעבדים).

מה שלא כתוב במסמך הזה הוא שההרצה של ההתקנה כפי שהיא מופיעה במסמך אינה Offline לחלוטין ולמעשה מורידה את כל הרכיבים למעט התקנת Framework 3.5 SP1 Beta 1, התקנה שלכשעצמה שוקלת 230 מגה ושאותה תוכנית ההתקנה מנסה עדין להוריד מהאינטרנט למרות שזו אמורה להיות התקנת Offline.

הפתרון, אתם מוזמנים לרשום לעצמכם :

  1. הורדה של ה-Bootstrap מכאן
  2. התקנה כפי שמופיע בהסבר עבור Admin deployment כאן
  3. הורדה של Framework 3.5 SP1 מכאן
  4. דריסה של הקובץ dotnetfx35setup.exe ששוקל 2.79 מגה בקובץ שהורדתם בסעיף 3 (הקובץ נמצא תחת תיקיית ה-Layout שניתנה כדוגמה במסמך, אל תשכחו לשנות את שם הקובץ לכפי שהוא מופיע בתיקיית היעד)
  5. לשים את ההתקנה המלאה בשרת קבצים ברשת, כך שכולם יוכלו למשוך ולהתקין

זהו, 451KB שהפכו ל-782 מגה.

בהצלחה.