Viết ASP.NET bằng MVP và NHibernate phần 2 - Implement Data Layer
Posted On Monday, October 20, 2008 at at 6:10 PM by UnknownBài viết này được dịch, tóm tắt và bổ sung dựa vào bài viết NHibernate Best Practices with ASP.NET, 1.2nd Ed trên code Project. Các code sample trong bài dựa vào database Northwind của Microsoft và tham khảo 99,99% từ code mẫu của tác giả Billy McCafferty.
I. Nhắc lại kĩ thuật Separated Interface
- Separated Interface là một kĩ thuật trong lập trình nhằm đạt được mục tiêu phân chia rạch ròi sự phụ thuộc giữa các tầng (tiers) trong chương trình. Kĩ thuật này được giới thiệu lần đầu bởi sư phụ Martin Fowler, nó cũng là một trong những nguyên tắc lập trình nổi tiếng được nhắc đến trong cuốn sách Agile Software Development của Robert Martin. Trong thực tế, kĩ thuật này thường được áp dụng khi người ta implement Domain Layer và Data Access Layer. Có một ví dụ thế này, trong domain layer ta tạo một lớp Customer. Lớp Customer này sẽ có nhiều method, một trong số đó là method GetAllCustomer(). Vậy lớp Customer cần phải sử dụng một data access object để xử lý yêu cầu đó. Kết quả là đối tượng Customer ít nhất đã có một sự phụ thuộc nhất định đến CustomerDao (nằm trong Data Access Layer). Mặt khác đối với lớp CustomerDao, nó sẽ trực tiếp truy xuất vào database và trả về kết quả là một mảng/list các đối tượng kiểu Customer, vì thế nó phải phụ thuộc ngược lại Domain Layer vì như chúng ta đã thống nhất với nhau mình sẽ khai báo các class Entity trong Domain Layer (Project EnterpriseSample.Core). Nếu chưa/không biết kĩ thuật này, có thể chúng ta có thể dùng cheat code bằng cách tách tất cả những lớp định nghĩa Entity ra một project khác ví dụ như EnterpriseSampe.Entities. Còn nếu đã biết Dependency Inversion Priciple rồi thì ta sẽ làm một cách sạch sẽ và trong sáng hơn nhiều .
- Có một giải pháp còn cheat ghê hơn nữa là đưa tất cả các lớp DAO, nói chung là mọi thứ trong Data Access Layer vào chung một assembly với Domain Layer, tức là vào chung project EnterpriseSample.Core. Và để các lớp này khỏi chung chạ với nhau, ta phải tạo ra một sự độc lập ảo bằng cách cho chúng vào những folder khác nhau trong project chẳng hạn như folder Domain và folder Data. Cách làm này sẽ gây ra những vấn đề sau đây:
+ Domain layer và Data layer sẽ phụ thuộc hai chiều vào nhau.
+ Ở chung một nhà lại phụ thuộc nhiều thế này thì lâu ngày … sẽ có con chung . Code của 2 layer này sẽ trùng lặp với nhau không chừng.
+ Nếu trong code của Entity object sử dụng trực tiếp một instance của class DAO thì sẽ rất khó Unit Test nó mà không cần database thực. Vì khó test nên có thể người ta sẽ lười test, mà lười rồi thì khỏi viết Unit Test .
- Thực ra cách mà tác giả đề nghị là domain và data layers nên được đặt trong các assembly độc lập. Chẳng hạn như XXXX.Core và XXXX.Data. Domain layer (assembly XXXX.Core) sẽ chứa những lớp domain và DAO Interface. Còn Data Layer (assembly XXXX.Data) chỉ chứa những lớp DAO, những lớp DAO này là những lớp sẽ implement các Interface nói trên trong domain layer. Các bạn hãy xem lại hình vẽ cấu trúc này lần nữa:
Hình 1: Quan hệ giữa project Core và project Data
Các bạn đang xem bài viết ASPNET bằng MVP và NHibernate phần 2 từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)
- Cách làm trong sáng này có một số lợi ích sau:
+Domain Layer sẽ hoàn toàn độc lập với các assembly liên quan đến database như NHibernate hay System.Data.SqlClient
+Domain Layer không cần quan tâm xem data layer hoạt động thế nào, truy xuất loại database gì… Vì thế việc chuyển đổi các loại database khác nhau cũng như thay đổi chi tiết implementation của tầng data layer rất dễ dàng và không hề ảnh hưởng đến code của domain layer.
+ Vì hai layer này quan hệ một chiều với nhau như vậy nên những fan của Unit testing có thể sử dụng các kĩ thuật “Mocked” data-access layer trong domain layer để test các lớp Entity mà không cần có database thực.
II. Khai báo các Interface cần thiết
- Bây giờ chúng ta sẽ từng bước khai báo các interface này. Trước hết là interface IDao, đây là một interface chung nhất khai báo các function cơ bản nhất mà bất cứ một data access object nào cũng có, nội dung interface này như sau:
Code 1: Khai báo các function chung trong interface IDao
- Đây là 1 generic interface với hai kiểu generic là T và IdT. Nếu ta nhìn phần khai báo các function thì cũng đoán ra nếu một lớp DAO nào implement interface này hoặc một interface nào inherit từ interface này sẽ phải khai báo kiểu của Entity và kiểu Id của Entity đó. Ví dụ như hai interface ICustomerDao và ISupplierDao như sau:
Code 2: Các interface kế thừa từ Generic interface IDao
- Vậy đến khi implement thực sự các Data Access Object, ta sẽ implement các interface như ICustomerDao, ISupplierDao. Ở phần 1, chúng ta đã tạo 1 lớp là HistoricalOrderSummaryDao để giữ giá trị khi gọi Stored Procedure. Vì đây là một “value class“ đặc biệt, không phải là một Entity chính thức nên nó không cần Id; do đó interface DAO tương ứng cũng không cần phải kế thừa từ interface IDao:
Code 3: Interface IHistoricalOrderSummaryDao không cần kế thừa từ IDao
Các bạn đang xem bài viết ASPNET bằng MVP và NHibernate phần 2 từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)
- Cuối cùng chúng ta sẽ tạo một interface IDaoFactory, nếu các bạn đã đọc về Factory Pattern thì trong trường hợp này chúng ta đang sử dụng Abstract Factory. Sẽ có 1 lớp Factory implement interface này và tùy chúng ta sử dụng ORM nào thì các lớp DAO tương ứng sẽ được tạo ra. Cho nên trong phần sau chúng ta sẽ viết 1 lớp NHibernateDaoFactory với mục đích tạo ra các NHibernate Dao Object:
Code 4: Nội dung của interface IDaoFactory
III. Làm việc với NHibernate Session
- Khi viết chương trình có sử dụng database, người ta thường tìm cách giới hạn số lần mở connection đến database server sao cho càng ít kết nối càng tốt. Nếu các bạn quan tâm đến vẫn đề này hẵn các bạn sẽ biết đến khái niệm Transaction. Trong NHibernate cũng có một khái niệm như vậy và chúng ta hãy tạm chấp nhận một NHibernate Session là một connection đến database server bằng NHibernate. Khi viết một ứng dụng web, việc giảm thiểu các connection đến database là điều rất đáng để quan tâm. Giả sử chúng ta viết 1 trang aspx trên trang đó có sử dụng 10 usercontrol, mỗi usercontrol lại cần kết nối với database để load dữ liệu riêng của nó. Như vậy một lần mở trang web ta đã mở 10+ connection đến database server. Chúng ta có thể giải quyết vấn đề nhức đầu này bằng cách đưa tất cả các request đó vào cùng một transtaction. Cụ thể thế nào thì chúng ta sẽ bàn vào bài sau, còn trong phạm vi bài này chúng ta hãy chấp nhận rằng mỗi một Dao object khi cần kết nối đến database để đọc/ghi dữ liệu nó sẽ cần đến một NHibernate.ISession. Và tác giả đã implement một lớp NHibernateSessionManager cho chúng ta sử dụng, đối số cần truyền cho method GetSesssionFrom() là một giá trị string chứa đường dẫn đến file config của NHibernate. Cách làm này của tác giả có rất nhiều mục đích như: hỗ trợ truy xuất nhiều database một lúc và sử dụng một kĩ thuật gọi là OpenSessionInView để quản lý connection đến database server,.. Tác giả đã implement các lớp Configuration để hỗ trợ đọc các file config NHibernate vì các config này được đặt vào một file xml riêng và không còn nằm chung trong file web/app.config. Các lớp này được viết một lần nhưng ta có thể sử dụng chúng cho nhiều project khác nên chúng được đặt trong một assembly riêng tên là ProjectBase.Data. Chúng được đặt trong thư mục NhibernateSessionMgmt; nếu các bạn quan tâm có thể tìm hiểu khái niệm Configuration của .NET Framework 2.0
Sau đây là một ví dụ có sử dụng NHibernate session:
Code 9: Sử dụng NHibernate Session
IV. Implement DaoFactory và các lớp Dao tương ứng
- Vậy là cơ bản chúng ta đã hoàn thành xong Domain Layer. Xin nhắc lại project EnterpriseSample.Core chỉ chứa các lớp Entity và các interface Dao, không chứa bất kì một lớp Dao nào. Ngay sau đây chúng ta sẽ tạo các lớp Dao này trong project EnterpriseSample.Data:
IV.1 Generic DAO
- Các lớp Dao là những lớp trực tiếp sử dụng NHibernate để thực hiện các lệnh thêm xóa, sửa, update vào database. Ví dụ ta có CustomerDao sẽ implement các function như UpdateCustomer, GetAllCustomers. Nhìn chung thì mỗi một Entity trong chương trình đều có các method tương tự nhau như: GetById, Save, Delete, Update. Thử tưởng tượng bạn có 20 tables, ứng với 20 class Entity trong chương trình và mỗi một Entity như vậy phải implement ít nhất 4 function GetById, Save, Delete, Update thì đúng là một cực hình. Người ta sẽ giải quyết vấn đề trùng lặp code này bằng cách sử dụng Generic của .NET framework 2.0, tất cả những hàm tương tự nhau trong các lớp Entity sẽ được implement trong một class Generic:
Code 5: Lớp Generic Dao AbstractNHibernateDao
- Các lớp Dao trong chương trình ngoài implement interface IxxxDao sẽ inherit từ lớp abstract này:
Code 6: Cách sử dụng lớp Generic AbstractNHibernateDao
IV.2 DAO Factory
- Dao Factory là gì và tại sao ta lại cần một lớp Factory? Nếu bạn có quan tâm đến Design Pattern chắc là bạn đã nghe đến các pattern như Abstract Factory, Factory Method. Một Dao Factory có khả năng tạo ra các Dao Object theo một tiêu chuẩn nào đó. Trong project này ta sử dụng NHibernate để thao tác với cơ sở dữ liệu, vậy thì Dao Factory của ta sẽ tạo ra các NHibernate Dao Object để tương tác với NHibernate. Ta sẽ tạo ra một lớp là NHibernateDaoFactory với tiêu chí đó, code như sau:
Code 7: Lớp NHibernateDaoFactory
Các bạn đang xem bài viết ASPNET bằng MVP và NHibernate phần 2 từ blog của Nguyễn Thoại (http://nthoai.blogspot.com)
IV.3 Nhận kết quả từ Stored Procedured như thế nào?
- Trong bài trước chúng ta có implement lớp HistoricalOrderSumary để giữ giá trị trả về khi gọi một stored procedure. Ta cũng đã làm một file mapping xml tương ứng cho nó. Nhưng để thực hiện các bước cụ thể nhằm map các kết quả từ database, ta phải làm quen một khái niệm trong NHibernate là IQuery và Transform. Chúng ta sẽ tạo một instance kiểu IQuery bằng static method GetNamedQuery với parameter là tên query được khai báo trong file mapping XML, từ đó hướng dẫn NHibernate map kết quả trả về bằng constructor của lớp HistoricalOrderSumary:
Code 8: Cách map kết quả từ stored procedure
V. Kết luận
- Qua phần hai này, chúng ta đã làm quen với khái niệm Session của NHibernate một cách rất khái quát , trong bài kế tiếp chúng ta sẽ tìm hiều kĩ hơn tại sao tác giả lại tổ chức lớp lang như vậy. Các lớp phục vụ Session management này được viết một lần và sử dụng lại ở nhiều project khác nhau nên tác giả đề nghị ta đưa các lớp này qua một assembly khác để tái sử dụng. Interface IDao và lớp generic Dao AbstractNHibernateDao cũng được đặt trong assembly này vì chúng rất generic và có thể tái sử dụng ở những project khác mà không cần sửa code. Trong assembly EnterpriseSample.Data, chúng ta implement các lớp Dao cũng như NHibernateDaoFactory để tạo ra các NHibernate Dao. Nhờ sử dụng Generic nên rất nhiều code được sử dụng lại nên các bạn thấy rằng rất ít lớp trong EnterpriseSamle.Data và code của những lớp này cũng khá là gọn. Các lớp Dao cũng như lớp Generic Dao sử dụng một instance NHibernate.ISession để thao tác với database, ta chỉ cần truyền đường dẫn file config và Session Manager sẽ trả về một NHibernate session. Mục đích của cách làm này là giúp giảm thiểu số lần mở connection đến database server để tăng performance. Chúng ta sẽ còn bàn nhiều về các kĩ thuật liên quan trong bài thứ 4.
Code phần 2: http://nthoaiblog.googlepages.com/EnterpriseSample-part2.zip
Các đoạn code minh họa trong bài viết được rút gọn cho dễ hiểu, code được implement cuối cùng trong project sẽ có nhiều điểm khác biệt với hình...
(Còn tiếp)
:D, chỉ có hồi xưa ở DDV mới gộp chung mấy lớp GetAll trong domain :P. Nói chung MVP và MVC có rất nhiều phong cách, tùy từng trường hợp mà người ta chơi một phong cách, tao giờ đang làm một project nó viết lại lớp DAL + lớp binding từ stored procedure -> object
à, mày nên chia nhỏ bài viết ra thêm nữa, với lại rõ ràng hơn, đọc bài dài lười lắm :D
:)...HI bạn!
mình mới học lập trình NHibernate nhưng mình dùng FluentNHibernate để map dữ liệu. (mình chỉ dùng 1 file là hibernate.hbm.xml để map, ko dùng đến file Web.config hay App.Config)
Mình có vấn đề muốn hỏi bạn là việc Caching trong NHibernate khi mình dùng FluentHibernate. Khi mình caching query mình dùng Iquery.SetCachable(true) nhưng mình ko biết là sao đặt đc tên cho cache của mình và set time để cache hết hiệu lực...ví dụ mình chỉ muốn cache trong 24h và sau đó nó sẽ xóa hết các kết quả cache và các từ khóa cache.
Nếu có thế bạn có thể gửi hướng dẫn cho mình qua mail: mrdhtai@yahoo.com