A Journey with Domain Driven Design (and NHibernate) - Part 8

We left off last time with our first association mapped and tested.  Let’s dig in a bit deeper with NHibernate mapping. 

Today I decided to separate my inherited classes into their own mapping file.  This was as easy as creating the new files and <hibernate-mapping> root elements, copying the <joined-subclass> elements over. 

I created the Item.hbm.xml:

<hibernate-mapping xmlns="urn:nhibernate-mapping-2.0" assembly="Flux88.Videocracy.DomainModel" namespace="Flux88.Videocracy.DomainModel" default-lazy="false">

  <class name="Item" table="Items">

    <id name="Id" column="itemId">

      <generator class="identity" /> 

    </id>

 

    <property name="Name" not-null="true" />

    <property name="Upc" not-null="true" />

 

  </class

</hibernate-mapping>

and Video.hbm.xml:

<joined-subclass name="Video" extends="Item" table="Videos">

    <key column="itemId" />

 

    <many-to-one name="Movie" class="Movie" column="movieId" />

    <property name="Format" type="Int32" column="videoFormatId" />

 

  </joined-subclass>

In the video mapping, you can see how it inherits from the Item class.  If you recall back to where I explained how inheritance works, you can see it in action here.   Each video will have it’s Name and UPC stored in the Items table, and the Format and MovieId in the Videos table.  The Movie.hbm.xml is below:

  <class name="Movie" table="Movies">

    <id name="Id" column="movieId" access="nosetter.camelcase-underscore">

      <generator class="identity" />

    </id>

 

    <property name="Name" not-null="true" />

    <property name="Year" not-null="true" />

    <property name="VideoReleaseDate" not-null="true" />

 

    <property name="Category" type="Int32" column="categoryId" not-null="true" />   

 

  </class>

This should give you an idea on how to map your entities.

I also did a little refactoring on the naming of my classes.  I didn’t like the naming of VideoGame because I thought it was better represented as a GameTitle.  I also renamed Transaction to RentalTransaction.  This will help avoid confusion when talking about database transactions.  I realize that not everything will be a rental (obviously most video stores also sell movies and candy, etc) but this name will suffice for now.  Since I am backed up by tests, I can refactor often and with complete confidence that I haven’t broken any old code.

So let’s revisit our feature list

  • Add new Customer / Account
  • Add other members to an account
  • Restrict Certain members from renting certain content
  • Query for a customer by customer # (swipe card, etc), phone number, or last name
  • Add new rental item (move game, console, vcr, etc)
  • Rent an item to a customer
  • Check Items Back in
  • Get movies checked out for a customer
  • Query for an item, see who has it

Some of the remaining things on the list involve querying, so we’ll create a class to assist in that.  Listed below is the overview of a class called Repository.  

CropperCapture[5]

Repository takes advantage of generics to avoid a lot of duplicate code.  This is a quick implementation of the Repository pattern, you’ll find more in-depth implementations in Ayende Rahien’s Rhino Commons and Dave Donaldson’s NHibernate Repository.  The main point here is that we can abstract 90% of NHibernates functionality in an easy to use class.  If we need to do something complex, we can either expose some more methods here, or we can expose the session to the rest of your project.  This is a design choice that is really up to you.  I generally choose to hide NHibernate from UI developers, especially junior developers.

I provided some static members for just retrieving data, however to save data and participate in transactions, I created instance methods.  This class will serve most all of our data access needs.  It is the single communication point with our persistence layer to the presentation layer.

One side-note.  I have not written any tests for this class (shame on me!).  I find it difficult to test it without duplicating what I have already done with the integration/persistence tests.  I imagine that I could use a mocking framework like NMock or RhinoMocks, however this would distract from the series and frankly I am not very skilled at mocking.  If someone would like to inject a few words about this in the comments, I’d love to hear them.

Ok, back to our list.  I’ll tackle Restrict Certain Members from renting certain content.  This basically means that minors shouldn’t be able to rent rated R movies. 

The ratings for our items apply not to the items themselves, but to the Movie and GameTitle classes.  So to validate that the customer can rent one of these items, we must inspect the rating.

A naïve approach would be like this:

public void AddRental(Rental r)

{

    //don't allow the same rental to be added twice

    if (_rentals.Contains(r)) return;

 

    if(r.Item is Video)

        if(!((Video)r.Item).Movie.Rating > _customer.RentalRestriction)

            throw new ApplicationException("Customer cannot rent this movie.");

 

    if(r.Item is Game)

        if(!((Game)r.Item).GameTitle.Rating > _customer.RentalRestriction)

            throw new ApplicationException("Customer cannot rent this game.");

 

    _rentals.Add(r);

    _subTotal += r.RentalPrice; 

}

 

