מדריך Windows Phone

מדריך Windows Phone – עבודה עם DB מקומי

‏ • Sela

קרדיט: פרק זה נכתב ע"י אלכס גולש, יועץ בכיר בקבוצת סלע ו Silverlight MVP.

 

הקדמה

עם שחרור גרסת "מנגו" של מערכת ההפעלה Windows Phone אנו, המפתחים, קיבלנו תוספת חזקה ומשמעותית ל-API הקיים: מנוע מסד נתונים מקומי על המכשיר. מנוע המסד שעל המכשיר מבוסס על מנוע SQL CE. אפליקציות מנגו משתמשות ב- LINQ to SQL עבור כל הפעולות הקשורות למסדי נתונים. טכנולוגיית LINQ to SQL מספקת גישה מונחית עצמים לעבודה עם נתונים ומורכבת ממודל אובייקטים וסביבת ריצה (Object Model & Runtime). קבצי מסד הנתונים מאוכסנים במכשיר ב- Isolated Storage וזמינים לאפליקציה בלבד (מכאן שאפליקציות אינן יכולות לחלוק מסד נתונים).

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

  • אפליקציות "רשימת קניות"
    • מסדי נתונים בעלי סכמה מורכבת, מכילים במקרים רבים מספר מועט של טבלאות (5-7), מאות רשומות וכמות גדולה מאוד של קשרים, הגבלים ומפתחות חיצוניים (Foreign Keys).
  • אפליקציות "מילון"
    • נתוני Reference – במקרים רבים כמות עצומה של נתוני Reference עם מעט מאוד טבלאות (2-3) והגבלים. הטבלאות (אחת או שתיים מהן) מחזיקות כמות עצומה של נתונים (500K-1M).
  • Cache מקומי עבור אפליקציות
    • Cache מקומי עבור נתונים שמגיעים מן הענן לעיתים בשילוב נתוני אפליקציה ספציפיים. לרוב מדובר במספר מועט של טבלאות נוספות המכילות נתונים פשוטים יחסית – בד"כ מדובר על מאות רשומות.

 

טכנולוגיית LINQ to SQL מספקת יכולות מיפוי יחסים בין האובייקטים (ORM) המאפשרות לאפליקציות מנוהלות להשתמש ב- LINQ בכדי לתקשר עם מסד נתונים רלציוני. LINQ to SQL ממפה את מודל האובייקטים המובע באמצעות קוד מנוהל של סביבת .NET למסד נתונים רלציוני. בזמן ריצת האפליקציה LINQ to SQL מתרגמת שאילתות LINQ ל"שפת" מסד הנתונים ושולחת את הביטויים למסד הנתונים לביצוע. כאשר מסד הנתונים מחזיר תשובה לשאילתא LINQ to SQL מתרגמת את התשובה חזרה לאובייקטים.

LINQ to SQL עובדת עם DataContext המגדיר את מודל האובייקטים של הנתונים. בד"כ DataContext מגדיר את הנתונים תוך שימוש ב- POCO (Plain Old CLR Object) ומוסכמות של Attributes. ע"מ ליצור מחלקות DataContext משלנו עלינו לרשת ממחלקת הבסיס DataContext. DataContext משתמש במחלקה מנוהלת המגדירה את מבנה מסד הנתונים ע"י הגדרת המבנה הטבלאי ומיפוי בין מודל האובייקטים וסכמת מסד הנתונים. המיפוי נוצר ע"י הוספת attributes של מיפוי לאובייקט. Attributes אלו מגדירות תכונות ספציפיות של מסד הנתונים כגון טבלאות, עמודות, מפתחות עיקריים, אינדקסים ועוד.

 

יצירת מסד נתונים

