May 12, 2007 – 5:29pm Left Joins with multiple conditions using Propel Criteria

Symfony by default uses Propel for its ORM. Although Propel supports supports custom SQL I have learned how to take advantage of the built-in Criteria class to help construct queries. As described in the Propel trac,

…its database neutrality and logical simplicity make it a good choice for expressing many common queries; however, for a very complex query, it may prove more effective (and less painful) to simply use a custom SQL query to hydrate your Propel objects. (propel trac)

In building a complex search system for a client, I benefited greatly from being able to add and remove columns and conditions to a criteria object on the fly without having to worry about the actual SQL syntax. However, eventually I hit a wall when I needed to add a left join using multiple columns. Adding left joins to a criteria object is typically done like this:

$c = new Criteria();
$c->addJoin(TABLE_A::ID, TABLE_B::ID, Criteria::LEFT_JOIN);
/* equivalent to: "...left join TABLE_B ON (TABLE_A.ID=TABLE_B.ID)" */

However, this syntax does not support a join with multiple conditions, so you can’t produce something like this:

...left join TABLE_B ON (TABLE_A.ID=TABLE_B.ID AND TABLE_B.VALUE > 100)

In playing around quite a bit and examining the source code, I realized you could hack the addJoin method call and trick it into adding the extra column for you. The addJoin method actually is creating a new Join object that has a left and right column definition as defined in the method call. These are used to build the join condition:

$condition = $join->getLeftColumn() . '=' . $join->getRightColumn();

(see the source)

So, if you add the extra columns to the RightColumn like so:

$c->addJoin(TABLE_A::ID, TABLE_B::ID.' AND '.TABLE_B::VALUE.' > 0', Criteria::LEFT_JOIN);

the generated SQL will yield something like this:

...LEFT JOIN TABLE_B ON (TABLE_A.ID = TABLE_B.ID AND TABLE_B.VALUE > 0)

Although it’s a hack and it may not work under all conditions, so far it has been a suitable workaround.

Posted in  Web Development   |     |  delicious  Digg

5 Responses to “Left Joins with multiple conditions using Propel Criteria”

  1. This is a good trick, thanks for sharing it.

    The sad thing is, it is completely not secure if the second condition of the JOIN relies on a request parameter (or any other tainted value). In your example, it is 0, so this is not a problem.

    To properly escape a value in the second condition of a join, you need much more code (about 30 lines) and that’s where Propel really gets heavy…

    By François Zaninotto on Jul 14, 2007

  2. This is a good point. In these cases, perhaps it would be best to just write the query out rather than use Criteria objects. The escaping methods found in the “PreparedStatement” classes of the necessary database driver are all protected, so there is really no way to access them from outside those classes. Bummer. The MySQL driver uses the following escape function, so fortunately so it wouldn’t be too hard to run your extra value in your join statement through a similar process. This of course would break if you ever took your app to another DB system.

    class MySQLPreparedStatement extends PreparedStatementCommon implements PreparedStatement {        
    
        /**
         * Quotes string using native mysql function (mysql_real_escape_string()).
         * @param string $str
         * @return string
         */
        protected function escape($str)
        {
            return mysql_real_escape_string($str, $this->conn->getResource());
        }    
    
    }
    

    By smeves on Jul 22, 2007

  3. Thank you, I was having the exact same problem.

    I get curious when I run into issues like this… it’s not like we’re the first two people to ever need to use this kind of join. Why didn’t the Propel developers build this functionality in already? Are they planning to?

    By Josh on Aug 8, 2007

  4. Love your site. I’ve been using this particular method in several places. I actually check my request parameters to ensure they’re exactly what i’m looking for.
    i.e. I may only pass integers so I make sure by putting something like:
    $foo = (int)$this->getRequestParameter(’foo’);

    Also, I have found that if you do this and use:
    $c->setIgnoreCase(true);

    A column map error occurs. Nothing major as long as you know to watch for it. Took me a while to track down what the issue was.

    By ernie on May 12, 2008

  5. woops.. hehe.. in reference to previous comment, “if you do this” means if you do your trick to add multiple conditions.

    By ernie on May 12, 2008

Post a Comment

*
To prove you're a person (not a spam script), type the security word shown in the picture. Click on the picture to hear an audio file of the word.
Click to hear an audio file of the anti-spam word