Repository và Unit of Work: Giải pháp tối ưu cho quản lý dữ liệu trong .NET

Trong phát triển phần mềm, đặc biệt là các ứng dụng doanh nghiệp, việc quản lý dữ liệu một cách hiệu quả và bảo trì là vô cùng quan trọng. Repository PatternUnit of Work Pattern là hai mẫu thiết kế phổ biến giúp giải quyết các vấn đề này, mang lại sự linh hoạt, dễ bảo trì và khả năng mở rộng cho ứng dụng. Bài viết này sẽ đi sâu vào khái niệm, lợi ích và cách triển khai hai mẫu thiết kế này trong .NET.

Repository là một mẫu thiết kế đóng vai trò là lớp trung gian giữa tầng dữ liệu (data layer) và tầng nghiệp vụ (business layer). Lớp này chứa các phương thức để tương tác với tầng dữ liệu, cung cấp một abstraction layer để tầng nghiệp vụ không cần quan tâm đến chi tiết triển khai của việc truy cập dữ liệu. Mục đích chính của Repository là cách ly logic nghiệp vụ khỏi việc truy cập dữ liệu, giúp ứng dụng dễ dàng thay đổi cơ sở dữ liệu mà không ảnh hưởng đến phần còn lại của ứng dụng.

Ví dụ, một chương trình POS được triển khai cho nhiều cửa hàng khác nhau. Một số khách hàng muốn sử dụng Microsoft SQL Server, trong khi những người khác lại muốn sử dụng Oracle hoặc các hệ cơ sở dữ liệu khác. Repository Pattern giúp phân tách các lớp và thêm một lớp truy cập dữ liệu mới, cho phép ứng dụng làm việc với nhiều loại cơ sở dữ liệu khác nhau.

Unit of Work là một mẫu thiết kế quản lý transaction khi thao tác với dữ liệu thông qua Repository Pattern. Nó đảm bảo rằng tất cả các thay đổi dữ liệu trong một quy trình nghiệp vụ được coi là một đơn vị công việc duy nhất và được thực hiện (commit) hoặc hủy bỏ (rollback) một cách nhất quán.

I. ORM và Entity Framework

1. ORM (Object-Relational Mapping)

ORM là một kỹ thuật lập trình ánh xạ dữ liệu giữa các hệ thống không tương thích, đặc biệt là giữa cơ sở dữ liệu quan hệ và ngôn ngữ lập trình hướng đối tượng. ORM cho phép bạn thao tác với dữ liệu trong cơ sở dữ liệu như các đối tượng trong mã của bạn, giúp đơn giản hóa việc truy cập và quản lý dữ liệu.

Với ORM, bạn có thể làm việc với các đối tượng/model như thể chúng là các đối tượng trong cơ sở dữ liệu. Bạn có thể viết code/query trên các đối tượng này để truy xuất dữ liệu một cách dễ dàng và trực quan.

II. Ưu điểm của ORM

  • Thao tác dữ liệu (CRUD) mà không cần viết các câu lệnh SQL trực tiếp.
  • Ánh xạ dữ liệu giữa result set và entity là chức năng chính của ORM.
  • Sử dụng Unit of Work/Context để quản lý transaction trong suốt quá trình thao tác dữ liệu.
  • Tập trung quản lý model trong một chỗ duy nhất.
  • Phù hợp với cách code tự nhiên của ngôn ngữ lập trình.
  • Cho phép lập trình viên lập trình hướng đối tượng hoàn toàn khi thực hiện các thao tác với lớp business và data.

III. Entity Framework