גישת "קידוד תחילה" (Code First) שעושה שימוש במחלקה מנוהלת בכדי להגדיר את הסכמה וליצור את מסד הנתונים היא הגישה המועדפת עבור אפליקציות Windows Phone. כמה נקודות הנוגעות למסד נתונים מקומי הנוצר ע"י אפליקציה:

  • מסד נתונים מקומי מתופעל רק באמצעות האפליקציה שייצרה אותו.
  • מסד הנתונים אינו בר שיתוף לאפליקציות אחרות והגישה אליו מתבצעת באמצעות האפליקציה שייצרה אותו בלבד.
  • מסד נתונים מקומי תומך במכניזם השאילתות LINQ to SQL בלבד. לא קיימת תמיכה ב T-SQL.

 

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

מדריך Windows Phone – עבודה עם DB מקומי

 

המחלקה המגדירה את טבלת Category היא פשוטה מאוד:

[Table]
public class Category
{
  [Column(IsPrimaryKey = true)]
  public int ID { get; set; }

  [Column]
  public string Name { get; set; }
}

 

וטבלת Product:

[Table]
[Index(Name = "NameIndex", Columns = "ShortName", IsUnique = false)]
public class Product
{

  [Column(IsPrimaryKey = true)]
  public int ID { get; set; }

  [Column()]
  public int CategoryID { get; set; }

  [Column]
  public string ShortName { get; set; }

  [Column]
  public string Description { get; set; }

  [Column(DbType = "DateTime", CanBeNull = true)]
  public DateTime? ExpirationDate { get; set; }

  [Column]
  public bool IsActive { get; set; }
}

 

הדבר היחיד שחסר במחלקות האלה הוא היחס ביניהן. ב- LINQ to SQL אנו צריכים להוסיף תכונה (Property) שעליה מודבקת Association Attribute. אותה Attribute צריכה להתווסף לשני חלקי הקשר (Relation). מחלקת Category צריכה להחזיק סט של ישויות מסוג Product, וכך אנו מוסיפים תכונה מסוג EntitySet<T>:

private EntitySet<Product> _Product;

[Association(Name = "FK_ProductCategory",
              Storage = "_Product",
              ThisKey = "ID",
              OtherKey = "CategoryID")]
public EntitySet<Product> Product
{
    get { return _Product; }
    set { _Product.Assign(value); }
}

 

על-מנת לאתחל את התכונה הזאת כיאות הבה נשנה את ה- Constructor של המחלקה ונוסיף שתי שיטות (Methods) שיעזרו לנו:

public Category()
{
    this._Product = new EntitySet<Product>(
                      new Action<Product>(attach_Product),
                      new Action<Product>(detach_Product));
}
 
private void attach_Product(Product entity)
{
    entity.Category = this;
}
 
private void detach_Product(Product entity)
{
    entity.Category = null;
}

 

הקוד הזה מטפל באיתחול נכון של ישות Product עבור ה- Category הנוכחי. עכשיו ניתן לשנות את מחלקת Product. יש לה קשר (Relation) לישות מסוג Category ולכן עלינו להוסיף תכונה מסוג EntityRef<T>:

private EntityRef<Category> _Category;
[Association(Name = "FK_ProductCategory",
              Storage = "_Category",
              ThisKey = "CategoryID",
              OtherKey = "ID",
              IsForeignKey = true)]
public Category Category
{
    get { return _Category.Entity; }
    set
    {
        Category previousValue = this._Category.Entity;
        if ((previousValue != value) ||
            (this._Category.HasLoadedOrAssignedValue == false))
        {
            if ((previousValue != null))
            {
                this._Category.Entity = null;
                previousValue.Product.Remove(this);
            }
            this._Category.Entity = value;
            if ((value != null))
            {
                value.Product.Add(this);
                CategoryID = value.ID;
            }
            else
            {
              CategoryID = default(int);
            }
        }
    }
}

 

לבסוף עלינו לאתחל את המשתנה EntityRef – נשנה את ה- Constructor:

public Product()
{
    this._Category = default(EntityRef<Category>);
}

 

כעת ניצור את מחלקת DataContext שלנו:

public class SimpleDC : DataContext
{
    public SimpleDC(string connectionString)
        : base(connectionString)
    {
    }

    public Table<Product> Products;
    public Table<Category> Categories;
}

 

