DCSIMG
September 2009 - Posts - שחר.נט

שחר.נט

בלוגים שאני קורא

ספרים מומלצים

September 2009 - Posts

גרסא חדשה: DTOGenerator – מחולל קוד המייצר Data Transfer Objects על בסיס LINQ to SQL Data Classes ו ADO.NET Entity Framework Model

בפוסט הקודם הצגתי את LINQ2DQLDTOCreator, אפליקציה קטנה שמייצרת Data Transfer Objects על בסיס LINQ to SQL Data Classes. היום ביצעתי קצת שיפורים באפליקציה והרחבתי אותה כך שהיא תייצר DTO’s גם ממודל של ADO.NET Entity Framework.

האפליקציה הנוכחית כוללת את כל היכולות של האפליקציה הקודמת. כלומר, עדיין ניתן לייצר DTO’s על בסיס LINQ to SQL Data Classes. אבל, עכשיו האפליקציה גם תייצר DTO’s על בסיס entities, כלומר מחלקות עם EdmEntityTypeAttribute.

בנוסף, כאשר עובדים מול ADO.NET Entity Framework האפליקציה גם מתייחסת ל relationships בין entities שונים. כלומר, אם במודל מוגדר של entityיש קשר של יחיד ליחיד עם ישות אחרת, הדבר ייוצג בם ב DTO (כמובן, באמצעות קשר ל-DTO הרלוונטי). גם קשר של יחיד לרבים ייוצג. למשל, הנה ה DTO שנוצר עבור הישות Products מהמסד נתונים Northwind:

using System;
using System.Data.Linq;
using System.Linq;
using System.Collections.Generic;
/*
This Data Transfer Object created Automatically by DTOGenerator.
You can download DTOGenerator and get support in the project site: http://dtogenerator.codeplex.com 
DTOGenerator developed by Shahar Gvirtz (http://weblogs.asp.net/shahar)
*/
 
namespace DTO
{
    public class ProductDTO
    {
        public static ProductDTO GetDTOFromDALObject(ConsoleApplication3.Product src, bool GetChilds)
        {
            ProductDTO obj = new ProductDTO();
            obj.Discontinued = src.Discontinued;
            obj.ProductID = src.ProductID;
            obj.ProductName = src.ProductName;
            obj.QuantityPerUnit = src.QuantityPerUnit;
            obj.ReorderLevel = src.ReorderLevel;
            obj.UnitPrice = src.UnitPrice;
            obj.UnitsInStock = src.UnitsInStock;
            obj.UnitsOnOrder = src.UnitsOnOrder;
            if(src.Category != null && GetChilds)
                obj.Category = CategoryDTO.GetDTOFromDALObject(src.Category,false);
            if(src.Order_Details != null && GetChilds)            {
                List<Order_DetailDTO> Order_DetailDTOlst = new List<Order_DetailDTO>();
                src.Order_Details.ToList().ForEach(p=>Order_DetailDTOlst.Add(Order_DetailDTO.GetDTOFromDALObject(p,false)));
                obj.Order_Details = Order_DetailDTOlst;
            }
 
            if(src.Supplier != null && GetChilds)
                obj.Supplier = SupplierDTO.GetDTOFromDALObject(src.Supplier,false);
 
            return obj;
        }
        public ConsoleApplication3.Product GetDALObject(bool IncludeChilds)
        {
            ConsoleApplication3.Product obj = new ConsoleApplication3.Product();
            obj.Discontinued = Discontinued;
            obj.ProductID = ProductID;
            obj.ProductName = ProductName;
            obj.QuantityPerUnit = QuantityPerUnit;
            obj.ReorderLevel = ReorderLevel;
            obj.UnitPrice = UnitPrice;
            obj.UnitsInStock = UnitsInStock;
            obj.UnitsOnOrder = UnitsOnOrder;
            if(Category != null && IncludeChilds)
                obj.Category = Category.GetDALObject(false);
            if(Order_Details != null && IncludeChilds)            {
                System.Data.Objects.DataClasses.EntityCollection<ConsoleApplication3.Order_Detail> Order_Detailsgetdallst = new System.Data.Objects.DataClasses.EntityCollection<ConsoleApplication3.Order_Detail>();
                Order_Details.ForEach(p=>Order_Detailsgetdallst.Add(p.GetDALObject(false)));
                obj.Order_Details = Order_Detailsgetdallst;            }
 
            if(Supplier != null && IncludeChilds)
                obj.Supplier = Supplier.GetDALObject(false);
 
 
            return obj;
        }
        