Entity Framework (EF) là một ORM framework phổ biến của Microsoft, cho phép các nhà phát triển .NET làm việc với cơ sở dữ liệu bằng cách sử dụng các đối tượng .NET. Nó giúp giảm thiểu code boilerplate cần thiết để truy cập và quản lý dữ liệu, đồng thời cung cấp các tính năng như LINQ to Entities để truy vấn dữ liệu một cách mạnh mẽ và linh hoạt.

  • Thao tác dữ liệu (CRUD) mà không cần viết SQL queries.
  • Ánh xạ dữ liệu giữa result set và entity là chức năng chính của ORM.
  • Sử dụng Unit of Work/Context để quản lý transaction trong suốt quá trình thao tác dữ liệu.
  • Tập trung quản lý Model trong database.
  • Tương thích với cách code tự nhiên của ngôn ngữ lập trình.
  • Cho phép lập trình hướng đối tượng trọn vẹn khi thực hiện các hành vi với lớp business và data.
  • Dễ dàng map với các đối tượng database.
  • Hỗ trợ viết POCO (Plain Old CLR Objects) và tạo database, cũng như tạo Model từ database có sẵn.

Bạn có thể tạo các lớp repository khác nhau cho mỗi entity. Tuy nhiên, đối với các thao tác cơ bản, một số phương thức chung (CRUD) có thể được sử dụng cho nhiều entity khác nhau. Việc sử dụng nhiều repository khác nhau có thể dẫn đến code trùng lặp. Do đó, việc sử dụng một repository chung (Generic Repository) là một giải pháp tốt để tránh sự dư thừa code.

IV. Triển khai Repository và Unit of Work

Dưới đây là một ví dụ về cách triển khai Repository và Unit of Work trong .NET sử dụng Entity Framework.

1. Tạo ứng dụng ASP.NET MVC

2. Tạo project Data Access Layer (DAL)

Thêm một project class library để chứa các lớp liên quan đến truy cập dữ liệu (Repository, Unit of Work, Entities). Đặt tên project này là SenTayHo.Data.

3. Tạo Database

Tạo một database để chương trình thao tác dữ liệu. Ví dụ, bạn có thể tạo một database đơn giản với các bảng như ContactsAddresses.

4. Thêm Entity Data Model

Quay lại code và thêm một Entity Data Model vào project SenTayHo.Data và cập nhật database.

Edmx File:

Mỗi đối tượng trên hình đều được tạo ra với những đối tượng tương quan, được gọi là những thuộc tính Navigation.

Bây giờ thêm một tham chiếu đến Contact Repository cho web project để bạn có thể bắt đầu phát triển. Tuy nhiên, nên tạo một lớp Repository qua các entity để tiếp cận database từ project web hoặc từ lớp business.

a. Thêm lớp BaseRepository:

b. Lớp BaseRepository thực hiện interface và định nghĩa các phương thức thao tác dữ liệu:

public class BaseRepository<T> : IBaseRepository<T> where T : class
{
    private readonly IUnitOfWork _unitOfWork;
    internal DbSet<T> dbSet;

    public BaseRepository(IUnitOfWork unitOfWork)
    {
        if (unitOfWork == null)
            throw new ArgumentNullException("unitOfWork");
        _unitOfWork = unitOfWork;
        this.dbSet = _unitOfWork.Db.Set<T>();
    }

    public T Single(object primaryKey)
    {
        var dbResult = dbSet.Find(primaryKey);
        return dbResult;
    }

    public T SingleOrDefault(object primaryKey)
    {
        var dbResult = dbSet.Find(primaryKey);
        return dbResult;
    }

    public bool Exists(object primaryKey)
    {
        return dbSet.Find(primaryKey) == null ? false : true;
    }

    public virtual int Insert(T entity)
    {
        dynamic obj = dbSet.Add(entity);
        this._unitOfWork.Db.SaveChanges();
        return obj.Id;
    }

    public virtual void Update(T entity)
    {
        dbSet.Attach(entity);
        _unitOfWork.Db.Entry(entity).State = EntityState.Modified;
        this._unitOfWork.Db.SaveChanges();
    }

    public int Delete(T entity)
    {
        if (_unitOfWork.Db.Entry(entity).State == EntityState.Detached)
        {
            dbSet.Attach(entity);
        }
        dynamic obj = dbSet.Remove(entity);
        this._unitOfWork.Db.SaveChanges();
        return obj.Id;
    }

