Designing a Generic Database Layer

Some people like to automatically generate their database tier. Personally, I had enough bad experiences with ORM tools that I try to avoid them. After all, data tier code can be knocked out pretty quickly and it's always nice to know you didn't take any shortcuts. It's been a while since I've seen data tiers discussed, so I figured I'd pass along a modern approach that I have found works quite well and results in really clean data tiers.

Very early in your programming days, you learn that when separating an application into logical tiers, it is important that lower tiers do not couple themselves with higher tiers. As coupling between the tiers is reduced, your code gains flexibility. The entire data tier should be able stand on its own. If later you need to completely revamp your domain model or business layer, the data tier should be able to remain untouched.

First, consider a simple create operation inside a database class.

One option is to simply pass a data row to your database class and let it handle the update. There are a few problems with this approach. First, DataSets aren't exactly a high performance solution and have a lot of overhead. Additionally, a simple DataRow doesn't have a defined set of columns. As a result, you can't use refactoring operations should the column name change, nor can you use the compiler to check argument types or counts.

Another option is to pass an entity object directly to your database class. This is slightly better, as it ensures type safety and supports IDE features such as refactoring and auto-complete. However, this approach has a few flaws of its own.  You can't tell by looking at the method which properties are required to create a customer.  You can't tell if the data tier assigns the customer an ID, or if the ID must the ID be set before you pass the customer in. Additionally, now your data tier is being coded with explicit knowledge about the layout of your domain model. Since the domain model lives above the data tier, this should not happen.

A third approach is to have a method which takes all the parameters required to create a record in the database and return information about the results of the database call, such as an ID from an identity column. By going this route, you keep the coupling between the two tiers low, support compile time checking, and explicitly encode information about inputs and outputs of your method in its signature

Bad
public class CustomerDB
{
  public void CreateCustomer(DataRow row) { … }
}
Better
public class CustomerDB
{
  public void CreateCustomer(Customer c) { … }
}
Best
public class CustomerDB
{
  public int CreateCustomer(string firstName, string lastName, string company, string phoneNumber) { … }
}

Create, update, and delete are easy to handle in this fashion. But how do we handle query operations?

Again, one option is to work with DataSets or DataTables. While this gets the job done, you again incur a needless performance penalty by loading things into temporary tables before the data is loaded into entity objects. Another option is to pass back entity objects from your data tier. Again, however, this couples your data tier to your object model and is bad for the same reasons as it was bad in your create and update methods.

An alternate approach, which utilizes delegates and generics, eliminates the coupling between the tiers as well as the performance problems associated with datasets. Because this approach is new, I have included more complete code, illustrating how the different layers can work together.

Bad

public class CustomerDB
{
  public Customer GetCustomer(int id) { … }
  public Collection<Customer> GetCustomers(string company) { … }
}


Better
public class CustomerDB
{
  public DataRow GetCustomer(int id) { … }
  public DataTable GetCustomers(string company) { … }
}

public class Customer
{
  public static Customer CreateFrom(DataRow row)
  {
    ...
  }
}

Best
public delegate T CreateEntity<T>(IDataReader reader);public delegate void AddToList<T>(T item); public class CustomerDB<TCustomer>
{
  public TCustomer GetCustomer(int id, CreateEntity<TCustomer> create)  
  {
    using(SqlDataReader reader = ExecuteStoredProc(“GetCustomer”, id))
    {
      if(reader.Read()) return create(reader);
      else return default(TCustomer)
    }
  }
  public void GetCustomers(string company, CreateEntity<TCustomer> create, AddToList< TCustomer> addToList)
  {
    using(SqlDataReader reader = ExecuteStoredProc(“GetCustomersByCompany”, company);
    {
      while(reader.Read())
      {
        addToList(create(reader));
      }
    }
  }
}
 public class Customer
{
  public static Customer CreateFrom(IDataReader reader)
  {
   
  }
}

public class CustomerManager
{
  CustomerDB<Customer> _db = new CustomerDB<Customer>();
  public Customer GetCustomer(int id)
  {
    return _db.GetCustomer(id, Customer.CreateFrom);
  }
  public Collection<Customer> GetCustomers(string company)
  {
    Collection<Customer> customers = new Collection<Customer>();
    _db.GetCustomers(company, Customer.CreateFrom, customers.Add);
  }
}
 
Published Thursday, January 10, 2008 7:52 PM by Jesse Ezell

Comments

# re: Designing a Generic Database Layer

Friday, January 11, 2008 2:02 AM by foobar

I'd throttle you and kick your butt to the curb if you ever worked for me and made a class called "CustomerManager".  Could you make a more generic name for a class?

# re: Designing a Generic Database Layer

Friday, January 11, 2008 2:16 AM by Jesse Ezell

Regardless of your opinion on the issue, this naming pattern is pretty commonly used and accepted.

Just ask Microsoft:

msdn2.microsoft.com/.../microsoft.commerceserver.marketing.customermanager.aspx

or BEA:

edocs.beasys.co.jp/.../CustomerManager.html

# re: Designing a Generic Database Layer

Friday, January 11, 2008 3:35 AM by Ramon Smits

You really should take a look at projects like NHibernate because I see a couple of bad practices in your examples.

A. Using a fixed result stored procedure to initialize a custom type?

B. Your "CreateCustomer" method is not flexible. What about optional fields? This could be a constructor for the Customer class.

C. Returning a default Customer instance when NO record is found in the database? How to check if data is returned then?

# re: Designing a Generic Database Layer

Friday, January 11, 2008 5:22 AM by JV

An ORM is much more then just a database layer imao. Besides that I totally agree with Ramon. The DAl you have created isn't very flexible. It will actually require more work then when you would be using an ORM. And let's be fair, with LINQ To SQL and the upcoming Entity Framework ORM in .NET was never this easy...

# re: Designing a Generic Database Layer

Friday, January 11, 2008 7:11 AM by Jesse Ezell

Ramon,

A. Maybe you could clarify what you are trying to say here and state what is bad, instead of asking a question in the form of a sentence fragment.

B. You can easily provide multiple overloads for a method, one taking only required parameters and one with required and optional parameters. This is just a simple sample.

C. Your question tells me you haven't aren't familiar with generics. default(T) used with a reference type returns null, so you would check for empty as you normally would. (http://msdn2.microsoft.com/en-us/library/xwth0h0d(VS.80).aspx)

# re: Designing a Generic Database Layer

Friday, January 11, 2008 7:26 AM by Jesse Ezell

JV,

I've seen far too many bad design decisions made because something is theoretically "less work." That is a really hazy term. Less code does not always equal less work. I've used ORM tools and I've used the approach described here, and my experience in taking projects through their full life cycle tells a different story.

However, this post was not to discuss whether or not to use ORM. If you've had success with ORM on the type of projects you are involved with, more power to you. There are situations where ORM makes sense.
 

# re: Designing a Generic Database Layer

Sunday, January 13, 2008 5:23 PM by Mike Gale

What fascinating responses.  (A response often tells you more about the person who wrote it than about the article they comment on!!)

I'd appreciate a follow up on the impact of this approach (maybe a couple of ORM's thrown in too) on real projects.

(It would be great to see issues like code readability, time to get back up to speed (after a break), making changes, LOC and time taken, addressed.  But I know, it's nearly always impossible.)

Leave a Comment

(required) 
(required) 
(optional)
(required)