הכירו את Entity Framework 4.1

מבוא

לאלו שלא מכירים, ADO.Net Entity Framework (בהמשך EF) היא המילה האחרונה בתחום הORM מבית מיקרוסופט. EF מאפשר למפתחים להגדיר בצורה נוחה את עולם הישויות שלהם (Conceptual Model) את הטבלאות שלהם (Storage Model) ואת המיפוי ביניהם (Mapping Model) כששלשתם יחד יוצרים את הEDM (ר"ת Entity Data Model). כמו"כ הוא מספק את הממשקים הן לאחזור ישויות מבסיס הנתונים והן לעדכון ושמירה שלהם לבסיס הנתונים בחזרה.

כמו ששמתם לב, בעולם המושגים של EF, המחלקות אינן מייצגות רשומות במסד הנתונים, אלא הנן "ישויות" הממופות לנתונים באמצעות הEDM. בדרך זו, כל ישות יכולה להיות ממופה ליותר מטבלה אחת, וכן לדוגמה להשתמש בStored Procedure לפעולות CRUD (ר"ת Create, Read, Update, Delete).

אם טרם יצא לכם לעבוד עם EF תוכלו להכיר את היסודות דרך המאמרים המצוינים של עידו פלטו "הכירות עם EF" וסדרת המאמרים של הרה"ג דוט נט המתאימה במיוחד ללימוד עצמי.

מה התחדש בEF 4.1?

החידושים שבגרסה החדשה כולם נשענים על הAPI החדש שחושפות המחלקות  System.Data.Entity.DbContext וSystem.Data.Entity.DbSet (את הרשימה המלאה תוכלו למצוא בMSDN).

אחד השיפורים המשמעותיים ביותר בEF 4.1 הוא תמיכה בגישת Code First. גישת Code first מאפשרת להגדיר את המודל כולו במחלקות C# או VB.NET רגילות ולהוסיף או לשנות קשרים בין מחלקות ע"י שימוש בAPI של DbContext. והחשוב מכל, EF יידע לבד ליצור מסד נתונים המתאים לשדות והקשרים שהגדרתם באמצעות המחלקות.

כדי להבין את מהות הגישה נעיין בהיסטוריה של EF והORMים המסורתיים:

.NET 3.5 SP1 Entity Framework

EF בגרסתו הראשונה תמך רק בגישה Database first, כלומר מתחילים מיצירת מסד נתונים וממנו הVisual Studio יודע ליצור EDM וקוד .NET. גישה זו היא גם היחידה שנתמכת בlinq2sql.

image003
image002

.NET 4.0 Entity Framework 4.0

בVisual Studio 2010 נוספה תמיכה גם בגישת Model first. כלומר, ניתן להתחיל מיצירת המודל, וממנו VS יידע לייצר טבלאות במסד הנתונים ואת הקוד.

.NET 4.0 Entity Framework 4.1

הגרסה האחרונה של EF, מביאה את את בשורת הCode first. מעתה, אפשר להתמקד בפיתוח הDAL ולתת לEF 4.1 למפות את המחלקות הDAL שיצרנו לEDM ובסופו של דבר, לאחד ממסדי המנונים הנתמכים ע"י EF 4.1 והכל בזמן ריצה!

image001

חשוב להדגיש: שלא כמו בשני הגישות האחרות, בגישת Code first הEDM אינו "מפורש", כלומר לא נוצרים קבצי edmx בפרוייקט, אלא הEDM "נגזר" ממבנה המחלקות באופן לא מפורש בזמן ריצה. כמו"כ, שבגישת Code first לא משתמשים בכלי או אשף כדי ליצור את מסד הנתונים, אלא גם מסד הנתונים נוצר בזמן ריצה (אם הוא לא היה קיים קודם) ע"י המחלקה System.Data.Entity.Database.

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

הדגמה

נדגים את גישת Code first ע"י יצירת אפליקציה של חנות פשוטה. החנות מכילה מוצרים המחולקים לקטגוריות ומאפשרת לבצע הזמנה של מוצר.

איך מתחילים?

קודם כל, אם עוד לא התקנתם יש להוריד והתקין את Entity Framework 4.1.

שנית, צריך להוסיף לפרויקט שלכם references לספריות של EF:

references

עכשיו כשהכל מוכן לעבודה, נתחיל מטיוטה ראשונית של מחלקות הDAL שלנו:

public class Category
{
    public int Id { get; set; }
    public string Title { get; set; }

    public virtual ICollection<Product> Products { get; set; }
}

public class Product
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }

    public virtual Category Category { get; set; }
}

