Optimize your Doctrine Workflow with Specialized Queries
Par NiKo le mardi 15 septembre 2009, 19:14 - Dev
- Lien permanent -
15 commentaires -
Tags :
I’m currently working on a big Symfony project, with a lot of Doctrine models and complex queries to write. I found a way to organize all of them in an object-oriented and cleaner way than using the traditionnal addNamedQuery() and createNamedQuery() methods workflow[1].
The idea is to create dedicated query classes for a given model ; this way, you can provide useful methods to build the business-related parts of your query.
As usual, the theory is more understandable with a concrete example. Let’s consider this simple Doctrine model[2] :
Disclaimer: The provided examples have been written in a hurry, so mistakes might have been not detected by my attentive proof-reading 
BlogAuthor:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
name:
type: string(255)
relations:
Post:
type: one
class: BlogPost
local: id
foreign: author_id
BlogPost:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
author_id:
type: integer(4)
notnull: true
title:
type: string(255)
content:
type: string(65535)
relations:
Author:
type: one
class: BlogAuthor
local: author_id
foreign: id
Comments:
type: many
class: BlogComment
local: id
foreign: post_id
BlogComment:
columns:
id:
type: integer(4)
primary: true
autoincrement: true
post_id:
type: integer(4)
notnull: true
author:
type: string(255)
content:
type: string(5000)
relations:
Post:
type: one
class: BlogPost
local: post_id
foreign: id
Now let’s imagine a Query class dedicated to query the BlogPost table:
<?php class BlogPostQuery extends Doctrine_Query { static public function create($conn = null, $class = null) { return parent::create($conn, 'BlogPostQuery') ->from('BlogPost p'); } public function addPosts($fields = 'p.*') { return $this->addSelect('p.*'); } public function addComments($fields = 'c.*') { return $this ->addSelect($fields) ->leftJoin('p.Comments c') ->addGroupBy('c.id'); } public function addAuthors($fields = 'a.*') { return $this ->addSelect($fields) ->leftJoin('p.Author a') ->addGroupBy('a.id'); } public function addCommentsCount($alias = 'nb_comments') { return $this ->addSelect(sprintf('COUNT(c.id) as %s', $alias)) ->addGroupBy('c.id'); } public function filterByAuthorName($authorName) { return $this ->andWhere('a.name = ?', $authorName); } }
So how can we use this query object? Here are some sample uses:
// Retrieve all posts $posts = BlogPostQuery::create() ->addPosts() ->fetchArray(); // Retrieve all posts with comments $posts = BlogPostQuery::create() ->addPosts() ->addComments() ->fetchArray(); // Retrieve all posts with comments and their count per post $posts = BlogPostQuery::create() ->addPosts() ->addComments() ->addCommentsCount('yataa') ->fetchArray(); // Retrieve all post with chuck as its author and related comments $posts = BlogPostQuery::create() ->addAuthors() ->addPosts() ->addComments() ->filterByAuthorName('chuck') ->fetchArray(); // and so on...
Of course, this example of use is not really relevant as our model is really simple, but when you’re dealing with dozens of internationalized objects, it can help cleaning your model classes, controllers and improving the organization of your work.
Update and important precisions
Some people are having negative feedback regarding this technique, claiming it will encourage people using the custom query object directly in the controllers; that’s absolutely not the case as the queries are to be used only within the model layer, for example in the BlogPostTable class:
<?php class BlogPostTable extends Doctrine_Table { static public function getPostsWithCommentsByAuthor($authorName) { return BlogPostQuery::create() ->addPosts() ->addComments() ->filterByAuthorName($authorName) ->fetchArray() ; } }
And in a controller:
class blogActions extends sfActions { public function executeListByAuthor(sfWebRequest $request) { $this->posts = BlogPostTable::getPostsWithCommentsByAuthor($request->getParameter('author')); } }
Notes
[1] … or raw queries written directly within controllers, but you may know that this is really bad 
[2] I’m using Doctrine 1.2 beta (bundled with upcoming symfony 1.3) in the provided example.










15 commentaires (Ajouter un commentaire)
This is indeed a very nice solution. I suppose it must be quite complicated to manage conflicts or dependencies between the individual methods though. In your example, f.i., addCommentsCount() depends on addComments() being called before.
How do you handle these problems?
Hi Bernhard
Actually I didn't implement any dependency check as I don't really need it myself, and the provided is very loosy.
But it could be feasible by using flags, or by checking the result of
getQueryComponents()everytime. But the problem you raise still exists with DQL or with OO query building: what if I forget a component? I got an error, too. So my aim here is not to rewriteDoctrine_Query, just to provide a flexible interface in a dedicated layer to build queries. But it could be a very nice enhancement, I agreeWhy don't you use the generated "Table" classes ? For example, you could define this kind of function :
<code>public function addCommentsToQuery(Doctrine_Query $query) { ... }</code>
Ixla> Matter of taste, and it's not incompatible with my solution
but personnaly, my Table classes are already full of methods and I wanted to keep them light and concise, so I decided to move the query building stuff in a dedicated class.
Hello,
I think this solution suffers from duplicated alias issues : a method can add a leftJoin and another method can create the same leftJoin => #fail.
In the same idea of queries centralization you can use symfony filter or the swDoctrineDatagrid from the swToolboxPlugin. Both solution allows to create a central place to fetch doctrine object/array. The advantage is that you can optimize queries easily.
Thomas, the naive and oversimplified example I gave is not intended to be used as a plugin or a generic universal reusable solution, but just to be a simple solution to deport the query management into a dedicated class.
As I already told Bernhard, you could write your own component conflicts detection/handling everytime something is added or filtered, but I won't just take too much time to write it, actually because I don't personally need it
That's all, and sorry if my post is not clear enough regarding its goal.
That remembers me of what i used to do with Propel, kind of "business Criterias". Indeed the queries are for more readable.
@Thomas: haha, the famous doctrine datagrids ^^
Please forgive me, I'm English and the site seems to be in French.
I think it's genius, if you want to really check if some other method included the alias as a join, then check the getParams and array_search the 'join' element sub array.
I think NiKo intended this as an example of how you could extend your core to centralize the DQL, making it easier to maintain.
Funny, I have seen something very similar some time ago. What was it ?
Yes, I remember: http://trac.symfony-project.org/bro...
Named queries make your model classes a lot cleaner and much easier to deal with.
François>
Congratulation on big Symfony project. This is a neat solution, better than cramming all the methods in one model class
Thanks for sharing this.
In the method addPosts, '$fields' should be passed as parameter instead of p.*
i.e.
<code>
public function addPosts($fields = 'p.*')
{
return $this->addSelect($fields);
}
</code>
Ashok> You spotted it, nice
i am getting the following error when trying to call addPosts, addComments, addAuthors etc..
"Call to undefined method Doctrine_Query::addPosts()"
What can cause that? any idea? thanks.
maximus> check you're instantiating the query object using the
BlogPostQuery::create()method. Here, it works.