LINQ to SQL – Entity Class: Mapping Database, Table và Relationship

Giới thiệu các bạn ví dụ dưới đây để bạn hiểu rõ cách tạo và sửa đổi các entity class khi cần thiết, bao gồm ví dụ về One-To-Many Relationship.

Giới thiệu

Trong ví dụ này tôi sẽ tạo các Entity class cho database Northwind, table Categories và Products. Mối quan hệ giữa hai bảng này được minh họa như hình sau, cùng các cột mà tôi sẽ sử dụng:

Bạn cũng đừng quên thêm tham chiếu đến thư viện System.Data.Linq và hai khai báo namespace sau:

using System.Data.Linq;

using System.Data.Linq.Mapping;

Lớp NorthwindDataContext

Khi tạo lớp này bạn có thể không cần đến từ DataContext trong phần tên lớp, tuy nhiên tôi muốn giữ lại để giúp phân biệt dễ dàng hơn giữa entity class cho database và cho các table.

Ta sử dụng attibute [DatabaseAttribute] và thuộc tính Name để tạo một entity class đại diện cho database Northwind, và tất nhiên lớp này phải kế thừa từ DataContext:

[DatabaseAttribute(Name = "northwind")]
public partial class NorthwindDataContext : DataContext
{
    public NorthwindDataContext(string connection)
        : base(connection)
    {
    }

    public Table<Category> Categories
    {
        get { return this.GetTable<Category>(); }
    }

    public Table<Products> Products
    {
        get { return this.GetTable<Products>(); }
    }
}

Constructor của entity class nhận một vào chuỗi kết nối, connection, ta gọi trực tiếp constructor của lớp cha (DataContext) với tham số là connection này để tạo kết nối.

Hai phương thức còn lại là Categories() và Products() chỉ đơn giản là cho phép lấy trực tiếp các table có tên tương ứng với phương thức, bằng cách gọi phương thức GetTable() của DataContext. Giả sử bạn có 10 table trong database và cần sử dụng chúng, bạn sẽ tạo 10 tên phương thức để trả về mỗi table với tên tương ứng.
Lớp Product

Lớp này đại diện cho một dòng dữ liệu của table Products, cũng có thể coi là lớp đại diện cho table Products trong database theo nguyên tắc ánh xạ ORM (Object-Relational Mapping).

Trong ví dụ này tôi chỉ dùng ba cột là ProductID, ProductName và CategoryID, mỗi cột ứng với một private field. Tuy nhiên như vậy chưa đủ, vì Product có mối quan hệ cha-con với Category nên ta cần một tham chiếu đến đối tượng Category để có thể truy xuất trực tiếp đến nó. Đối tượng tham chiếu này sẽ có kiểu là EntityRefvới tên _Category.

Trong constructor của Product ta sẽ khởi tạo giá trị mặc định cho đối tượng _Category này với từ khóa default:

[Table(Name = "Products")]
public partial class Product
{
    private int _ProductID;

    private string _ProductName;

    private System.Nullable<int> _CategoryID;

    private EntityRef<Category> _Category;

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

    // ...
}

Trong đoạn mã trên bạn có thể thấy field _CategoryID được khai báo với kiểu System.Nullable, điều này cho phép _CategoryID có thể được gán giá trị null (một giá trị mà int không thể có được). Điều này là do trong cột CategoryID trong table Products được thiết lập Allow Nulls là true. Nếu như bạn không cho phép null, ta chỉ cần khai báo với kiểu int như _ProductID.

Tiếp đến là tạo các property tương ứng cho các cột tương ứng là ProductID, ProductName và CategoryID:

[Table(Name = "Products")]
public partial class Product
{
    // ...

    [Column(Storage = "_ProductID", AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)]
    public int ProductID
    {
        get { return this._ProductID; }
        set
        {
            if ((this._ProductID != value))
                this._ProductID = value;
        }
    }

    [Column(Storage = "_ProductName", DbType = "NVarChar(40) NOT NULL", CanBeNull = false)]
    public string ProductName
    {
        get { return this._ProductName; }
        set
        {
            if ((this._ProductName != value))
                this._ProductName = value;
        }
    }

    [Column(Storage = "_CategoryID", DbType = "Int")]
    public System.Nullable<int> CategoryID
    {
        get { return this._CategoryID; }
        set
        {
            if ((this._CategoryID != value))
            {
                if (this._Category.HasLoadedOrAssignedValue)
                {
                    throw new ForeignKeyReferenceAlreadyHasValueException();
                }
                this._CategoryID = value;
            }
        }
    }

    // ...
}

Các property này không có gì đặc biệt ngoại trừ một điểm khi gán giá trị cho CategoryID. Giá trị của _CategoryID phải khớp với đối tượng _Category. Chính vì vậy ta cần phải kiểm tra xem _Category đã có giá trị chưa bằng property HasLoadedOrAssignedValue của EntityReftrước khi thay đổi giá trị của _CategoryID.