מעתה ואילך ניתן להשתמש ב- SimpleDC ליצירת מסד נתונים על המכשיר ולבצע מולו פעולות.

יצירת סכמת מסד נתונים עבור מסד נתונים פשוט היא יחסית קלה ולא מורכבת, שימוש במסד נתונים קצת גדול (ומורכב) יותר הופך לפרובלמטי יותר בהקשר של יצירת ה- DataContext. ע"מ להשתמש במסד נתונים מורכב יותר כמו "AdventureWorks" הידוע – עבור דוגמת SQL CE עלינו לייצר סכמה מאוד מורכבת:

מדריך Windows Phone – עבודה עם DB מקומי

 

  • הסכמה ממוזערת בכדי להתאים לדף זה.

 

ה- DataContext עבור מסד נתונים זה יסתכם בכ – 2500 שורות קוד… מה בנוגע ליצירה ידנית של DataContext עבור מסד נתונים זה? ברור שיצירת DataContext עבור מסד נתונים זה אינה תהליך פשוט. האם לא ניתן להשתמש במסדי נתונים מהעולם האמיתי עבור אפליקציות Windows Phone?

ובכן… ברור שזה אפשרי אך אנו נזדקק לעזרים חיצוניים בכדי לייצר את מחלקת DataContext המורכבת. הבה נייצר DataContext. ה-SDK של Windows מכיל תוכנית עזר (Utility) ליצירת DataContext עבור LINQ to SQL למסדי נתונים המבוססים על SQL Server ו- Compact SQL Server, עזר זה נקרא sqlMetal. נשתמש בו ע"מ ליצור את קובץ ה-DataContext.

הערה: sqlMetal אינו תומך ישירות במסדי נתונים מסוג SQL CE של Windows Phone ולכן קבצי DataContext שנוצרו באמצעות sqlMetal לא יקומפלו באופן מיידי במפרוייקט מסוג Windows Phone. השתמשו בהם כנקודת פתיחה בכדי לחסוך עבודת קידוד מרובה.

הבה ניצור את קובץ ExternalDC.cs (נשתמש בו לאחר היצירה בפרויקט הדוגמה שלנו)

%ProgramFiles(x86)%\Microsoft SDKs\Windows\v7.0A\Bin>SqlMetal.exe 
          /code:"D:\My Documents\Blog\Samples\DatabaseSample\Data\ExternalDC.cs" 
               
"D:\My Documents\Blog\Samples\DatabaseSample\Data\ExternalDB.sdf"

 

לאחר הוספת הקובץ שנוצר לפרויקט עלינו להסיר שני Constructors שאינם נתמכים ע"י מנגו:

public ExternalDB(System.Data.IDbConnection connection) :
    base(connection, mappingSource)
{
    OnCreated();
}

public ExternalDB(System.Data.IDbConnection connection,
System.Data.Linq.Mapping.MappingSource mappingSource) :
    base(connection, mappingSource)
{
    OnCreated();
}

 

לאחר הסרתם הקובץ מתקמפל וה- DataContext המורכב שלנו מוכן.

 

שימוש במסד הנתונים

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

theDC = new SimpleDC(DBName);

if (!theDC.DatabaseExists())
  theDC.CreateDatabase();

 

DBName הוא מיקומו של קובץ מסד הנתונים. מיקום הקובץ חייב להיות בהתאם לפורמט הבא:

isostore:/DIRECTORY/FILE.sdf

 

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

"Data Source='isostore:/DIRCTORY/FILE.sdf';Password='MySecureP@ssw0rd'"

 

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

לאחר יצירת מסד הנתונים המקומי ניתן להשתמש בשאילתות LINQ to SQL באמצעות המופע של DataContext בכדי לגשת למסד הנתונים ולנתונים השמורים בו.

נעשה שימוש ב- LINQ to SQL בכדי לתחקר את מסד הנתונים. שאילתות LINQ עובדות עם מופע DataContext בכדי לתשאל נתונים. דוגמת הקוד הבאה בוחרת עובדים לא פעילים מתוך מסד הנתונים ומסדרת אותם לפי LastName:

var res = from p in mainDB.Products
      where p.IsActive == false
      orderby p.ShortName
      select p;

 

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

Product product = new Product();
product.ID = random.Next();
product.ShortName = "Nike Shox Navina";
product.Description = "The Nike Shox Navina SI running shoe is for the " +
                      "longtime female runner who is looking for a better " +
                      "ride and fit. Synthetic upper with synthetic overlays " +
                      "provides a flexible upper to enhance fit and feel";
product.CategoryID = 101;
product.ExpirationDate = new DateTime(2012, 12, 14);
product.IsActive = false;
 
mainDB.Products.InsertOnSubmit(product);
mainDB.SubmitChanges();

 

אותה גישה עובדת עבור אובייקטים מרובים אך נצטרך לייצר IEnumerable<T> קודם ואז לקרוא לפונקציית InsertAllOnSubmit:

List<Product> products = new List<Product>();
product = new Product();
product.ID = random.Next();
product.ShortName = "Jockey Lace Bikini";
product.Description = "All over lace with satin bow detail; " +
                      "Scallop-edge waistband and leg openings";
product.CategoryID = 103;
product.IsActive = true;
products.Add(product);
 
product = new Product();
product.ID = random.Next();
product.ShortName = "Jockey Flora Tong";
product.Description = "Slip into captivating beauty; Ultra-light " +
                      "and smooth microfiber & a touch of stretch " +
                      "with feminine colors and prints";
product.CategoryID = 103;
product.ExpirationDate = new DateTime(2012, 12, 14);
product.IsActive = false;
products.Add(product);
 
product = new Product();
product.ID = random.Next();
product.ShortName = "Kangol Wool Mowbray";
product.Description = "The Kangol Wool Mowbray nicely presents the " +
                      "classic pork pie shape, rendered masterfully in " +
                      "sturdy yet lightweight Kangol quality wool";
product.CategoryID = 104;
product.ExpirationDate = new DateTime(2013, 12, 14);
product.IsActive = true;
products.Add(product);
 
mainDB.Products.InsertAllOnSubmit(products);
mainDB.SubmitChanges();

 

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

עדכון:

var prods = from p in mainDB.Products
            where p.IsActive
            select p;
 
foreach (var p in prods)
{
    p.IsActive = false;
}
 
mainDB.SubmitChanges();

 

מחיקה:

var prods = from p in mainDB.Products
            where p.ExpirationDate == null
            select p;
 
mainDB.Products.DeleteAllOnSubmit(prods.ToList());
mainDB.SubmitChanges();

 

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

הבה נחזור למסד הנתונים AdventureWorks, ברור שבמקרה של מסד נתונים מסדר גודל כזה המכיל כמות נכבדת של נתוני "System" לא ניתן יהיה ליצור אותו על המכשיר. במקרה כזה נצטרך לארוז את קובץ מסד הנתונים כמשאב Content של האפליקציה, להעתיק אותו לIsolated Storage – של האפליקציה ולעשות שימוש ב- DataContext בכדי לגשת לנתונים:

מדריך Windows Phone – עבודה עם DB מקומי

 

דוגמת הקוד הבאה שולפת את Stream המשאבים של האפליקציה שולחת אותו לשיטת עזר שמחזקירה את התוכן שב- Stream כמערך של byte ולבסוף שומרת את ה- bytes האלו ב- Isolated Storage של האפליקציה.

public static void CreateExternalDatabase(string DBName)
{
    Stream str = Application.GetResourceStream(new Uri("Data/" + DBName, UriKind.Relative)).Stream;

    using (IsolatedStorageFile isoStore = IsolatedStorageFile.GetUserStoreForApplication())
    {
        IsolatedStorageFileStream outFile = isoStore.CreateFile(DBName);

        outFile.Write(ReadToEnd(str), 0, (int)str.Length);
        str.Close();
        outFile.Close();
    }
}