        public Boolean Discontinued { get; set; }
        public Int32 ProductID { get; set; }
        public String ProductName { get; set; }
        public String QuantityPerUnit { get; set; }
        public Int16? ReorderLevel { get; set; }
        public Decimal? UnitPrice { get; set; }
        public Int16? UnitsInStock { get; set; }
        public Int16? UnitsOnOrder { get; set; }
        public DTO.CategoryDTO Category { get; set; }
        public List<DTO.Order_DetailDTO> Order_Details { get; set; }
        public DTO.SupplierDTO Supplier { get; set; }
 
 
    }
}

בנוסף, קיימת האפשרות לא לטעון את האובייקטים שקיים קשר אליהם מה DTO, באמצעות העברת false למתודות העזר GetDTOFromDALObject ו- GetDALObject שקיימות בכל DTO.

כדי לאפשר מעקב קל יותר אחרי גרסאות, יצרתי פרוייקט ב codeplex שנקרא DTOGenerator.

ניתן להוריד את הגרסא הראשונית החדשה (שתומכת גם ב LINQ to SQL וגם ב ADO.NET Entity Framework) מכאן.

בהצלחה.

פרסום: אפליקציית קוד פתוח קטנה ושימושית ליצירת Data Transfer Objects באופן אוטומטי על בסיס LINQ to SQL Data Classes

הקדמה

באחד הפוסטים הקודמים כתבתי על Data Transfer Objects והשימוש בהם לעבודה נכונה ונוחה עם LINQ to SQL ו- ADO.NET Entity Framework.
בין התגובות שקיבלתי בפורום “תכנות .NET” בתפוז שבתגובה לשאלה שעלתה בו פורסם הפוסט, עלה העניין שלמרות שמדובר בכתיבה יותר נוחה, היא דורשת למעשה עבודה כפולה בהתחלה, וגם ציינתי זאת בפוסט. כשמתחילים לכתוב, יש צורך לכתוב DTO נפרד לכל אחת מהישויות עם תחילת העבודה, וגם בד”כ לכתוב מתודות שמבצעות העברה בין הDTO לאובייקט שנמצא ב DAL (האובייקט שמופה ע”י הORM) ולהיפך – לכתוב מתודה שמחזירה את ה DTO בהתאם לאובייקט שנמצא ב DAL. לטעמי, לא מדובר בהרבה עבודה, אבל יש כאלה שזה מפריע להם.

למי שהקצת יותר כתיבת קוד זה מה שמפריע לו, כתבתי היום במסגרת העבודה בסטארטאפסידס Code Generator קטן, שיוצר על סמך DataClasses של LINQ to SQL אובייקטים של DTO מקבילים ותואמים.

LINQ2SQLDTOCreator – האפליקציה עצמה