Mối quan hệ của Product và Category được thể hiện bởi một property với [AssociationAttribute]. Việc thay đổi giá trị của property này cần được kiểm tra kĩ càng và phải đảm bảo _CategoryID cũng phải được thay đổi theo. Ngoài ra, bởi vì bên entity class Category (sẽ trình bày trong phần kế tiếp) cũng sẽ có một collection chứa các đối tượng Product. Ta phải loại bỏ đối tượng Product ra khỏi tập hợp đó nếu như “cha” (Category) của nó được thay đổi:

[Association(Name = "FK_Products_Categories", ThisKey = "CategoryID", IsForeignKey = true)]
public Category Category
{
    get { return this._Category.Entity; }
    set
    {
        Category previousValue = this._Category.Entity;
        if (((previousValue != value)
                    || (this._Category.HasLoadedOrAssignedValue == false)))
        {
            if ((previousValue != null))
            {
                this._Category.Entity = null;
                previousValue.Products.Remove(this);
            }
            this._Category.Entity = value;
            if ((value != null))
            {
                value.Products.Add(this);
                this._CategoryID = value.CategoryID;
            }
            else
            {
                this._CategoryID = default(Nullable<int>);
            }
        }
    }
}

Các thuộc tính của [ColumnAttribute] dựa vào tên gọi của chúng bạn cũng có thể đoán ra được, tuy nhiên còn một vài thuộc tính bạn cần chú ý:

NameTypeDescription
AutoSync(enum) AutoSyncBao gồm:Default, Always, Never, OnInsert, OnUpdateChỉ ra việc lấy giá trị cho property sau lệnh Insert hoặc Update.Ví dụ như các cột ID sẽ được database tự động gán giá trị, việc dùng attribute này sẽ giúp đồng bộ dữ liệu của cột này trong database với property tương ứng sau khi Insert.
IsDbGeneratedBooleanXác định cột có được database tự động sinh ra không (như primary key).
StorageStringThuộc tính này xác định tên của field lưu trữ giá trị cho property. Nhờ đó, LINQ có thể lấy giá trị trực tiếp từ field thay vì thông qua property.

Lớp Category

Tương tự như lớp Product, trong ví dụ này ta chỉ sử dụng hai cột là CategoryID, CategoryName, mỗi cột tương ứng với một private field và một private field khác chứa tập hợp các Product có liên hệ với Category hiện tại. Entity class của table cha (Categories) sẽ chứa một collection EntitySet<TEntity> các thể hiện entity class của table con (Products):

[Table(Name = "Categories")]
public partial class Category
{
    private int _CategoryID;

    private string _CategoryName;

    private EntitySet<Product> _Products;

    public Category()
    {
        Action<Product> attachProducts = new Action<Product>((p) => p.Category = this);
        Action<Product> detachProducts = new Action<Product>((p) => p.Category = null);
        this._Products = new EntitySet<Product>(new Action<Product>(Attach_Products), detachProducts);
    }

    [Column(Storage = "_CategoryID", AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)]
    public int CategoryID
    {
        get { return this._CategoryID; }
        set
        {
            if ((this._CategoryID != value))
                this._CategoryID = value;
        }
    }

    [Column(Storage = "_CategoryName", DbType = "NVarChar(15) NOT NULL", CanBeNull = false)]
    public string CategoryName
    {
        get { return this._CategoryName; }
        set
        {
            if ((this._CategoryName != value))
                this._CategoryName = value;
        }
    }

    [Association(Name = "FK_Products_Categories", Storage = "_Products", OtherKey = "CategoryID", DeleteRule = "NO ACTION")]
    public EntitySet<Product> Products
    {
        get { return this._Products; }
        set { this._Products.Assign(value); }
    }
}

Constructor của lớp này tạo ra hai delegate System.Actionlà attachProducts và detachProducts để truyền vào làm tham số của constructor EntitySet(). Mỗi lần collection EntitySet, _Products, được gán hay chèn giá trị, delegate attachProduct sẽ được kích hoạt để gán tham chiếu đến đối tượng Category hiện tại. Tương tự như vậy, khi bạn xóa các đối tượng Product ra khỏi collection này, delegate detachProduct sẽ được kích hoạt để gán tham chiếu Category của đối tượng đó thành null.

Bạn có thể thấy phương thức Assign() được sử dụng trong property Products của lớp này. Ngoài lý do để delegate được kích hoạt ra, phương thức này còn tạo ra một bản sao của giá trị được gán.
Mã nguồn hoàn chỉnh

Lớp Northwnd.cs:

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;

namespace Northwnd
{
    [DatabaseAttribute(Name = "northwind")]
    public partial class NorthwindDataContext : DataContext
    {
        public NorthwindDataContext(string connection)
            : base(connection)
        {
        }

        public Table<Category> Categories
        {
            get { return this.GetTable<Category>(); }
        }

        public Table<Product> Products
        {
            get { return this.GetTable<Product>(); }
        }
    }

    [Table(Name = "Categories")]
    public partial class Category
    {
        private int _CategoryID;