public static byte[] ReadToEnd(Stream stream)
{
    long originalPosition = stream.Position;
    stream.Position = 0;
    try
    {
        byte[] readBuffer = new byte[4096];
        int totalBytesRead = 0;
        int bytesRead;

        while ((bytesRead = stream.Read(readBuffer, totalBytesRead, readBuffer.Length - totalBytesRead)) > 0)
        {
            totalBytesRead += bytesRead;
            if (totalBytesRead == readBuffer.Length)
            {
                int nextByte = stream.ReadByte();
                if (nextByte != -1)
                {
                    byte[] temp = new byte[readBuffer.Length * 2];
                    Buffer.BlockCopy(readBuffer, 0, temp, 0, readBuffer.Length);
                    Buffer.SetByte(temp, totalBytesRead, (byte)nextByte);
                    readBuffer = temp; totalBytesRead++;
                }
            }
        }

        byte[] buffer = readBuffer;

        if (readBuffer.Length != totalBytesRead)
        {
            buffer = new byte[totalBytesRead];
            Buffer.BlockCopy(readBuffer, 0, buffer, 0, totalBytesRead);
        }

        return buffer;

    }
    finally
    {
        stream.Position = originalPosition;
    }
}

 

נגישות למסד נתונים שנוצר בדרך זו דומה לנגישות למסד נתונים המתקבלת מ- DataContext על המכשיר:

externalDB = new ExternalDB(ExternalDBFileName);
var products = (from p in externalDB.Products
                select p).Take(10);

 

Schema Updater

החלק האחרון של מאמר זה יתמקד בעדכון סכמת מסד הנתונים. מהלך חייהן הנורמלי של אפליקציות Windows Phone כולל עדכון של האפליקציה מפעם לפעם. עדכון זה יכול לכלול שינויים לסכמת מסד הנתונים. ברוב המקרים נרצה לשמור על נתוני המשתמש השמורים במסד הנתונים ולכן נצטרך לעדכן את מסד הנתונים תוך שימוש במחלקה מיוחדת- DatabaseSchemaUpdater מתוך Microsoft.Phone.Data.Linq. מחלקה זו יכולה לערוך שינויים במסד הנתונים כגון הוספת טבלאות, עמודות, אינדקסים וקשרים. עבור שינויים מרחיקי לכת יותר נצטרך ליצור מסד נתונים חדש ולהעתיק את הנתונים לסכמה החדשה. המחלקה מכילה תכונה- DatabaseSchemaVersion שניתן לעשות בה שימוש בכדי להבחין בין גרסאות שונות של מסד הנתונים באופן תכנותי:

DatabaseSchemaUpdater schemaUpdater = theDC.CreateDatabaseSchemaUpdater();

if (schemaUpdater.DatabaseSchemaVersion < 2)
{
    //Incompatible change detected, create new database,
    //copy data from old database to new and delete old database
    MigrateDataToNewDatabase(theDC);
}
else if (schemaUpdater.DatabaseSchemaVersion == 2)
{
    //Compatible change. Add new column to existing table
    //NOTE: IsRetired is a new property defined in Employees
    schemaUpdater.AddColumn<Product>("OnSale");
}

 

DatabaseSchemaUpdater מאפשרת הוספת טבלה, עמודה, אינדקס וקשר. על התוספות החדשות להיות מוגדרות במחלקת ה- DataContext המתאימה קודם. בדוגמת הקוד הקודמת מחלקת Product המעודכנת הכילה עמודה נוספת: OnSale:

[Column]
Public bool OnSale { get; set; }

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

תגיות: , , , ,

arikp

אריק פוזננסקי הוא יועץ בכיר ומרצה בסלע. הוא השלים שני תארי B.Sc. במתמטיקה ומדעי המחשב בהצטיינות יתרה בטכניון. לאריק ידע נרחב בטכנולוגיות מיקרוסופט, כולל .NET עם C#, WPF, Silverlight, WinForms, Interop, COM/ATL, C++ Win32 ו reverse engineering.

תגובות בפייסבוק