האפליקציה שכתבתי, ששמה הלא-מקורי-בעליל מופיע בכותרת של הפיסקה (LINQ2SQLDTOCreator), מקבלת נתיב לאסמבלי כלשהו (קובץ DLL של האפליקציה, למשל) ונתיב של המקום אליו היא תפלוט את התוצר שלה. האפליקציה עוברת על כל המחלקות ב DLL ומתעלמת מכולן למעט ממחלקות שמסומנות באטריביוט: System.Data.Linq.Mapping.TableAttribute שמסמל אובייקט שמייצג למעשה מיפוי של טבלה מהדטאבייס. 
בכל אחת מהמחלקות הללו עוברת האפליקציה על כל ה properties שמסומנים באטריביוט System.Data.Linq.Mapping.ColumnAttribute שמסמן מיפוי של ה property לעמודה כלשהי בדטאבייס.
בסופו של דבר, מייצרת האפליקציה עבור כל אובייקט בDAL שממופה לטבלה, מחלקה (.cs) שמכילה הגדרות של properties עבור כל אחד מה types (כאשר ה properties הם מאותו ה type שהוגדרו ב DAL – במידה שהם מוגדרים ב database כ Allow Null, אז הם יוגדרו גם ב DTO כ Nullable Types). בנוסף, במחלקה הזאת, כלולות שתי מתודות באופן אוטומטי: מתודה סטאטית שמקבלת מופע מה type של האובייקט שבDAL שממופה לטבלה המקורית ומחזירה DTO, ומתודה שמביאה עבור המופע של הDTO את האובייקט DAL התואם, עם הערכים המתאימים (זה אמנם לא חלק מההגדרה של DTO, אבל שילבתי את זה לשם הנוחות. כמובן, שזה דורש שאם ה DTO בפרוייקט נפרד, אז יהיה רפרנס לDAL, כדי שאפשר יהיה ליצור אובייקט של הDAL ולקבל אותו).
לדוגמא, הנה קובץ שנוצר על-ידי האפליקציה:

   1: using System;
   2: /*
   3: ------------------------DTO OBjECT-----------------------------
   4: --------------Generated By LINQ2SQLDTOCreator------------------
   5: ----------------Developed by Shahar Gvirtz---------------------
   6: ---------http://blogs.microsoft.co.il/blogs/shahar-------------
   7: ---------------http://weblogs.asp.net/shahar-------------------
   8: */
   9:  
  10: namespace DTO
  11: {
  12:     public class VideoCategoryDTO
  13:     {
  14:         public static VideoCategoryDTO GetDTOFromDALObject( DAL.VideoCategory src )
  15:         {
  16:             VideoCategoryDTO obj = new VideoCategoryDTO ();
  17:             obj.ID = src.ID;
  18:             obj.Name = src.Name;
  19:             obj.Sorting = src.Sorting;
  20:  
  21:             return obj;
  22:         }
  23:         public DAL.VideoCategory GetDALObject()
  24:         {
  25:             DAL.VideoCategory obj = new DAL.VideoCategory ();
  26:             obj.ID = ID;
  27:             obj.Name = Name;
  28:             obj.Sorting = Sorting;
  29:  
  30:  
  31:             return obj;
  32:         }
  33:         
  34:         public Int32 ID { get; set; }
  35:         public String Name { get; set; }
  36:         public Int32 Sorting { get; set; }
  37:  
  38:  
  39:     }
  40: }


השימוש באפליקציה הזאת פשוט יחסית:

  1. הורידו את האפליקציה
  2. הקובץ שהורדתם כולל את הקוד המלא. בתוך תיקיית הפרוייקט, בנתיב \bin\release תמצאו את קבצי ההרצה הנדרשים. העתיקו את כולם לאיזושהי תיקייה עם נתיב קצר לשם הנוחות.
  3. האפליקציה היא Console Application. בהרצה, היא מקבלת שני פרמטרים מופרדים ברווח. הראשון, הוא הנתיב ל DLL (אם הוא מכיל רווחים, יש לעטוף אותו במרכאות). השני, הוא הנתיב שבו רוצים את הפלט של הקבצי קוד שנוצרו. למשל:
       1: c:\myApp\LINQ2SQLDTOCreator.exe "c:\Dev\App\bin\debug\logic.dll" "c:\outputfromapp"

    שימו לב שהפרמטרים מופרדים עם רווח יחיד.
  4. האפליקציה תרוץ, ובתיקייה שהכנסתם תמצאו קבצים כמספר האובייקטים שממופים לטבלאות אצלכם באסמבלי. כל קובץ מורכב מהשם המקורי בסיומת DTO. את הקבצים האלה עליכם לשלב בפרוייקט (copy paste) במקום שאתם רוצים.
    זכרו שאם אתם שמים בפרוייקט נפרד, יש צורך שהוא יכיל רפרנס לפרוייקט שמכיל את ה LINQ to SQL Data Classes.
  5. בהמשך, כל פעם שאתם מבצעים שינוי, אתם יכולים להריץ את האפליקציה שוב כדי לקבל DTO’s מעודכנים.

מגבלות שימוש ידועות

  • האפליקציה אינה תומכת בקשרים בין האובייקטים. אם קיים Relationship בדטאבייס, בתהליך יצירת ה DTO יווצרו properties מתאימים לID בטבלה האחרת, אבל לא יהיה קיים קשר בין האובייקטים השונים.
  • האפליקציה מסוגלת לעבוד עם assemblies שה Target Framework שלהם הוא .NET 3.5. היא לא מסוגלת לעבוד עם אסמבליס שה target framework שלהם הוא 4. ההסבר לזה מופיע פה. מי שממש חשוב לו, פשוט צריך לפתוח את הפרוייקט, להגדיר לו target framework של 4 והכל יעבוד טוב. לטובת כאלה שהם לא פריקים של בטות של מיקרוסופט, אני בניתי אותו ב-3.5.
  • האפליקציה לא משלבת אוטומטית את הקבצים בפרוייקט ולא דואגת לנושאי רפרנסים וכו’.

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