        private string _CategoryName;

        private EntitySet<Product> _Products;

        public Category()
        {
            Action<Product> attachProduct = new Action<Product>((p) => p.Category = this);
            Action<Product> detachProduct = new Action<Product>((p) => p.Category = null);
            this._Products = new EntitySet<Product>(attachProduct, detachProduct);
        }

        [Column(Storage = "_CategoryID", AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)]
        public int CategoryID
        {
            get { return this._CategoryID; }
            set
            {
                if ((this._CategoryID != value))
                    this._CategoryID = value;
            }
        }

        [Column(Storage = "_CategoryName", DbType = "NVarChar(15) NOT NULL", CanBeNull = false)]
        public string CategoryName
        {
            get { return this._CategoryName; }
            set
            {
                if ((this._CategoryName != value))
                    this._CategoryName = value;
            }
        }

        [Association(Name = "FK_Products_Categories", Storage = "_Products", OtherKey = "CategoryID", DeleteRule = "NO ACTION")]
        public EntitySet<Product> Products
        {
            get { return this._Products; }
            set { this._Products.Assign(value); }
        }
    }

    [Table(Name = "Products")]
    public partial class Product
    {
        private int _ProductID;

        private string _ProductName;

        private System.Nullable<int> _CategoryID;

        private EntityRef<Category> _Category;

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

        [Column(Storage = "_ProductID", AutoSync = AutoSync.OnInsert, DbType = "Int NOT NULL IDENTITY", IsPrimaryKey = true, IsDbGenerated = true)]
        public int ProductID
        {
            get
            {

                return this._ProductID;
            }
            set
            {
                if ((this._ProductID != value))
                    this._ProductID = value;
            }
        }

        [Column(Storage = "_ProductName", DbType = "NVarChar(40) NOT NULL", CanBeNull = false)]
        public string ProductName
        {
            get { return this._ProductName; }
            set
            {
                if ((this._ProductName != value))
                    this._ProductName = value;
            }
        }

        [Column(Storage = "_CategoryID", DbType = "Int")]
        public System.Nullable<int> CategoryID
        {
            get { return this._CategoryID; }
            set
            {
                if ((this._CategoryID != value))
                {
                    if (this._Category.HasLoadedOrAssignedValue)
                    {
                        throw new ForeignKeyReferenceAlreadyHasValueException();
                    }
                    this._CategoryID = value;
                }
            }
        }

        [Association(Name = "FK_Products_Categories", ThisKey = "CategoryID", IsForeignKey = true)]
        public Category Category
        {
            get { return this._Category.Entity; }
            set
            {
                Category previousValue = this._Category.Entity;
                if (((previousValue != value)
                            || (this._Category.HasLoadedOrAssignedValue == false)))
                {
                    if ((previousValue != null))
                    {
                        this._Category.Entity = null;
                        previousValue.Products.Remove(this);
                    }
                    this._Category.Entity = value;
                    if ((value != null))
                    {
                        value.Products.Add(this);
                        this._CategoryID = value.CategoryID;
                    }
                    else
                    {
                        this._CategoryID = default(Nullable<int>);
                    }
                }
            }
        }
    }
}

Kiểm tra với lớp Program.cs, đoạn mã trong Main() sẽ lấy ra dòng dữ liệu trong bảng Categories có CategoryName bắt đầu bằng “M”, sau đó in ra tất cả các dòng trong bảng Products có liên hệ với Category này:

using System;
using System.Linq;
using System.Data.Linq;
using Northwnd;

class Program
{
    static void Main()
    {
        NorthwindDataContext db = new NorthwindDataContext("C:\\SampleDB\\Northwnd.mdf");

        Table<Category> categories = db.Categories();
        var query = from c in categories where c.CategoryName.StartsWith("M") select c;
        Console.WriteLine("Category:");
        foreach (var cat in query)
        {
            Console.WriteLine(cat.CategoryID + " | " + cat.CategoryName);
            Console.WriteLine("Products:\n\t{0,-4} | {1,-25} | {2}\n","ID","Name","CategoryID");
            foreach (var p in cat.Products)
                Console.WriteLine("\t{0,-4} | {1,-25} | {2}",p.ProductID,p.ProductName,p.CategoryID);
        }
        Console.Read();
    }
}

Output:
Category:
6 | Meat/Poultry
Products:
ID                    | Name                                           | CategoryID

9                       | Mishi Kobe Niku                      | 6
17                    | Alice Mutton                             | 6
29                    | Thüringer Rostbratwurst     | 6
53                    | Perth Pasties                              | 6
54                    | Tourtière                                     | 6
55                    | Pâté chinois                                | 6

Kết luận

Trong khuôn khổ của bài viết chỉ trình bày về cách tạo các entity class với các DatabaseAttribute, ColumnAttribute và AssociationAttribute. Mã nguồn của các entity class trên được tạo ra bằng công cụ SQLMetal và được rút gọn để tiện trình bày.