public class Order
{
    public int Id { get; set; }
    public int Quantity { get; set; }
    public string BuyerName { get; set; }

    public virtual Product Product { get; set; }
}

public class StoreContext : DbContext
{
    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
}

כפי שניתן לראות אלו מחלקות POCO, בלי שום מודעות לEF. למעט המחלקה StoreContext היורשת מDbContext (ראה המשך).

Conventions

אני אקפוץ קצת קדימה ואציג את מבנה הטבלה שEF יצר עבור המחלקה Product:

products_nullable

אפשר לראות, שEF הסיק אוטומטית שאני רוצה שהמאפיין Id יהיה מפתח ראשי של הטבלה וכן יצר קשר Foregin Key לCategory. פירוש זה מתאפשר כי EF מבין conventions (מבני קוד מקובלים). ביניהם:

  • מאפיין בעל שם Id או ID יהיה המפתח הראשי של הטבלה
  • מאפיין virtual מטיפוס של מחלקה אחרת (במקרה שלנו Category במחלקה Product) ייצור Foregin Key לטבלה השנייה
  • מאפיין virtual מטיפוס של ICollection<T> כשT היא מחלקה אחרת (במקרה שלנו Product במחלקה Category) מבטא קשר יחיד לרבים (מספר Product לכל Category)
  • כל שדה יכול לקבל Null או בSQL – להיות Nullable
  • מסד הנתונים ייוצר ב.\sqlexpress וייקרא על שם המחלקה שמייצגת את הDbContext (במקרה שלנו: StoreContext)

כל ההגדרות הנ"ל יכולות להיות משוכתבות ע"י עוד 2 פקטורים הנלקחים בחשבון ע"י הEF בעת קביעת תצורת הטבלאות: Data Annotations וDbContext Fluent API. כשסדר העדיפות שלהם הוא כדלהלן (כל המאוחר דורס את קודמיו):

  1. Conventions
  2. Data Annotations
  3. DbContext Fluent API

Data Annotations

שני דברים לא מוצאים חן בעיניי בטבלה שייצר הEF ע"פ הconventions. האחד - השדה Title לא צריך להיות Nullable, כמו"כ השדה Category_Id. עכשיו נשנה את הדברים האלו בעזרת הData Annotations.

אנו נשתמש באטריבוט RequiredAttribute ממרחב שמות System.ComponentModel.DataAnnotations כדי לציין שאנחנו רוצים ששתי השדות הנ"ל לא יהיו Nullable. נשנה את המחלקה Product להיראות כך:

public class Product
{
    public int Id { get; set; }

    [Required]
    public string Title { get; set; }

    public string Description { get; set; }

    public decimal Price { get; set; }

    [Required]
    public virtual Category Category { get; set; }
}

והנה, הטבלה נראית יותר לטעמינו!

products_nonnullable

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

[ComplexType]
public class AddressInfo
{
    public string Address { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

ניתן לראות, שסימנתי את המחלקה באטריבוט ComplexTypeAttribute.

כעת נשנה את המחלקה Order להכיל את מאפיין הכתובת:

public class Order
{
    public int Id { get; set; }

    [Required]
    public int Quantity { get; set; }
        
    [Required]
    public string BuyerName { get; set; }

    public AddressInfo Address { get; set; }

    [Required]
    public virtual Product Product { get; set; }
}

במסד הנתונים זה ייראה כך:

orders_with_address

בגלל שסימנו את המחלקה AddressInfo כComplexType, במקום ליצור טבלה מיוחדת עבור הטיפוס AddressInfo, כמו שEF עשה עם המחלקות האחרות בDAL שלנו, EF "שיטח" את מבנה הנתונים והכניס את השדות של AddressInfo ישר אל תוך הטבלה Orders ומיפה אותם אל הטיפוס המקורי שלהם – AddressInfo.

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

נציין בקצרה Data Annotations שימושיים נוספים:

DbContext

DbContext מהווה את הגרעין של גישת Code first. פעולת "גילוי" המודל של EF מתחילה ממחלקה זו. בדיוק בשביל זה, כפי שאתם זוכרים, יצרנו מחלקה StoreContext שיורשת מDbContext. במחלקה, הכרזנו 3 מאפיינים מסוג DbSet<T> עבור שלשת הישויות שלנו (Category, Product, Order). אציין, שלא חייבים ליצור מאפיינים עבור כל הישויות במודל, למשל את הישות AddressInfo מנוע הEF "מגלה" אוטומטית כשהוא מנתח את המחלקה Order.

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

לדוגמה: אם נרצה להגדיר שהמאפיין Title בProduct חייב להיות לא Nullable באמצעות הFluent API (במקום מה שהגדרנו קודם בData Anotations) אנחנו נכתוב כך:

public class StoreContext : DbContext
{
    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Product>()
            .Property(x => x.Title)
            .IsRequired();

    }
}

והתוצאה תהיה זהה.

IDatabaseInitializer

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

