LINQ to SQL – Entity Class: Mapping Database, Table và Relationship theo yinyangit.wordpress.com

Trong bài trước khi giới thiệu về “Object-Relational Mapping, Entity Class, Association và DataContext”, tôi đã làm một ví dụ nhỏ tạo entity class và truy vấn dữ liệu trên database Northwind. Hôm nay tôi sẽ làm một ví dụ tương tự nhưng hoàn chỉnh hơn để 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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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<TEntity>() 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à EntityRef<TEntity> vớ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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[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<int>, đ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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
[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 EntityRef<TEntity> trướ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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
[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.Action<in T> là attachProducts và detachProducts để truyền vào làm tham số của constructor EntitySet<Product>(). Mỗi lần collection EntitySet<Product>, _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.


Nhận xét

Bài đăng phổ biến từ blog này

Khôi phục phân vùng ổ cứng do ghost nhầm theo Hieuit.net

Cách sử dụng 2 phương thức [httppost] và [httpget] trong MVC theo https://dzungdt.wordpress.com

MVC định dạng tiền tệ vnđ - Format currency vnd in MVC