LINQ to SQL – Entity Class: Mapping Database, Table và 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.

Advertisements

11 thoughts on “LINQ to SQL – Entity Class: Mapping Database, Table và Relationship

  1. Cho phép ABT xin bàn về vấn đề này (mình học Entity Framework và nghĩ LINQ to SQL cũng giống nhau).
    Trước khi ORM ra đời, thường thì người ta sử dụng mô hình 3 lớp và chia ra làm BUS-DAO-DTO. Trong đó lớp DTO là Data Transfer Object. DTO rất quan trọng để các lớp trên tham chiếu tới nó.
    Nhưng trong mô hình ORM, các thực thể được tự động phát sinh và cái gọi là Data Model đã thực hiện chức năng của lớp Data Access Layer. Như vậy mình thấy vấn đề là 2 lớp DAO và DTO bị dính lại với nhau.
    Vấn đề đặt ra là nếu gom chung 2 lớp này lại dựa trên đặc điểm của ORM, thì lớp Business và UI bị đụng, lại bỏ đi lớp Business.
    Hiii, bạn tạo dữ liệu các thực thể bằng code, nhưng nếu tạo bằng thao tác trên UI, thì sẽ không kiểm soát được đoạn mã nó phát sinh ra. Và cho dù tạo bằng code, thì trong lớp Product vẫn chứa phương thức, không thuần thuộc tính như DTO.
    Đây là vấn đề từ khi dùng ORM, mình đã phát hiện ra được. Không biết YinYang có suy nghĩ thế nào cho cách giải quyết vấn đề này?

    Phản hồi
  2. Sorry YinYang nhiều lắm. Khi YinYang giới thiệu 1 bài viết nào đó, mình hay nghĩ tới hướng ứng dụng và nêu vấn đề, chứ không tập trung vào bài viết. Nhưng mình nghĩ đó là cách tốt nhất để mọi người thấy được điểm hay, điểm dở và có thể ứng dụng công nghệ khác nhau vào các ứng dụng thực tế.

    Phản hồi
  3. Thật ra mình nên cảm ơn vì những chia sẻ của bạn. Về lĩnh vực database thì có lẽ bạn là người đi trước so với mình. Đúng như bạn nói là Entity Framework được tạo ra dựa trên và để thay thế LINQ to SQL. Chính vì thế ta có thể bắt gặp những khái niệm quen thuộc trong Entity Framework khi đã biết về LINQ to SQL.

    Trước hết xin nói một chút về DTO, khái niệm này có thể được hiểu tương tự với VO (value object), là lớp được tạo ra giống như entity class (đại diện cho một đối tượng trong database hoặc tương tự). Mặc dù các khái niệm này là khác nhau. Trước đây khi làm mô hình 3-tier thì mình thường bỏ qua DTO vì khá mất thời gian. Nhưng bây giờ công việc này có thể được VS làm tự động và thay thế bởi ORM.

    Theo nhận xét của mình thì việc tạo ra mô hình 3 tier/layer, chẳng qua chỉ là cách để dễ quản lý và phát triển ứng dụng do, lúc đó còn dùng những cách truy xuất database cơ bản (như ADO.NET). Việc áp dụng những hướng tiếp cận và mô hình truy xuất dữ liệu mới (như LINQ to SQL) có thể không cần thiết đến việc áp dụng mô hình 3-tier. Khái niệm entity class cho thấy công việc của DAL được gói chung với DTO, hay nói cách khác là DAL được đơn giản hóa đi rất nhiều. Vậy có thể thu gọn DTO và DAL lại thành một.

    Bởi vì chưa có kinh nghiệm về phát triển các ứng dụng 3-tier thực tế nên đây cũng chỉ là chủ quan nhất thời, hi vọng bạn có thể thảo luận và chia sẻ một số kinh nghiệm mà bạn đã từng làm.

    Phản hồi
    • 3 tier là mô hình 3 tầng, như client, server,… Còn 3 layer là mô hình 3 lớp, có thể nằm trên 1 server. Ở đây mình nói là 3 layer.
      Thực ra mình cũng không rõ cách xây dựng mô hình 3 lớp dựa trên ORM. Nhưng theo nguyên tắc, phải áp dụng đúng mô hình 3 layer cơ bản, dù dự án nhỏ hay lớn.
      Nếu kết hợp DTO và DAL lại làm 1, thì quy ra, phải kết hợp BLL với DAL lại với nhau( vì các đối tượng nằm ở tầng DAL. UI tham chiếu đến DAL, mà BLL cũng tham chiếu đến DAL thì không ổn tí nào).
      Nên mình nghĩ cấu trúc ứng dụng sẽ là:

      UI (WebForm, WinForm,…)
      LogicLayer

      Repositories: ORM, DAO
      Services: cung cấp các chức năng xử lý nghiệp vụ.

      Services: Các dịch vụ bên ngoài giao tiếp với Hệ thống.

      Còn về việc thiết kế các lớp Interface, implement class cho các interface cho thư mục Repositories và Services, mình thấy hơi rối. Không biết YinYang có ý kiến thế nào?
      Lưu ý: lớp UI, LogicLayer, Services là các project nằm trong Solution Dự án của bạn.

      Phản hồi
  4. Hic, mình đánh mã Html, mà phần comment không cho phép nên hơi rồi. Mình xin viết lại thế này:
    -UI (WebForm, WinForm,…)
    -LogicLayer
    +Repositories: ORM, DAO
    +Services: cung cấp các chức năng xử lý nghiệp vụ.
    -Services: Các dịch vụ bên ngoài giao tiếp với Hệ thống.

    Phản hồi
  5. 3 Tier hay layer cũng chỉ cách nói về cùng mô hình nhưng phạm vi khác nhau. Vì vậy khi nói chungthì mình dùng tier/layer để phù hợp.

    Ý của mình là có thể coi các entity class là DTO, và DAL xem như đã được tự động hiện thực bằng DataContext. Với ứng dụng nhỏ thì có lẽ đa số có thể dùng cách này, tuy nhiên với ứng dụng lớn muốn rõ ràng hơn thì việc tạo ra các lớp DTO riêng sẽ cần thiết. Ví dụ như DataSet cũng có thể coi là loại DTO nhưng là untyped, vì vậy cho dù không có DTO thì mô hình 3 tier/layer vẫn có thể được hiện thực.

    Ví dụ về việc hiện thực một mô hình 3 tier trên client/server thì chỉ cần chú trọng vào phần DAL (có vẻ như là project Services của bạn), thay vì lấy dữ liệu từ cục bộ thì lấy thông qua mạng.
    Trong ví dụ bạn muốn áp dụng Repository pattern thì có thể làm tương tự như các mô hình ứng dụng thông thường, mình chưa thấy điểm gì khác biệt trong trường hợp này.

    Nếu có thể thì mình sẽ bàn tiếp vấn đề này trong một bài viết mà mình sẽ giới thiệu. Tuy nhiên do phải vừa nghiên cứu đề tài vừa viết bài nên có lẽ sẽ ra chậm. Như cũng nói trong comment trước là kinh nghiệm phát triển ứng dụng của mình rất hạn chế nên mình sẽ rất vui nếu được trao đổi hoặc đọc một vài bài hướng dẫn của bạn.

    Phản hồi
  6. Ok, mình hi vọng sẽ sớm được đọc bài viết của bạn.
    Sau khi đọc comment của bạn, mình thấy DTO thuần túy chỉ là định nghĩa các object và mang giá trị từ tầng UI xuống tầng BLL và DAL. Và khi xây dựng những ứng dụng lớn, việc dùng DTO sẽ càng bất lợi, vì nó không định nghĩa được các mối quan hệ giữ các object với nhau.
    Service mà mình nói ở đây chính là các dịch vụ mà 2 tầng dưới cung cấp cho UI. Mỗi khi UI cần gì, sẽ thông qua Service và không thao tác trực tiếp xuống DAL.
    Ngoài ra Service còn mang ý nghĩa rộng hơn, đó là các dịch vụ trong và ngoài hệ thống mà bạn cung cấp (ví dụ như Sms Service dùng để gửi tin tự động. Đây là dịch vụ không nằm trong hệ thống).
    Nếu sắp tới YinYang có ý định viết bài về mô hình 3 lớp, mình xin nêu thử 1 vài ý kiến: (Tất nhiên mô hình 3 lớp này sẽ áp dụng hoàn toàn công nghệ mới. Mình nghĩ đó là xu thế, dù không dùng nhưng vài năm nữa sẽ dùng 🙂 )
    -Tầng DAL: ORM nào đó (EF) và phải có 1 class chung chung, gọi là đưa dữ liệu lên (Mình hiện gọi là Repository nhưng vẫn chưa hiểu công dụng của nó).
    -Tầng BLL: Sẽ có 2 nhánh lớn: 1 là cung cấp giao diện interface sử dụng các hàm trong hệ thống, 2 là tích hợp các dịch vụ bên ngoài (hệ thống SMS).
    Ngoài ra, YinYang thử suy nghĩ xem, nên có 1 tầng gọi là Library, dùng để phát triển các thư viện mở rộng (tùy theo từng dự án).
    Có 1 phần mình vẫn muốn chia sẽ ý nghĩ với YinYang: Tầng DAL cảm thấy dễ nhưng rất khó thiết kế: Ví dụ với dữ liệu là các Table, thì có thể dùng Entity Framework, nhưng nếu dữ liệu là dạng XML thì phải có 1 class đọc dữ liệu từ XML. Thiết kế sao cho hợp lý mới khó.
    Ngoài ra, theo mình được biết, Silverlight hoặc Flash chỉ nhận dữ liệu từ XML, nên chắc chắn sẽ có tầng Service (như mình nói ở trên) để biến đổi dữ liệu từ DataTable sang XML.
    Cuối cùng ABT xin cảm ơn YinYang rất nhiều.

    Phản hồi
  7. Cảm ơn bạn, việc thảo luận với bạn cũng giúp mình hiểu ra một số vấn đề quan trọng. Mình sẽ tiếp thu ý kiến của bạn và tìm hiểu sâu hơn những vấn đề liên quan. Hi vọng sẽ tiếp tục được thảo luận với bạn trong các chủ đề khác.
    Thân!

    Phản hồi

Trả lời

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s