Thứ ba, ngày 21 tháng 11 năm 2017

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

Ngày đăng: 19/3/2012, 12:49:20PM | Lượt xem: 5,796
Hot!

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.

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:

[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:

[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:

[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:

[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ú ý:

Name Type Description
AutoSync (enum) AutoSyncBao gồm:Default, Always, Never, OnInsert, OnUpdate Chỉ 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.
IsDbGenerated Boolean Xác định cột có được database tự động sinh ra không (như primary key).
Storage String Thuộ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.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.

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 tôi 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 tôi rút gọn để tiện trình bày.

 Chia sẻ qua: 
Hot!
Ý kiến bạn đọc

These items will be permanently deleted and cannot be recovered. Are you sure?

Gallery

image

Maecenas viverra rutrum pulvinar

Maecenas viverra rutrum pulvinar! Aenean vehicula nulla sit amet metus aliquam et malesuada risus aliquet. Vestibulum rhoncus, dolor sit amet venenatis porta, metus purus sagittis nisl, sodales volutpat elit lorem…

Read more

Text Links

Thiết kế logo chuyên nghiệp Insky
DAFABET
W88 w88b.com/dang-ky-tai-khoan-w88
W88
ca do bong da online
Copyright © 2011 - 2012 vietshare.vn by phamkhuong102@gmail.com doanhkisi2315@gmail.com. All rights reserved.