The bold portion is what I added to check for the rating.  This code should never see the light of day.  Each new type of item needs another cryptic if statement here.  A better solution is to use the Template Method design pattern.  We’ll leave it up to the item to setup a construct for verifying that rentals are allowed.

By default, we won’t restrict rentals.  In the item class, we’ll provide a virtual method that returns true always.  Inherited classes can make their own decisions by overriding the method.

public virtual bool CanRentFor(Customer customer)

{

    return true;

}

In our Video class, we need to base our decision off of the Movie’s rating.

public override bool CanRentFor(Customer customer)

{

    return _movie.Rating <= customer.RentalRestriction;

}

In our Game class, we need to base the decision on the GameTitle’s rating.

public override bool CanRentFor(Customer customer)

{

    return _gameTitle.Rating <= customer.RentalRestriction;

}

unit test results

For another Item we might create, say Console, we can choose to accept the default behavior or write our own.  This will ensure that certain customers do not rent items that they are not allowed to rent.

I’ve added a few tests and ensure they are passing.

Now I’ll switch gears and look at the next item on the list, which is Can Query for Account by # (swipe card) or customer phone number.  This requirement will allow us to bring up the customer’s account when they are at the counter.  We would normally extend this requirement to add many more search options, however to be brief, we’ll just implement the two of them.

For this we can make excellent use of the Repository class we wrote earlier.  This test will involve the database, so we will place it in the Persistence test project.

 I want to keep the database related code out of the DomainModel class.  I created a class that can hold any of our queries for us.  This way we keep a clean separation between the model and persistence.  Remember, we cannot have a dll reference to the persistence project in the domain model project.  (This would lead to circular references – not to mention a bad design idea).

Here is the AccountFinder class:

public class AccountFinder

{

    Repository<Account> _repository;

 

    public AccountFinder(ISession session)

    {

        _repository = new Repository<Account>(session);

    }

 

 

    public Account FindById(int id)

    {

        return _repository.Session.Get<Account>(id);           

    }

 

    public IList<Account> FindByCustomerPhone(string phone)

    {

        string hql = "SELECT a FROM Account a JOIN a.Members c WHERE c.HomePhone LIKE :phone";

        IQuery query = _repository.Session.CreateQuery(hql);

 

        //this will set our parameter named :phone

        query.SetString("phone", phone);

 

        return query.List<Account>();

    }

}

Here I am allowing you to specify the session to use in the constructor. This way we can easily supply a session with a transaction (which will come in handy for the test).  In the first method we simply defer the call to Session.Get().  The second method takes advantage of HQL, or Hibernate Query Language.  This allows us to write queries easily, using familiar syntax, only we speak in terms of our objects.  You don’t see any database columns here.  HQL is very powerful, but can take some getting used to, because the results of your queries go directly into objects.  I’ve found that the best reference so far for HQL is Hibernate in Action book.

I created 2 tests to verify the above functionality:

[Test]

public void CanQueryForAccountByIdWithRepository()

{

    using (ISession session = SessionSource.Current.GetSession())

    {

        using (ITransaction tx = session.BeginTransaction())

        {

            Account acct = new Account();

            Customer customer = new Customer("Some", "Dude");                   

            customer.HomePhone = "212-224-3456";

            customer.Address = new Address();                   

            acct.AddMember(customer);

 

            session.SaveOrUpdate(acct);

            session.Flush();                   

 

            int id = acct.Id;

 

            AccountFinder finder = new AccountFinder(session);

 

            Assert.AreEqual(acct, finder.FindById(id));                   

 

            tx.Rollback();

        }

    }

}

 

[Test]

public void CanQueryForAccountByCustomerPhoneWithRepository()

{

    using (ISession session = SessionSource.Current.GetSession())

    {

        using (ITransaction tx = session.BeginTransaction())

        {

            Account acct = new Account();

            Customer customer = new Customer("Some", "Dude");

            customer.HomePhone = "212-224-3456";

            acct.AddMember(customer);

 

            session.SaveOrUpdate(acct);

            session.Flush();

 

            int id = acct.Id;

 

            AccountFinder finder = new AccountFinder(session);

            IList<Account> matches = finder.FindByCustomerPhone(customer.HomePhone);

            Assert.IsTrue(matches.Contains(acct));                   

 

            tx.Rollback();

        }

    }

}

 

And they are both passing!  We’ve come to a good stopping point here.  I’d like to wrap up our feature list next time, then start on a basic UI.

 

I’ve gotten a lot of positive feedback so far with this series, and I always welcome more!  If you haven’t downloaded the code to follow along I encourage you to do so.

 

Here is the latest source: Videocracy - Part 8

#1 Carlo Bertini avatar
Carlo Bertini
2.14.2007
9:00 AM

Good tutorial, i hope to view next lession :P:P:PCarlo