החלק הטכני – מה קורה מאחורי הקלעים?

כפי שכבר כתבתי בחלק הקודם, בגדול, מדובר ב Reflection לא מסובך במיוחד. האפליקציה מוצאת בהתאם ל attributes את הנתונים הרלוונטיים מבחינתה (השימוש ב attribute הוא כדי להבחין בין סתם class ל class של ה LINQ to SQL Data Classes. כעיקרון, ה properties היחידים שקיימים ב classes שנוצרים על ידי ה LINQ to SQL הם properties שממופים לעמודות. אבל, בכל זאת אני בודק בהתאם ל attributes, למקרה שהשמשתמש בחר לעשות שינויים בקבצים הללו בעצמו.

באפליקציה קיים Resource שמכיל קובץ טקסט שמהווה למעשה את התבנית למחלקות שנוצרות באמצעות המחולל. בתבנית יש מספר markers (מתחילים בסולמית) שלמקומות האלה מוכנסים (באמצעות replace פשוט) הערכים שנווצרים ע”י האפליקציה על סמך ה reflection.

מבחינת השימוש ב reflection – רובו פשוט למדיץ. הנקודה היחידה שייתכן שפחות מוכרת הוא העבודה מול ה Nullable types. כעיקרון, מי שלא יודע, nullable types זה למעשה מופעים של Nullable<T>. על-מנת לבדוק האם ה type של פרופרטי מסויים הוא nullable או לא, אני בודק האם מדובר ב type גנרי, והאם הוא שווה ל Nullable:

   1: if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Nullable<>))
   2:  

הסיבה לבדיקה, דרך אגב, היא כי אם אני ישר מדפיס את ה type באמצעות ה property שנקרא name, במידה ומדובר ב type גנרי הוא יודפס בצורה חסרת משמעות – למשל, Nullable’ (עם גרש בסוף). על-מנת שהקוד כן יעבור קומפילציה, אני משנה בידת הצורך את הסינטקס לסינטקס סי-שארפי.

סיכום

האפליקציה שהוצגה היא אפליקציה פשוטה, המשתמשת ב Reflection, על-מנת לייצר מחלקות Data Transfer Objects על סמך LINQ to SQL Data Classes. האפליקציה יכולה להיות שימושית מאד למפתחים המשתמשים ב LINQ to SQL ובORM-ים אחרים.

ניתן להוריד את האפליקציה מכאן.

שחר.

טיפ קצר: ייצוא מלא של הנתונים ב DB כולל סכמה ונתונים

אפשרות חמודה שקיימת ב SQL Manegment Studio 2008 היא להגדיר לאופציית ה” GEnerate Scripts” של הדטאבייס גם לייצא את הנתונים (כלומר, לייצר משפטי INSERT לטבלאות).

האפשרות הזאת לא קיימת בגרסא 2005 (יש Generate Scripts – אין Data – כלומר שלמדתי על בשרי כשלא הבנתי לאן נעלמה האפשרות), אבל אין שום מגבלה על התקנה ושימוש ב Management Studio 2008 גם מול instance של SQL Server 2005.

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

אז, איך עושים את זה?

  1. קליק ימני על הדטאבייס שרוצים לגבות, בחירה ב Tasks->Generate Scripts
  2. בשלב הבא בוחרים את הדטאבייס שממנו רוצים לגבות. אפשר לסמן כבר בשלב זה את ה checkbox שאומר Script all objects in the selected database. במידה שלא מסמנים אותו, יהיה צריך בהמשך לבחור מה מייצאים מה DB.
  3. מסך הבא הוא מסך האפשרויות. הרלוונטיות בינהן לרוב המשתמשים הן: Script Database Create (האם לייצר גם שאילתת Create ל-DB. טוב להפעלה הראשונה, אבל אם אתם מייצאים פריטים בודדים – לא תרצו את זה), Script for Server Version – מה גרסאת היעד שלה אמורים הסקריפטים להתאים, Script USE DATABASE – האם הסקריפט יכלול משפט Use ל DB מסויים (עלול להפריע אם שם ה DB בשרת היעד לא תואם לשם ה DB שממנו אתם מייצאים), Script DATA – האפשרות העיקרית שבגללה אני כותב את הפוסט הזה - האם לכלול משפטי INSERT לנתונים.
  4. בשלב הבא, אם סימנתם לייצר סקריפטים עבור כל האובייקטים ב DB, תעברו ישר לבחירה לאן לייצא את הסקריפט. אחרת, תידרשו קודם לבחור מה האובייקטים ב DB עבורם יווצרו סקריפטים.
  5. השלב האחרון הוא סיכום, שבו אפשר לראות את האופציות שנבחרו.