  • CreateDatabaseIfNotExists<TContext> – פעולת האתחול תתרחש רק אם מסד הנתונים לא היה קיים ונוצר זה עתה.
  • DropCreateDatabaseAlways<TContext> – מסד הנתונים ייווצר מחדש והמידע יאותחל בפעם הראשונה שהDbContext יהיה בשימוש בתוך AppDomain (כל הנתונים הקיימים יימחקו).
  • DropCreateDatabaseIfModelChanges<TContext> - מסד הנתונים ייווצר מחדש והמידע יאותחל אם המודל שונה מאז שמסד הנתונים נוצר (כל הנתונים הקיימים יימחקו).
  • IDatabaseInitializer<TContext> – ממשק המאפשר מימוש של אסטרטגיית אתחול מתואמת אישית.

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

public class StoreInitializer : DropCreateDatabaseIfModelChanges<StoreContext>
{
    protected override void Seed(StoreContext context)
    {
        var category = new Category() { Title = "Laptops" };

        var product = new Product()
        {
            Title = "HP Pavilion dv6000",
            Description = "A bit old but works just fine",
            Price = 200,
            Category = category
        };

        var order = new Order()
        {
            Product = product,
            BuyerName = "Israel Israeli",
            Quantity = 1,
            Address = new AddressInfo() { Address = "Zabutinsky 1", City = "Tel Aviv", Country = "Israel" }
        };

        context.Orders.Add(order);
        context.SaveChanges();
    }
}

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

public class StoreContext : DbContext
{
    static StoreContext()
    {
        Database.SetInitializer<StoreContext>(new StoreInitializer());
    }

    public DbSet<Category> Categories { get; set; }
    public DbSet<Product> Products { get; set; }
    public DbSet<Order> Orders { get; set; }
}

חשוב לציין: EF בגרסה 4.1 לא תומך ב"שדרוג" מסדי נתונים. כלומר, אם שיניתם את במנה המודל בצורה שאינו תואם עוד את מבנה הטבלאות במסד הנתונים, EF לא יידע לעדכן את מבנה הטבלאות תוך שמירה על הנתונים שבהן. אם כי תוכלו לכתוב לוגיקה שלכם לביצוע שדרוג זה ע"י מימוש IDatabaseInitializer<TContext>.

מה ההבדל בין EF ל-LINQ to SQL

רבים תוהים מה ההבדל בין LINQ to SQL (להבא linq2sql) לבין EF, במיוחד לאור כך שהם שוחררו בהפרש קטן זה מזה.

עיקר ההבדל נעוץ בתפיסת העולם של EF, שכמו שאמרנו עוסקת ב"ישויות" ולא ב"מבנה טבלאי". כך שבlinq2sql אין את מנגנון המיפוי העשיר שיש לEF. בנוסף, linq2sql עוצבה לתת מענה לעבודה מול בסיס נתונים ממשפחת SQL Server בלבד.

לאומת זאת, EF מתאימה למודלי ישויות גדולים ומורכבים, תומכת בעבודה עם אובייקטים פשוטים (POCO) ובעבודה מול בסיסי נתונים מסוגים שונים (Oracle, MySql, Sybase ועוד) ע"י מודל Providerים גמיש וניתן להרחבה.

כמובן אם לקחת בחשבון את השיפורים שנכנסו בגרסה 4.1 כל השוואה לlinq2sql מחווירה.

קטגוריות: .NET 4.0

תגובות (3) -

רן
04/08/2011 16:53:34 #

אחלה!!!!
תודה

אסף
07/11/2011 09:49:13 #

מצויין,
אתה כותב מעולה וברור ממש הבהיר לי מספר דברים,
הייתי שמח מאוד לקרא קצת המשך עם מימוש של:
blogs.msdn.com/.../...ons-alpha-2-walkthrough.aspx

תודה.

דניאל כץ
07/11/2011 09:55:07 #

תודה. לכתוב על EF 4.2 זה בהחלט משהו שאני מתכנן לעשות בקרוב.

הוסף תגובה




biuquote
  • תגובה
  • תצוגה מקדימה
Loading