Blog: Propel's Criteria Gets Smarter

The Propel Team – 10 December 2009

I've always been frustrated with Propel's Criteria. Despite its powerful Object-Oriented API for querying a database, I find it quite dumb and very hard to learn. It used to be one of Propel's main drawbacks. But starting with Propel 1.5, you can count on the Criteria to be much smarter.

Criteria's Shortcomings

Let me be more specific. The Criteria object doesn't know about your model schema, and forces you to repeat both the members of a relation each time you write a join:

[code] $c = new Criteria(); $c->addJoin(BookPeer::AUTHOR_ID, AuthorPeer::ID); [/code]

With a Criteria, you can't write queries that make two joins on the same table. You can't easily write a custom SQL clause that will use proper binding.

Criteria also follows a syntax that you cannot guess, so you need the Criteria API documentation open at all times for months until you can figure out that kind of snippet:

[code] $c = new Criteria(); $c1 = $c->getNewCriterion(BookPeer::TITLE, '%Leo%', Criteria::LIKE); $c2 = $c->getNewCriterion(BookPeer::ISBN, '1234', Criteria::EQUAL); $c1->addOr($c2); $c->add($c1); $c->addAscendingOrderByColumn(BookPeer::CREATED_AT); [/code]

After giving several trainings for Propel, I keep on seeing developers struggle to write a simple query. Propel's Criteria is definitely one of its less intuitive features, and it's not the most powerful query API in the world either.

Criteria's Power

I already tried to address these shortcomings a year and a half ago, through a plugin for the Symfony framework called DbFinder. With DbFinder, it became a pleasure to develop model code based on Propel, and to teach how to use it.

DbFinder relied heavily on Criteria, but added a lot of custom code. Some of this code was required to do runtime introspection of the model struture. Propel 1.4 recently introduced the RelationMap classes, and the ability for a model to know all its relations at runtime. This was the first brick for a native implementation of DbFinder in Propel.

Building DbFinder abilities right into Propel Criteria was the next step, but the Propel implementation had to differ from DbFinder's one. There is one thing that I have understood in the past months, and it's the biggest strength of Propel: the fact that Propel can generate objects at buildtime is what makes it faster than all its competitors. So I used Propel's generator in conjunction with DbFinder's power to create the ModelCriteria class and the PropelQuery factory.

By initializing a Criteria object with a Model name, it is now possible to write queries in the following way:

[code] $books = PropelQuery::from('Book b')   ->join('b.Author a')   ->where('a.FirstName = ?', 'Leo')   ->orderBy('b.Title')   ->limit(10)   ->find(); [/code]

Behind this `PropelQuery`, it's the same old Criteria at work. That means that this new syntax is entirely backwards compatible. You can see it as a usability layer on top of Criteria, that doesn't break any existing application. The new syntax is already committed to the Propel 1.5 branch, fully unit tested, and documented in a brand new documentation chapter.

There Is One Way To Do It

You probably understood that this new syntax can completely replace the use of Peer classes in Propel. But for the time being, that means that there are two ways to write and execute a query with Propel. The old way, using Criteria and Peer classes, and the new way, using PropelQuery.

And it's a good usability guideline not to leave too much choice to avoid that users get lost.

The old way has to be there for backwards compatibility. It will remain the in the Propel core for as long as the Propel 1.x branch lives.

The new way will become the default way some time in the future - maybe in time for the 1.5 release. That will mean a large rewrite of the current documentation and behaviors, but it's not such a big task. More specifically, the main documentation will omit the Criteria and Peer way. But rest assured, the cookbook will still provide a chapter to learn how to run them.

Propel Is Not Becoming Doctrine

I know what most Propel old timers will think of this addition: this is Propel becoming Doctrine and giving away its soul to the new ORM golden boy. Well, in a way, I agree with that. This new feature takes some of the recipes that proved successful in Doctrine, and that Doctrine took from other projects beforehand. This is called Open-Source cross-pollinisation. Reusing the best ideas found elsewhere is not losing one's soul. What makes Propel's soul is its ability to query a database through an object-oriented API. This is still there, and better than ever.

Now the new Propel Query API has some significant differences with Doctrine's Query API. First of all, it is faster. Propel still relies on generated code to execute queries in roughly half the time Doctrine needs. Propel's queries don't need a Parser/Lexer implementation, because they only accept simple clauses, described in a standard way. By combining these simple clauses, you can write very complex queries. Also, you can extend Propel Query objects to write Named Queries, which are objects embedding a query for later reuse. This will be the true revolution in your development habits. Your model code will be more readable, more maintainable, and more robust than ever.

Propel's Future

One last word: This new feature is not particularily aimed at current Propel users. You guys already master the Criteria, you have Criteria all over your application code, and you will be able to use its seamlessly until the 1.x branch dies. If you want to ignore the new Propel Query features, then go on as before and don't worry about it - it won't interfere with your code.

But newcomers who have to choose an ORM nowadays have basically two choices, and I don't want them to ignore Propel for the wrong reasons. The poor usability of Propel's Criteria used to be a reason for not choosing Propel. It should no longer be the case.

So as of now, Propel is a very usable ORM. Make sure you spread the word.