בהצלחה.

טיפ קצר: גישה ל Controls הנמצאים ב FooterTemplate של Repeater

בעקבות שאלה שקיבלתי, כמה מילים על FooterTemplate ב Repeater: ה FooterTemplate מאפשר לקבוע תבנית עיצובית שתופיע בתחתית ה Repeater לאחר כל ה DataBound Items.

ניתן, כמובן, כמו כל מקום, לשלב שם כל Control שאתם רוצים. וכעת, לשאלה – איך ניגשים אליהם בלי להשתמש באף אחד מה events של ה Repeater? קצת מגושם, לא מורכב. בדוגמא הזאת, נניח של Repeater קוראים rpSample, ל control שאחנו מחפשים קוראים calDate והוא מהסוג Calendar. ככה הקוד ייראה:

 
Calendar myCal = (Calendar)rpSample.Controls[rpSample.Controls.Count - 1].Controls[0].FindControl("calDate")
 
 

בהצלחה.

טיפ לעבודה מול LINQ to SQL ו- ADO.NET Entity Framework: שימוש ב FactoryMethod כדי לקבל מופע של ה DataContext או ה Entities

כשעובדים מול LINQ to SQL או ADO.NET Entities Framework, אנחנו עובדים הרבה מאד מול אובייקט ה DataContext / Entities.
דוגמא מאד נפוצה בדוגמאות ובמדריכים של הטכנולוגיות הללו היא ליצור מופע של המודל בכל מתודה וכל פעם שמשתמשים בו. למשל:

   1: public static void Method1()
   2: {
   3:     using (var data = new DataClassesDataContext())
   4:     {
   5:         //Code   
   6:     }
   7: }
   8: public static void Method2()
   9: {
  10:     using (var data = new DataClassesDataContext())
  11:     {
  12:         //Code   
  13:     }
  14: }

וכך יש הרבה מתודות, כל אחת עושה new DataClassesDataContext. ואז מגיעה היום שבו רוצים לעשות איזשהו שינוי מינורי. רוצים לעשות connection string שונה בהתאם לסביבה או בהתאם לאיזשהו תנאי. איך עושים את זה? רמז – במקרה הזה, זה כנראה יהיה (במקרה הטוב) find&replace והרבה עבודת ידיים (במקרה הרע). וזה עוד לפני שדיברנו על דברים יותר מורכבים.

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

   1: public static DataClassesDataContext GetDataContextInstance()
   2: {
   3:     return new DataClassesDataContext();
   4: }

ואז לשנות את המתודות בהתאם, שבמקום לעשות new DataClassesDataContext הן יעבדו עם המתודה הזאת. בצורה הזאת, נניח אם מחר צריך לעשות Connection String שונה בהתאם לצורך, או אפילו שינויים יותר משמעותיים – הכל מרוכז במקום אחד.

בהצלחה.

יצירת Thumbnail (צלמיות) בקלות

אם רוצים ליצור צלמית של תמונה (גרסא מוקטנת שלה), ניתן כמובן לחתוך אותה באופן ידני. אולם, למי שלא מכיר (והרבה, כנראה, לא מכירים) הפונקציונאליות הזאת כבר מובנת ב BCL במחלקה System.Drawing.Image.

השימוש במתודה הזאת, GetThumbnailImage מאד פשוט. הנה לדוגמא קוד של Console Application שיוצר גרסא מוקטנת לכל התמונות בפורמט jpg שבתיקייה:

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

   1: ...
   2: using System.Drawing;
   3: using System.IO;
   4: ...
   5:  
   6: string dir = @"C:\Users\Public\Pictures\Sample Pictures";
   7: int height = 60;
   8: int width = 60;
   9: string[] Images = Directory.GetFiles(dir, "*.jpg");
  10: foreach (var image in Images)
  11: {
  12:     Image.FromFile(image)
  13:         .GetThumbnailImage(width, height, delegate() { return false; }, IntPtr.Zero)
  14:         .Save(string.Format(@"c:\tmp\{0}",Path.GetFileNameWithoutExtension(image) + "_thumb.jpg"));
  15: }