    public IUnitOfWork UnitOfWork
    {
        get { return _unitOfWork; }
    }

    internal DbContext Database
    {
        get { return _unitOfWork.Db; }
    }

    public Dictionary<string, string> GetAuditNames(dynamic dynamicObject)
    {
        throw new NotImplementedException();
    }

    public IEnumerable<T> GetAll()
    {
        return dbSet.AsEnumerable().ToList();
    }
}

Trong lớp này, IUnitOfWork interface được sử dụng để duy trì transaction trong khi thêm mới hoặc cập nhật dữ liệu. IUnitOfWork chứa 2 phương thức, Commit()StartTransaction(), sẽ được triển khai bởi lớp UnitOfWork. Lớp này chịu trách nhiệm cung cấp DbContext hoặc Transaction để bắt đầu và triển khai transaction.

Với lớp BaseRepository, bạn sẽ thao tác với các entity như Contact, Address. Trong EF, bạn có những Model tương tự với những Model là những bảng trong database.

Bây giờ bạn sẽ tạo ra 2 lớp repository mà thừa kế lớp BaseRepository:

a. ContactRepository:

public class ContactRepository : BaseRepository<Contact>
{
    public ContactRepository(IUnitOfWork unit) : base(unit)
    {
    }
}

b. AddressRepository:

public class AddressRepository : BaseRepository<Address>
{
    public AddressRepository(IUnitOfWork unit) : base(unit)
    {
    }
}

c. Lớp controller để gọi lớp repository:

public class ContactController : Controller
{
    private IUnitOfWork uow = null;
    private ContactRepository repo = null;

    public ContactController()
    {
        uow = new UnitOfWork();
        repo = new ContactRepository(uow);
    }

    // GET: /Contact/
    public ActionResult Index()
    {
        var contacts = repo.GetAll();
        return View(contacts.ToList());
    }

    // GET: /Contact/Details/5
    public ActionResult Details(int id = 0)
    {
        Contact contact = repo.SingleOrDefault(id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    // GET: /Contact/Create
    public ActionResult Create()
    {
        return View();
    }

    // POST: /Contact/Create
    [HttpPost]
    public ActionResult Create(Contact contact)
    {
        if (ModelState.IsValid)
        {
            repo.Insert(contact);
            return RedirectToAction("Index");
        }

        return View(contact);
    }

    // GET: /Contact/Edit/5
    public ActionResult Edit(int id = 0)
    {
        Contact contact = repo.SingleOrDefault(id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    // POST: /Contact/Edit/5
    [HttpPost]
    public ActionResult Edit(Contact contact)
    {
        if (ModelState.IsValid)
        {
            repo.Update(contact);
            return RedirectToAction("Index");
        }
        return View(contact);
    }

    // GET: /Contact/Delete/5
    public ActionResult Delete(int id = 0)
    {
        Contact contact = repo.SingleOrDefault(id);
        if (contact == null)
        {
            return HttpNotFound();
        }
        return View(contact);
    }

    // POST: /Contact/Delete/5
    [HttpPost, ActionName("Delete")]
    public ActionResult DeleteConfirmed(int id)
    {
        Contact contact = repo.SingleOrDefault(id);
        repo.Delete(contact);
        return RedirectToAction("Index");
    }

    protected override void Dispose(bool disposing)
    {
        uow.Dispose();
        base.Dispose(disposing);
    }
}

V. Kết luận

Sử dụng Repository Pattern để chia lớp data từ lớp logical. Nếu kiến trúc chương trình muốn thay đổi hệ thống lưu trữ, bạn không cần lo lắng về business logic. Việc thực hiện mẫu Repository là một cách tiếp cận tốt để xây dựng chương trình của bạn, tuy có thể chậm hơn một chút so với việc truy cập trực tiếp vào data. Tuy nhiên, nó mang lại sự linh hoạt, khả năng bảo trì và mở rộng cao hơn cho ứng dụng của bạn.