Thursday, June 10, 2010

Practical MongoDB Part 2: NoRMalized Data Access

In Part 1 of this series I demonstrated setting up MongoDB to run as a Windows service. In this segment, I'll show you how I setup my data access layers using NoRM.

The Glue

For starts I created two modules: an abstract class for my data access objects, and an interface for my POCO's.
public abstract class DaoBase<T>
    where T : ICollectable
{
}

public interface ICollectable
{
    ObjectId Id { get; }
}
(ObjectId is NoRM's wrapper type for the Mongo generated id.)

Now we'll add the NoRM integration. You'll need to download the latest source from github and compile it, but I'm sure you can figure that out.
public abstract class DaoBase<T>
    where T : ICollectable
{
    public DaoBase()
    {
        var connectionString = ConfigurationManager.ConnectionStrings["default"].ConnectionString;
        Mongo = Mongo.Create(connectionString);
    }

    private Mongo Mongo { get; set; }

    protected MongoCollection<T> Collection
    {
        get
        {
            return Mongo.GetCollection<T>();
        }
    }

    protected IQueryable<T> Query
    {
        get
        {
            return Collection.AsQueryable();
        }
    }
}
(Connection strings are covered in detail here. On my dev box I use mongodb://localhost/databasename. Mongo will create the database on the first insert.)

Finally, we don't want to leave the Mongo object just hanging around. The Mongo instance houses the connection so by disposing of it we can return it to the connection pool.
public abstract class DaoBase<T> : IDisposable
    where T : ICollectable
{
    ...
    public void Dispose()
    {
        Mongo.Dispose();
    }
}
So much for the theory. Let's try using it.

A Basic Domain Object

We don't need to do a thing in Mongo to start storing our objects. We can simply define some classes and start inserting them. Mongo will create the appropriate collections as needed.

Here's a basic domain class I would like to store
public class User : ICollectable
{
    public ObjectId Id { get; internal set; }
    public string Email { get; internal set; }
    public string Password { get; internal set; }
    ...
}
Let's write a test for inserting and retrieving a User from Mongo.
public class UserDaoFacts
{
    public UserDaoFacts()
    {
        // At the beginning of each test, drop the User collection
        // strict=false tells NoRM not to error if the collection doesn't exist
        using (var mongo = Mongo.Create(TestHelper.ConnectionString, "strict=false"))
            // NoRM uses the class name by default. I'll show you how to override this in Part 3
            mongo.Database.DropCollection(typeof(User).Name); 
    }

    [Fact]
    public void Can_Insert_And_Retrieve()
    {
        using (var dao = new UserDao())
        {
            User user = new User();
            user.Email = "email";
            user.Password = "password";
            dao.Insert(user);

            User found = dao.Get(user.Email);

            Assert.NotNull(user.Id);
            Assert.Equal(user.Email, found.Email);
        }
    }
}
For which we create a concrete implementation of DaoBase.
public class UserDao : DaoBase<User>
{
    internal void Insert(User user)
    {
        Collection.Insert(user);
    }

    public User Get(string email)
    {
        return Query.FirstOrDefault(u => u.Email == email);
    }
}
This is enough to make our test successfully pass.


I'm going to move the Insert method to DaoBase and add some other common methods. Hopefully you can figure out how to write some tests for these methods.
public abstract class DaoBase<T> : IDisposable
    where T : ICollectable
{
    ...
    public IQueryable<T> GetAll()
    {
        return Query;
    }

    public T Get(ObjectId id)
    {
        // Here's where ICollectable comes in handy!
        return Query.SingleOrDefault(t => t.Id == id);
    }

    internal void Update(T obj)
    {
        // Mongo and NoRM offer partial updates, but this is sufficient for our immediate purposes
        Collection.Update(new { Id = obj.Id }, obj, false, false);
    }

    internal void Insert(T obj)
    {
        Collection.Insert(obj);
    }

    internal void Insert(IEnumerable<T> objs)
    {
        Collection.Insert(objs);
    }
    ...
}
There you have it! A fully functional User Dao! In Part 3 I'll discuss embedded documents, indexing, and configuration options.

2 comments:

nZeus said...

This is not an abstraction.
Imagine, that you want to use EF+MSSQL as your data-access layer.
You have to make modification in your BLL, too

JP said...

nZeus,

I'm not sure I see your point. This article refers to basic DAO design for MongoDB. Where does EF+MSSQL even come in?