בשורות 6-8 אני מגדיר hard-coded את המידע שאני רוצה (dir היא תיקיית התמונות המקורית, ו height ו width מעידים על עצמם).
בשורה 9, אני מקבל את כל הקבצים שהסיומת שלהם jpg ונמצאים בתיקייה שציינתי.
לאחר מכן, אני עובד על כל הקבצים כאשר, בשורות 12-14 אני מבצע את הדברים הבאים: עבור כל נתיב, אני מקבל System.Drawing.Image. ובאמצעות המתודה GetThumbnailImage (שמקבלת רוחב, גובה, delegate ואת הערך של IntPtr.Zero שחייב להיות ככה) אני מקבל אובייקט Image נוסף, המכיל את התמונה המוקטנת, שאותו אני שומר לדיסק.

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

בהצלחה.

----------------------------

תודה, תודה, תודה לשלומי בן שמואל שהמליץ לי על התוסף הזה לכתיבת קוד. סוף סוף משהו שבאמת עובד!

SELECT SCOPE_IDENTIFY() עם LINQ to SQL ו- ADO.NET Entity Framework

כאשר מוסיפים שורה לטבלה המכילה איזושהי עמודה בשם ID שמוגדרת, לצורך העניין, כ Primary Key, פעמים רבות נרצה לקבל מיד אחרי הוספת הרשומה את ה ID שהוקצה לה.
באמצעות T-SQL, זה היה נעשה עם SCOPE_IDENTIFY(), בצורה הזאת:

   1: SELECT SCOPE_IDENTIFY() AS [NewID]

זה היה מגיע לאחר ה INSERT, היינו מקבלים את הID וכולם היו שמחים ומאושרים. היום ראיתי דוגמאת קוד שבה מיד לאחר הוספת השורה ב LINQ to SQL וביצוע SubmitChanges, הולכים לבטלה ושולפים את השורה האחרונה בה כדי לקבל את הID. בסופו של דבר, התהליך הזה מיותר.

ברגע שמשתמשים במתודה InsertOnSubmit ומעבירים לה אובייקט שלו מוגדר, למשל, Primary Key מסוג int שמוגדר כ IsIdentify = True, לאחר ביצוע SubmitChanges ערך הID החדש יוכנס לאובייקט. למשל:

   1: using (var data = new DataClassesDataContext())
   2: {
   3:     Event ev = new Event(); //כאשר מדובר באובייקט שממופה לטבלה בדטאבייס
   4:     ev.Prop1 = "value1";
   5:     ev.Prop2 = "value2"
   6:     ....
   7:     data.Events.InsertOnSubmit(ev);
   8:     data.SubmitChanges();
   9:     Console.WriteLine(ev.ID); //ID is PK
  10: }

אז, כאשר ID לא הוגדר ידנית, אבל הוא מוגדר באמת כ Primary Key, אז בעת ההוספה ל DB, מאחורי הקלעים באמת יישלף ה SCOPE_IDENIFY ויושם אוטומטית ל property של הPrimary Key באובייקט שהוספנו. נוח ושימושי.

בהצלחה.

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

LINQ to SQL/ADO.NET EF, מודל השכבות ו Data Transfer Objects

בפורום “תכנות .NET” בתפוז, עלה לאחרונה נושא השילוב בין ADO.NET Entity Framework ומודל השכבות. ובאמת, אם נסתכל על הדוגמאות הנפוצות באינטרנט, מאד קל לראות עמודים ששמכילים ב code behind שלהם שאילתות LINQ שמקומם ב DAL ופעולות לוגיות שונות שמקומם ב BLL. בסופו של דבר, יש משהו מאד מפתה בנוחות שעלול לגרום ליצירת קוד מבולגן.

