Thursday, September 2, 2010

Separating Model from Data Access Layer

At last I found some time to provide you with an example of design I wrote about in previous posts. For the sake of easy understanding I'll keep the example simple but of course in reality it'd be much more complex. This example is based on a real implementation.

So lets say we have two tables: user and user_detail looking like that:
User:
  columns:
    id:
    first_name: 
    last_name:
    detail_id:
  relations:
    UserDetail: {local: detail_id, foreign: id}

UserDetail:
  columns:
    id:
    email:
    phone:

After running doctrine:build-model we will receive 4 classes: User, UserTable, UserDetail and UserDetailTable. Each of them represent a physical database table or a single record in them. So those classes are not models - they belong to DAL and don't represent business processeses or objects per se. In regular symfony approach, accessing user data in actions would look like this:

public function executeGetUserEmail(sfWebRequest $request)
{
  $id = $request->getParameter('id');
  $user = Doctrine::getTable('User')->findOneById($id);
  $email = $user->getUserDetail()->getEmail();
  .
  .
  .
}

Of course this action is just a hypothetical one so please don't look at he sense of it. So lets see what we've just done. We have put doctrine queries in action, exposed the table structure and closed our way to changing that structure or switching ORMs. Ooops, we've broken the MVC architecture! Lets look at the Wikipedia definition of Model:

The model is used to manage information and notify observers when that information changes. The model is the domain-specific representation of the data upon which the application operates. Domain logic adds meaning to raw data (for example, calculating whether today is the user's birthday, or the totals, taxes, and shipping charges for shopping cart items). When a model changes its state, it notifies its associated views so they can be refreshed.

Many applications use a persistent storage mechanism such as a database to store data. MVC does not specifically mention the data access layer because it is understood to be underneath or encapsulated by the model. Models are not data access objects; however, in very simple apps that have little domain logic there is no real distinction to be made. Active Record is an accepted design pattern which merges domain logic and data access code - a model which knows how to persist itself.

As you can see, it confirms what I'd written before. So, how to fix our example? Well, first we have to define what Model do we really need. To do this we have to reach for business abstraction layer of our system. We must know, what kind of business entity or process our Model is to represent. In our example the answer is easy - it is to represent a user. But a user as a whole, not as a database table. Our model will work on both tables so outside the Model system won't know anything about the database structure. Lets call our Model UserModel to distinguish it from the ORM class. Below is an example how our Model could look like.

class UserModel {
  protected $user, $detail;

  public function __construct($id=null)
  {
    if($id == null)
    {
      $this->user = new User();
      $this->detail = new UserDetail();
    }
    else
    {
      $this->user = Doctrine::getTable('User')->findOneById($id);
      $this->detail = $this->user->getUserDetail();
    }
  }

  public function getFirstName()
  {
    return $this->user->getFirstName();
  }

  public function getEmail($secure=false)
  {
    if (!$secure)
      return $this->detail->getEmail();
    else 
      return str_replace('@','[at]',$this->detail->getEmail());
  }
}

Lets see how we can use it in the action.

public function executeGetUserEmail(sfWebRequest $request)
{
  $id = $request->getParameter('id');
  $secure = sfConfig::get('app_email_secure');
  $user = new UserModel($id);
  $email = $user->getEmail($secure);
  .
  .
  .
}

We've just fixed all the flaws mentioned earlier. Dont you think it looks cleaner? Now no matter if you changed the database structure or switched ORMs - you don't have to touch your controller. Switching ORMs would require only a slight change in the Model constructor and it won't affect the rest of the code. We have just separated the Model from DAL, gained lots of flexibility and simplified the controller. I hope you get the idea.

Any comments will be appreciated.

7 comments:

  1. SO does this mean that every time some data from the User model is required (e.g. first_name) it will automatically perform a second SQL query for the userDetail record?

    This doesn't seem very efficient and would put additional unnecessary load onto the db.

    Would you not be better off performing a check in the getEmail method if the UserDetail data has already been loaded and if not then running "$this->detail = $this->user->getUserDetail();"

    ReplyDelete
  2. Yes, you could do that. On the other side - performing that check in each method operating on UserDetail data would have a negative influence on efficency as well and would make the code longer. I just assumed that details will be needed most of the time that's why i wrote it this way. If it was just additional data required occasionally then I would have used your approach.

    ReplyDelete
  3. You could also write a doctrine query on your UserTable class that joined the detail table (something like findOneByIdWithDetail and you'd only run one query and have both user and detail populated.

    ReplyDelete
  4. Do you also use some magic __call to try to load from the child objects or do you recreate every getFirstName, getLastName, getXYZColumn in the UserModel class?

    ReplyDelete
  5. Yes, I use __call. It's really handy :)

    Another cool thing one can do is to make the UserModel class abstract and then create child models for different types of users. That's the beauty of this design - it's damn flexible.

    ReplyDelete
  6. I think that last sentence of the Wikipedia quote justifies symfony's decision to refer to the Doctrine classes as the model (discussed in your previous rant):

    "Active Record is an accepted design pattern which merges domain logic and data access code - a model which knows how to persist itself."

    It's a purposeful decision/pattern done with a goal in mind. i.e. that the domain and DAL are mixed.

    Having said that, I find the whole ActiveRecord pattern pretty strange next to the "Data Mapper" style of ORM that Doctrine 2 is. Very much looking forward to when that will be the norm.

    Thanks for the posts; they've been great. It's really useful to see how other people tackle these sorts of problems.

    ReplyDelete
  7. Thanks for the comment :) Sorry for the long response time - I had no internet connection at home for couple of weeks.

    As to wiki quote, you're right. But that last sentence is a refference to the previous one:

    "Models are not data access objects; however, in very simple apps that have little domain logic there is no real distinction to be made."

    So it is acceptable but only when your business logic isn't too complex.

    I've covered this subject here: http://devfactor.blogspot.com/2010/08/symfony-and-mvc-follow-up.html

    ReplyDelete