在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
IntroductionNHibernate, like other ORM tools, has alleviated the maintenance of thousands of lines of code and stored procedures, thus allowing developers to focus more attention on the core of a project: its domain model and business logic. Even if you automatically generate your ADO.NET data-access layer using a tool such as CodeSmith or LLBLGen Pro (both great tools), NHibernate provides the flexibility in decoupling your business model from your relational model. Your database should be an implementation detail that is defined to support your domain model, not the other way around. As forums quickly fill with heated debates concerning these points, this article will not focus on proving the benefits of using an ORM tool over ADO.NET, or NHibernate over another ORM tool, but on describing best practices for integrating NHibernate into ASP.NET using well established design patterns and "lessons learned from the field". This article assumes a good understanding of C# and NHibernate, experience with the Data Access Object pattern, and at least a basic familiarity with Generics. If you're just getting acquainted with NHibernate, I'd recommend reading these two great introductions at TheServerSide.net: Part 1 and Part 2. For an extensive overview of the Data Access Object (DAO) design pattern, go to J2EE's BluePrints catalog. In building solid data integration within an ASP.NET 2.0 application, we aim to achieve the following objectives:
A sample application has been included that demonstrates the merging of NHibernate, ASP.NET, and .NET Generics while meeting the above objectives. What follows is a description of how each of the aforementioned objectives has been tackled in the application. But before getting into the implementation details, let's skip right to the chase and get the sample up and running. The Sample ApplicationThe sample application, at the risk of being terribly cliché, utilizes the Northwind database within SQL Server 2000 to view and update a listing of Northwind customers. To demonstrate the use of lazy-loading, the application also displays the orders that each customer has made. All you need to run the sample locally is IIS with the .NET 2.0 Framework installed, and SQL Server 2000 containing the Northwind database. To get the application up and running:
Now that you're able to follow along with the example in front of you, we'll examine how the application was developed to meet our design objectives... Meeting the Design ObjectivesWhen writing an ASP.NET application, my primary goals are:
To assist with keeping logical tiers loosely coupled, the included sample application is split into four projects: Web, Core, Data, and Tests. In peeling the layers of the onion, let's begin with the simple presentation/controller layer and work our way down. NHibernateSample.WebAs expected, the NHibernateSample.Web project contains application configuration and web pages. In this sample, the code-behind pages act as controllers, communicating with the business and data access layers accordingly. Arguably, this is not best-practice MVC separation, but it's simple, and serves well for the demonstration. (I'll leave the thoroughly debated MVC-and-ASP.NET discussion for another day.) Here's a closer look at some of the more interesting bits... Open Session in ViewIf you want to leverage NHibernate's lazy-loading (which you most definitely will), then the Open-Session-in-View pattern is the way to go. ("Session" in this context is the NHibernate session...not the ASP.NET " To see how this has been implemented, take a look at the class <httpModules>
<add name="NHibernateSessionModule"
type="NHibernateSample.Web.NHibernateSessionModule" />
</httpModules>
The HTTPModule included in the sample application begins a transaction at the beginning of the web request, and commits/closes it at the end of the request. There's very little overhead associated with this as NHibernate doesn't actually open a database connection until needed. As we'll see, you may switch out this strategy with others exposed by the session manager. Other strategies that you may want to consider are opening a session not associated with a transaction and/or registering an NHibernate Settings Within web.configThere are two key settings within web.config to optimize NHibernate:
The other optional setting not to be ignored, <add key="hibernate.connection.isolation" value="ReadCommitted" />
<add key="hibernate.default_schema" value="Northwind.dbo" />
In order to decouple the implementation details of data access from the NHibernateSample.Web project, NHibernate session management has been completely separated into the NHibernateSample.Data project. To inform NHibernate where the embedded HBM mapping files reside, define a web.config setting named A Simple List and Update FormThe web project contains two web pages: ViewCustomers.aspx and EditCustomer.aspx. (I'll give you three guesses to figure out what they do.) The important thing to note is that the code-behind pages work with a DAO factory to talk to the database; i.e., the code isn't bound to a concrete implementation of a data access object. This makes it much easier to swap DAO implementations and unit test your code without depending on a live database. With everything in place, the following is an example of how easy it is to retrieve all the customers in the database: IDaoFactory daoFactory = new NHibernateDaoFactory();
ICustomerDao customerDao = daoFactory.GetCustomerDao();
IList<Customer> allCustomers = customerDao.GetAll();
In the above example, a concrete reference to
While it is acceptable to use the IDaoFactory daoFactory = new NHibernateDaoFactory();
ICustomerDao customerDao = daoFactory.GetCustomerDao();
Customer customer = customerDao.GetById("CHOPS", false);
// Give the customer its DAO dependency via a public setter
customer.OrderDao = daoFactory.GetOrderDao();
Using this technique, the business layer never needs to depend directly on the data layer. Instead, it depends on interfaces defined within the same layer, as we'll see next within the NHibernateSample.Core project. NHibernateSample.CoreThe NHibernateSample.Core project contains the domain model and NHibernate HBM files. This project also contains interfaces to data access objects in the Data Dependency InversionYou'll notice that the NHibernateSample.Core project does not contain implementation details of data access objects, only interfaces describing the services it needs. The concrete DAO classes which implement these interfaces are found within NHibernateSample.Data. This is a technique called Separated Interface by Martin Fowler or "Dependency Inversion" by Robert Martin in Agile Software Development. Considering NHibernateSample.Core as an "upper-level layer" and NHibernateSample.Data as a "lower-level layer", then, as Martin describes, "each of the upper-level layers declares an abstract interface for the services that it needs. The lower-level layers are then realized from these abstract interfaces. ... Thus, the upper layers do not depend on the lower layers. Instead, the lower layers depend on abstract service interfaces declared in the upper layers". Dependency inversion is the perfect technique for breaking a bi-directional dependency between domain and data layers. To see this in action, the data interfaces are described in Generics with NHibernateBy far, one of the greatest benefits that C# 2.0 has brought to the table is the inclusion of generics. With generics, more code reuse can be effectively realized while still enforcing strongly typed coding "contracts". But while the benefits are great, NHibernate has not yet been upgraded to take advantage of this new language feature. (I know they're busy at work doing just that, though.) In the meantime, a solution can be found here called - quick, take a guess - NHibernate.Generics. NHibernate.Generics provides generic-typed wrappers for the public Customer() {
// Implement parent/child relationship add/remove scaffolding
_orders = new EntityList<Order>(
delegate(Order order) { order.ParentCustomer = this; },
delegate(Order order) { order.ParentCustomer = null; }
);
}
In the above example, note that private members encapsulating these relationships must be in the following format: NHibernateSample.DataThe NHibernateSample.Data project contains the implementation details for communicating with the database and managing NHibernate sessions. The DAO Factory and Generic DAOThe DAO factory and generic DAO objects I've implemented as
Looking at the Handling the NHibernate SessionFinally, the only remaining question is how are NHibernate sessions managed? Details answering this question may be found in the class
This flow, as well as the rest of The HTTPModule described in the NHibernateSample.Web project opens a transaction at the beginning of a web request, and commits/closes it at the end of the request. The following is an example of modifying the HTTPModule so that an public void Init(HttpApplication context) {
context.BeginRequest +=
new EventHandler(InitNHibernateSession);
...
}
private void InitNHibernateSession(object sender, EventArgs e) {
IInterceptor myNHibernateInterceptor = ...
// Bind the interceptor to the session.
// Using open-session-in-view, an interceptor
// cannot be bound to an already opened session,
// so this must be our very first step.
NHibernateSessionManager.Instance.RegisterInterceptor(myNHibernateInterceptor);
// Encapsulate the already opened session within a transaction
NHibernateSessionManager.Instance.BeginTransaction();
}
NHibernateSample.TestsI'll assume you can probably guess what this project is for. Unit Test PerformanceIt's imperative for unit tests to be blazing fast. If a suite of unit tests takes too long to run, developers stop running them - and we want them running unit tests all the time! In fact, if a test takes more than 0.1 second to run, the test is probably too slow. Now, if you've done unit testing in the past, you know that any unit test requiring access to a live database takes much longer than this to run. With NUnit, you can put tests into categories, making it easy to run different groups of tests at a time, thus excluding the tests that connect to a database most of the time. Here's a quick example: [TestFixture]
[Category("NHibernate Tests")]
public class SomeTests
{
[Test]
public void TestSomethingThatDependsOnDb() { ... }
}
Testing with NHibernateIn a previous version of this article, the Testing with NHibernate "Mocked"Unless you're specifically testing DAO classes, you usually don't want to run unit tests that are dependent on a live database. They're slow and volatile by nature; i.e., if the data changes, the tests break. When testing business logic, unit tests shouldn't break if data changes. But the major obstacle is that business objects themselves may depend on DAOs. Using the abstract factory pattern that we've put into place, we can inject mock DAOs into the business objects, thereby simulating communications with the database. An example is included in NHibernateSample.Tests.Domain.CustomerTests. The following snippet creates the mock DAO and gives it to the Customer customer = new Customer("Acme Anvils");
customer.ID = "ACME";
customer.OrderDao = new MockOrderDao();
Unfortunately, more often than not, you're maintaining legacy code that has no semblance of the ideal "code-to-interface" that allows for such flexibility. Usually, there are many explicit dependencies to concrete objects, and it is difficult to replace data-access objects with mock objects to simulate database communications. In these situations, your options are to either refactor the legacy code to fit within a test harness, or to use an object mocking tool such as TypeMock. With TypeMock, it is even possible to mock sealed and singleton classes - an impressive feat to pull off without such a tool. Albeit powerful, TypeMock is best left alone unless absolutely needed; using TypeMock prematurely makes it tempting to not always code to interface. The more appropriate course to take when working with legacy code - time and budget permitting - is to refactor the code for greater flexibility. Working Effectively with Legacy Code by Michael Feathers is full of great ideas for refactoring legacy code into a test harness. Putting it into PracticeThe sample application provides a strong data-layer foundation for building scalable web applications up to the enterprise level. (Almost all of the techniques also fit well for building Windows Forms applications.) But before using it within your own environment, I would recommend the following:
I hope this article has helped with putting best practices into place for leveraging the benefits of ASP.NET, NHibernate, and Generics. I'm currently having great success with this approach on my projects, and look forward to hearing your experiences as well. Please let me know if you have any questions or suggestions. |
请发表评论