אם תחשבו על אפליקציה הבנוייה עפ”י מודל השכבות, מאד ברור לנו מה אמור להיות ב DAL ומה אמור להיות ב BLL ומה אמור להיות בGUI, למשל. אבל, השאלה היא, כשהאפליקציה מתבססת על LINQ to SQL/ ADO.NET EF כשכבת ה DAL, היא איך אנחנו מעבירים את המידע בין השכבות. שיטה שמאד מאד מקובלת בכל מיני דוגמאות באינטרנט, היא להשתמש באובייקטים שמופו מה DB ע”י ה ORM כBO’s ולמעשה, אותם להעביר בין השכבות.
שיטה זו בעייתית. דבר ראשון, האובייקטים שנוצרים באמצעות המיפוי של הORM-ים האלה עמוסים בהרבה מאד… זבל (לפחות, עד הגרסא הבאה של EF שתתמול בעבודה עם POCO). כלומר, דברים מאד מאד חשובים, אבל לא דברים שחשובים לאובייקטים ב BO שלנו. הם גם יורשים וממשים כל מיני דברים שיכולים “לבלגן” את האפליקציה שלנו (כל מתודות ה OnInsert וכו’).
הבעייתיות השנייה, היא ששימוש באובייקטים האלה כ-ORM כופה עלינו תלות משמעותית מדי ב ORM. אנחנו מסתמכים על האובייקטים שהוא מייצר בצורה שתגרום לכך שאם מתישהו נצטרך להחליף, התיקונים שלנו יהיו הרבה מעבר לתיקונים בDAL, אלא נצטרך לתקן גם ב BLL וגם ב GUI. או לחלופין, במידה שנחליף, נצטרך לעשות שמיניות באוויר כדי לשמור על איזושהי תאימות.
וחוץ מזה, נוצרת איזושהי פגיעה בהפרדה בין השכבות כשלGUI יש רפרנס ישיר ל DAL. המצב האידיאלי מבחינתי, הוא שלGUI יש רפרנס ל BLL שמחזיק בתורו רפרנס ל DAL ושלכולם יש רפרנס לBO’s.

אז, מה עושים? מה צורת העבודה המומלצת?
התשובה לזה קצת מורכבת, כי יש הרבה שיטות. השיטה שלי היא להשתמש ב DTO’s שגם משמשים כסוג של BO.
Data Transfer Objects הם אובייקטים שהייעוד העיקרי שלהם הוא העברת מידע בין השכבות. הם כמעט תמיד לא יכילו שום פונקציונאליות, למעט הצהרה על properties עם getters ו setters.

דוגמא מעשית:

כאשר משכבת ה GUI אני רוצה להוסיף, נניח, מאמר. לחיצה על כפתור ההוספה תגרור יצירת מופע של BO.ArticleDTO (אובייקט, שתפקידו להחזיק את המידע). המופע הזה יועבר כפרמטר למתודה שנמצאת, נניח, ב BLL.Articles.Insert. בBLL, למשל, אני אשלח אימייל על המאמר החדש שהתווסף לרשימת התפוצה, ואקרא ל DAL.Articles.Insert שאעביר אליו גם כן את אותו המופע של BO.ArticleDTO. בשכבת ה DAL, אני אצור מופע של DAL.Article (שהוא האובייקט שיצר ה ORM ושאיתו הוא עובד, כי הוא הרי לא מכיר את הDTO שלי) ואבצע את ההוספה.
במקרה של שליפה, מה ששכבת ה DAL תחזיר הלאה, זה גם כן מופע של BO.ArticleDTO.

חוץ מהעובדה ששימוש בשיטה הזאת פותר, למעשה, את הבעיות שציינתי כסיבה לא להשתמש באובייקטים שיוצר ה ORM, הוא גם נותן כמה יתרונות נוספים: דבר ראשון, ה DTO לא צריך לחפוך בשום צורה שהיא למודל ב DB, אין שום חובה שכל הנתונים שבו יהיו מקושרים ל DB, ולמעשה יש חופש מוחלט לגבי המבנה שלו.
מעבר לכך, במידה שקורה איזשהו שינוי בשכבת ה DAL, אין שום צורך לגעת באף שכבה עליונה יותר – כולם מכירים את ה DTO ועובדים מולו, והוא מאפשר ליצור איזושהי חציצה בין השכבות.

כמובן, שלשיטה הזאת יש חסרון – צריך, בפעם הראשונה, לעשות עבודה כפולה. ליצור את ה DTO. אבל, כבר לא פעם השיטה הזאת חסכה לי לא מעט עבודה בהמשך ואני ממליץ עליה בחום כשיטת עבודה כשעובדים עם ADO.NET EF או עם LINQ to SQL, לפחות עד ש EF יציג תמיכה סבירה בעבודה עם POCO.

בהצלחה.