This QuickStart application uses the all too familiar Northwind database and uses NHibernate browse and edit customers. It It is a very simple application that directly uses the DAO layer in many use-cases, as it is doing nothing more than table maintenance, but there is also a simple service layer that handles a fullillment process. The application uses Spring's declarative transaction management features, standard NHibernate API, and Open Session In View module. See Chapter 21, Object Relational Mapping (ORM) data access for information on those features.
Note | |
---|---|
Even though data access is performed through NHibernate API all Spring.NET provided functionality is still present when using the standard NHibernate API, as Spring transaction managment is integrated into NHibernate extension points and exception translation is provided by AOP advice. |
The QuickStart application is located in the directory directory
<spring-install-dir>\examples\Spring\Spring.Data.NHibernate.Northwind
.
Load the application using the VS.NET 2008 solution file
Spring.Northwind.2008.sln
. The application uses the
SqlLite database so no additional configuration is needed. To run the
application set the Web application as the project that starts and
Default.aspx as the start page.
The application has several layers with each layer represented as one or more VS.NET projects.
The data access layer consists of two projects, Spring.Northwind.Dao and Spring.Northwind.Dao.NHibernate. The former contains only the DAO (data access object) interfaces and the latter the NHibernate implementation of those interfaces. The project Spring.Northwind.Service contains a simple service that calls into multiple DAO objects in order to satisfy a fulliment process. The Web project is a ASP.NET web application and the Spring.Northwind.IntegrationTests project contains integration tests for the DAO and Service layers.
When you run the application you will see
Following the link to the customer listing pages bring up the following screen
You can click on the Name of the customer or the Orders link to view that customers orders. Selecting "BOTTM"'s orders brings us to the next page
Notice that the order 11045 has yet to be shipped. If you select 'Process Orders' this will call the Fulliment Service and the order will be processed and shipped.p
You can then go back to the customer list. If you select the name Elizabeth Lincoln, then you can edit the customer details.
This section discussed the Spring implementation details for each layer.
The interface IDao is a generic DAO layer that provides basic retrieval methods. They are located in the Spring.Northwind.Dao project.
public interface IDao<TEntity, TId> { TEntity Get(TId id); IList<TEntity> GetAll(); }
The ISupportsSave and ISupportsDeleteDao interfaces provide the rest of the CRUD functionality.
public interface ISupportsSave<TEntity, TId> { TId Save(TEntity entity); void Update(TEntity entity); } public interface ISupportsDeleteDao<TEntity> { void Delete(TEntity entity); }
The ICustomerDao interface combines these to manage the persistence of customer objects.
public interface ICustomerDao : IDao<Customer, string>, ISupportsDeleteDao<Customer>, ISupportsSave<Customer, string> { }
Similar interfaces are defined to manage Order and Products in IOrderDao and IProductDao respectfully.
The POCO domain objects, Customer, Order, OrderDetail and Product are defined in the Spring.Northwind.Domain namespace within the Spring.Northwind.Dao project.
The NHibernate based DAO implemenation uses the standard NHibernate APIs, retrieving the current session from the SessionFactory and using the session to retrieve or store objects to the database. An abstract base class HibernateDao is used to capture the common ISessionFactory property, provide a convenience property to access the current session, and define a GetAll Method.p
public abstract class HibernateDao { private ISessionFactory sessionFactory; /// <summary> /// Session factory for sub-classes. /// </summary> public ISessionFactory SessionFactory { protected get { return sessionFactory; } set { sessionFactory = value; } } /// <summary> /// Get's the current active session. Will retrieve session as managed by the /// Open Session In View module if enabled. /// </summary> protected ISession CurrentSession { get { return sessionFactory.GetCurrentSession(); } } protected IList<T> GetAll<T>() where T : class { ICriteria criteria = CurrentSession.CreateCriteria<T>(); return criteria.List<T>(); } }
The implementation of ICustomerDao is shown below
[Repository] public class HibernateCustomerDao : HibernateDao, ICustomerDao { // Note that the transaction demaraction is here only for the case when // the DAO object is being used directly, i.e. not as part of a service layer // call. This would be commonly only when creating an application that contains // no business logic and is essentially a table maintenance application. // These applications are affectionaly known as 'CRUD' applications, the acronym // refering to Create, Retrieve, Update, And Delete and the only operations // performed by the application. // If called from a transactional service layer, typically with the transaction // propagation setting set to REQUIRED, then any DAO operations will use the // same settings as started from the transactional layer. [Transaction(ReadOnly = true)] public Customer Get(string customerId) { return CurrentSession.Get<Customer>(customerId); } [Transaction(ReadOnly = true)] public IList<Customer> GetAll() { return GetAll<Customer>(); } [Transaction(ReadOnly = false)] public string Save(Customer customer) { return (string) CurrentSession.Save(customer); } [Transaction(ReadOnly = false)] public void Update(Customer customer) { CurrentSession.SaveOrUpdate(customer); } [Transaction(ReadOnly = false)] public void Delete(Customer customer) { CurrentSession.Delete(customer); } }
Note | |
---|---|
As mentioned in the code comments above, as this application has a distinctly CRUD based component, Spring's Transaction attribute is used to ensure that that method exeuctes as a unit of work. Often in more sophisticated applications even the basic of CRUD are handled through a service layer so as to enforce security, auditing, alterting or enforce business rules. |
The Repository attribute is used to
indicate that this class plays the role of a Repository or a Data Access
Object. The term repository comes from modeling terminology popularized
by Eric Evan's book Domain Driven Design (DDD). Those familiar with DDD
will note that this implementation is very simply and does not expose
higher level persistence functionality to the application, for example
FindCustomersWithOpenOrders
. How well the role of
Repository applies to this implementation is not relevant, and we will
often refer to Repository and DAO intechangable when describing the data
access layer. What is relevant is that the
Repository attribute serves as a marker, a place
in the code that can be used to identify methods whose invocation should
be intercepted so that additional behavior can be added. In
Aspect-Oriented Programming terminology, the Repository attribute
represents a pointcut. The behavior that we would like to add to this
DAO implementation exception translation. Exception translation from the
data access layer to a service layer is important as it shields the
service layer from the implementation details of the data access layer.
A NHibernate based DAO will throw different exceptions and a ADO.NET
based implementation and so on. Spring provides a rich technology
neutral data-access exception hierarchy. See Chapter 18, DAO support.
Instead of adding exception translation code in each data access method, AOP offers a simple solution. Using Spring's IObjectPostProcessor extension point, each DAO object that is managed by Spring will be automatically wrapped up in a proxy that adds the exception translation behavior. This is done by adding the following object definition to the Spring application context.
<objects> <!-- configure session factory --> <!-- Exception translation object post processor --> <object type="Spring.Dao.Attributes.PersistenceExceptionTranslationPostProcessor, Spring.Data"/> <!-- Configure transaction management strategy --> <!-- DAO objects go here --> </objects>
The Spring managed DAO object definitions are shown below, referring to a SessionFactory that is created via Spring's LocalSessionFactoryObject. See the file Dao.xml for more details.
<objects xmlns="http://www.springframework.net" xmlns:db="http://www.springframework.net/database"> <!-- Referenced by main application context configuration file --> <description> The Northwind object definitions for the Data Access Objects. </description> <!-- Database Configuration --> <db:provider id="DbProvider" provider="SQLite-1.0.65" connectionString="Data Source=|DataDirectory|Northwind.db;Version=3;FailIfMissing=True;"/> <!-- NHibernate SessionFactory configuration --> <object id="NHibernateSessionFactory" type="Spring.Data.NHibernate.LocalSessionFactoryObject, Spring.Data.NHibernate21"> <property name="DbProvider" ref="DbProvider"/> <property name="MappingAssemblies"> <list> <value>Spring.Northwind.Dao.NHibernate</value> </list> </property> <property name="HibernateProperties"> <dictionary> <entry key="hibernate.connection.provider" value="NHibernate.Connection.DriverConnectionProvider"/> <entry key="dialect" value="NHibernate.Dialect.SQLiteDialect"/> <entry key="connection.driver_class" value="NHibernate.Driver.SQLite20Driver"/> </dictionary> </property> <!-- provides integation with Spring's declarative transaction management features --> <property name="ExposeTransactionAwareSessionFactory" value="true" /> </object> <!-- Transaction Management Strategy - local database transactions --> <object id="transactionManager" type="Spring.Data.NHibernate.HibernateTransactionManager, Spring.Data.NHibernate21"> <property name="DbProvider" ref="DbProvider"/> <property name="SessionFactory" ref="NHibernateSessionFactory"/> </object> <!-- Exception translation object post processor --> <object type="Spring.Dao.Attributes.PersistenceExceptionTranslationPostProcessor, Spring.Data"/> <!-- Data Access Objects --> <object id="CustomerDao" type="Spring.Northwind.Dao.NHibernate.HibernateCustomerDao, Spring.Northwind.Dao.NHibernate"> <property name="SessionFactory" ref="NHibernateSessionFactory"/> </object> <object id="OrderDao" type="Spring.Northwind.Dao.NHibernate.HibernateOrderDao, Spring.Northwind.Dao.NHibernate"> <property name="SessionFactory" ref="NHibernateSessionFactory"/> </object> </objects>
Note | |
---|---|
It is not required that you use Spring's [Repository] attribute. You can specify an attribute type to the PersistenceExceptionTranslationPostProcessor via the property RepositoryAttributeType to avoid coupling your DAO implementation to Spring. |
The service layer is located in the Spring.Northwind.Services project. It defines a single service for the fulliment process
public interface IFulfillmentService { void ProcessCustomer(string customerId); }
The implementatiion class is shown below
public class FulfillmentService : IFulfillmentService { private IProductDao productDao; private ICustomerDao customerDao; private IOrderDao orderDao; private IShippingService shippingService; // Properties for the preceding fields omitted for brevity [Transaction] public void ProcessCustomer(string customerId) { //Find all orders for customer Customer customer = CustomerDao.Get(customerId); foreach (Order order in customer.Orders) { if (order.ShippedDate.HasValue) { log.Warn("Order with " + order.Id + " has already been shipped, skipping."); continue; } //Validate Order Validate(order); log.Info("Order " + order.Id + " validated, proceeding with shipping.."); //Ship with external shipping service ShippingService.ShipOrder(order); //Update shipping date order.ShippedDate = DateTime.Now; //Update shipment date OrderDao.Update(order); //Other operations...Decrease product quantity... etc } } private void Validate(Order order) { //no-op - throw exception on error. } } }
What is important to note about this method is that it uses two DAO objects, CustomerDao and OrderDao as well as an additional collaborating service, IShippingService. The fact that all of the collaborating objects are interfaced based means that we can write a unit test for the business functionality of the ProcessCustomer method. Also, the use of the [Transaction] attribute will enable this business processing to proceed as a single unit-of-work. Spring's declarative transaction management features make it very easy to mix and match different DAO objects with a service method without having to worry about propagating the transaction/connection or hibernate session to each DAO object.
The Fullfillment service layer is configured to refer to its collaborating objects as shown below in the configuration file Services.xml
<!-- Property placeholder configurer for database settings --> <object id="FulfillmentService" type="Spring.Northwind.Service.FulfillmentService, Spring.Northwind.Service"> <property name="CustomerDao" ref="CustomerDao"/> <property name="OrderDao" ref="OrderDao"/> <property name="ShippingService" ref="ShippingService"/> </object> <object id="ShippingService" type="Spring.Northwind.Service.FedExShippingService, Spring.Northwind.Service"/> <tx:attribute-driven/>
Integraiton testing in addition to unit testing can be done before integrating the service and data access layer into the Web application - where automated testing is much more difficult. While coding to interfaces and using an IoC container help enable unit testing, unit tests should not have any Spring dependency. Integration tests however greatly benefit from being able to access the objects that Spring is managing in production. This way the gap between what you testi in QA and what runs is minimized, ideally with only environment specific settings being different. In addition to easily obtaining, say a transactionally aware service object from the Spring IoC container, Spring intergration testing support classes allow you to implicitly start a transaction at the start of test method and rollback at the end. The isolation guaranteed by the database means that multiple developers can run integration tests for their data access layers simultaneously and the rollback ensures that the changes made are not persisted. While in the test method, you have a consistent view of the data and can therefore exercise all the methods of your DAO object.
The project Spring.Northwind.IntegrationTests shows how this works. As a convenience, an abstract base class is created that in turn inherits from Spring's integration testing class AbstractTransactionalDbProviderSpringContextTests
[TestFixture] public abstract class AbstractDaoIntegrationTests : AbstractTransactionalDbProviderSpringContextTests { protected override string[] ConfigLocations { get { return new string[] { "assembly://Spring.Northwind.Dao.NHibernate/Spring.Northwind.Dao/Dao.xml", "assembly://Spring.Northwind.Service/Spring.Northwind.Service/Services.xml" }; } } }
Note | |
---|---|
This unit test is NUnit based but there is similar support available for Microsoft MSTest framework. |
The exact same object definition files that will be used in the production application are loaded for the integration test. To test the data access layer, you inherit from AbstractDaoIntegrationTests and expose public properties for each DAO implementation you want to test. Within each test method exercise the API of the DAO. This also tests the NHibernate mappings.
[TestFixture] public class NorthwindIntegrationTests : AbstractDaoIntegrationTests { private ICustomerDao customerDao; private IOrderDao orderDao; private ISessionFactory sessionFactory; // These properties will be injected based on type public ICustomerDao CustomerDao { set { customerDao = value; } } public IOrderDao OrderDao { set { orderDao = value; } } public ISessionFactory SessionFactory { set { sessionFactory = value; } } [Test] public void CustomerDaoTests() { Assert.AreEqual(91, customerDao.GetAll().Count); Customer c = new Customer(); c.Id = "MPOLL"; c.CompanyName = "Interface21"; customerDao.Save(c); c = customerDao.Get("MPOLL"); Assert.AreEqual(c.Id, "MPOLL"); Assert.AreEqual(c.CompanyName, "Interface21"); //Without flushing, nothing changes in the database: int customerCount = (int)AdoTemplate.ExecuteScalar(CommandType.Text, "select count(*) from Customers"); Assert.AreEqual(91, customerCount); //Flush the session to execute sql in the db. SessionFactoryUtils.GetSession(sessionFactory, true).Flush(); //Now changes are visible outside the session but within the same database transaction customerCount = (int)AdoTemplate.ExecuteScalar(CommandType.Text, "select count(*) from Customers"); Assert.AreEqual(92, customerCount); Assert.AreEqual(92, customerDao.GetAll().Count); c.CompanyName = "SpringSource"; customerDao.Update(c); c = customerDao.Get("MPOLL"); Assert.AreEqual(c.Id, "MPOLL"); Assert.AreEqual(c.CompanyName, "SpringSource"); customerDao.Delete(c); SessionFactoryUtils.GetSession(sessionFactory, true).Flush(); customerCount = (int)AdoTemplate.ExecuteScalar(CommandType.Text, "select count(*) from Customers"); Assert.AreEqual(92, customerCount); try { c = customerDao.Get("MPOLL"); Assert.Fail("Should have thrown HibernateObjectRetrievalFailureException when finding customer with Id = MPOLL"); } catch (HibernateObjectRetrievalFailureException e) { Assert.AreEqual("Customer", e.PersistentClassName); } } [Test] public void ProductDaoTests() { // ommited for brevity } }
This test uses AdoTemplate to access the database using the standard ADO.NET APIs. It is done to demonstrate that the common configuration of NHibernate is for it not to flush to the database until a commit occurs. If we did not explicitly flush, then no SQL would be sent down to the database and some potential errors would go undetected. Since the test method will rollback the transaction, we don't have to worry about 'dirtying' the database and changing its state.
The Web application uses Dependency Injection on the .aspx pages so that they can access the servcies of the middle tier, for example the FullfillmentService, or in the case of simple table maintenance, the DAO objects directly.
For example the FullfillmentResult.aspx code behind is shown below
public partial class FullfillmentResult : Page { private IFulfillmentService fulfillmentService; private ICustomerEditController customerEditController; public IFulfillmentService FulfillmentService { set { fulfillmentService = value; } } public ICustomerEditController CustomerEditController { set { customerEditController = value; } } protected void Page_Load(object sender, EventArgs e) { /// code omitted for brevity fulfillmentService.ProcessCustomer(customerEditController.CurrentCustomer.Id); } protected void customerOrders_Click(object sender, EventArgs e) { SetResult("Back"); }
The page is configured in Spring as shown below
<object type="FulfillmentResult.aspx"> <property name="FulfillmentService" ref="FulfillmentService" /> <property name="CustomerEditController" ref="CustomerEditController" /> <property name="Results"> <dictionary> <entry key="Back" value="redirect:CustomerOrders.aspx" /> </dictionary> </property> </object>
The page is injected with a reference to the FullfillmentService and also another UI component. While Spring's ASP.NET framework supports DI for standard ASP.NET pages and user controls, you can also inherit from Spring's base page class to get added functionality. In this example the use of externalized page flow, or Result Mapping is shown. The Results property indicates the 'how', 'where' and 'what data' to bring along when moving between different web pages and associates it with a logical name "Back". This avoid hardcoding server side transfers or redirects in your code as well as other ASP.NET page references. See the chapter on Spring's ASP.NET Web Framework for more details.