Browse Source

Merge pull request #4017 from cakephp/3.0-missing-db-stuff

3.0 - better join methods
José Lorenzo Rodríguez 11 years ago
parent
commit
48ca3dea6a
2 changed files with 261 additions and 18 deletions
  1. 131 3
      src/Database/Query.php
  2. 130 15
      tests/TestCase/Database/QueryTest.php

+ 131 - 3
src/Database/Query.php

@@ -485,7 +485,12 @@ class Query implements ExpressionInterface, IteratorAggregate {
 			if (!is_array($t)) {
 				$t = ['table' => $t, 'conditions' => $this->newExpr()];
 			}
-			if (!($t['conditions']) instanceof ExpressionInterface) {
+
+			if (is_callable($t['conditions'])) {
+				$t['conditions'] = $t['conditions']($this->newExpr(), $this);
+			}
+
+			if (!($t['conditions'] instanceof ExpressionInterface)) {
 				$t['conditions'] = $this->newExpr()->add($t['conditions'], $types);
 			}
 			$alias = is_string($alias) ? $alias : null;
@@ -503,6 +508,113 @@ class Query implements ExpressionInterface, IteratorAggregate {
 	}
 
 /**
+ * Adds a single LEFT JOIN clause to the query.
+ *
+ * {{{
+ * // LEFT JOIN authors ON posts.author_id' = authors.id
+ * $query->leftJoin('authors', ['posts.author_id' = authors.id']);
+ * }}}
+ *
+ * You can pass an array in the first parameter if you need to alias
+ * the table for the join:
+ *
+ * {{{
+ * // LEFT JOIN authors a ON posts.author_id' = a.id
+ * $query->leftJoin(['a' => 'authors'], ['posts.author_id' = 'a.id']);
+ * }}}
+ *
+ * @param string|array $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+	public function leftJoin($table, $conditions = [], $types = []) {
+		return $this->join($this->_makeJoin($table, $conditions, 'LEFT'), $types);
+	}
+
+/**
+ * Adds a single RIGHT JOIN clause to the query.
+ *
+ * {{{
+ * // RIGHT JOIN authors ON posts.author_id' = authors.id
+ * $query->rightJoin('authors', ['posts.author_id' = authors.id']);
+ * }}}
+ *
+ * You can pass an array in the first parameter if you need to alias
+ * the table for the join:
+ *
+ * {{{
+ * // RIGHT JOIN authors a ON posts.author_id' = a.id
+ * $query->righJoin(['a' => 'authors'], ['posts.author_id' = 'a.id']);
+ * }}}
+ *
+ * @param string|array $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+	public function rightJoin($table, $conditions = [], $types = []) {
+		return $this->join($this->_makeJoin($table, $conditions, 'RIGHT'), $types);
+	}
+
+/**
+ * Adds a single INNER JOIN clause to the query.
+ *
+ * {{{
+ * // INNER JOIN authors ON posts.author_id' = authors.id
+ * $query->innerJoin('authors', ['posts.author_id' = authors.id']);
+ * }}}
+ *
+ * You can pass an array in the first parameter if you need to alias
+ * the table for the join:
+ *
+ * {{{
+ * // INNER JOIN authors a ON posts.author_id' = a.id
+ * $query->innerJoin(['a' => 'authors'], ['posts.author_id' = 'a.id']);
+ * }}}
+ *
+ * @param string|array $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param array $types a list of types associated to the conditions used for converting
+ * values to the corresponding database representation.
+ * @return $this
+ */
+	public function innerJoin($table, $conditions = [], $types = []) {
+		return $this->join($this->_makeJoin($table, $conditions, 'INNER'), $types);
+	}
+
+/**
+ * Returns an array that can be passed to the join method describing a single join clause
+ *
+ * @param string|array $table The table to join with
+ * @param string|array|\Cake\Database\ExpressionInterface $conditions The conditions
+ * to use for joining.
+ * @param string $type the join type to use
+ * @return array
+ */
+	protected function _makeJoin($table, $conditions, $type) {
+		$alias = $table;
+
+		if (is_array($table)) {
+			$alias = key($table);
+			$table = current($table);
+		}
+
+		return [
+			$alias => [
+				'table' => $table,
+				'conditions' => $conditions,
+				'type' => $type
+			]
+		];
+	}
+
+/**
  * Adds a condition or set of conditions to be used in the WHERE clause for this
  * query. Conditions can be expressed as an array of fields as keys with
  * comparison operators in it, the values for the array will be used for comparing
@@ -1212,10 +1324,26 @@ class Query implements ExpressionInterface, IteratorAggregate {
  * this function in subclasses to use a more specialized QueryExpression class
  * if required.
  *
+ * You can optionally pass a single raw SQL string or an array or expressions in
+ * any format accepted by \Cake\Database\QueryExpression:
+ *
+ * {{{
+ *
+ * $expression = $query->newExpression(); // Returns an empty expression object
+ * $expression = $query->newExpression('Table.column = Table2.column'); // Return a raw SQL expression
+ * }}}
+ *
+ * @param mixed $rawExpression A string, array or anything you want wrapped in a expression object
  * @return \Cake\Database\QueryExpression
  */
-	public function newExpr() {
-		return new QueryExpression([], $this->typeMap());
+	public function newExpr($rawExpression = null) {
+		$expression = new QueryExpression([], $this->typeMap());
+
+		if ($rawExpression !== null) {
+			$expression->add($rawExpression);
+		}
+
+		return $expression;
 	}
 
 /**

+ 130 - 15
tests/TestCase/Database/QueryTest.php

@@ -140,8 +140,8 @@ class QueryTest extends TestCase {
 		$this->assertEquals(array('foo' => 'Second Article Body', 'text' => 'Second Article Body', 'author_id' => 3), $result->fetch('assoc'));
 
 		$query = new Query($this->connection);
-		$exp = $query->newExpr()->add('1 + 1');
-		$comp = $query->newExpr()->add(['author_id +' => 2]);
+		$exp = $query->newExpr('1 + 1');
+		$comp = $query->newExpr(['author_id +' => 2]);
 		$result = $query->select(['text' => 'body', 'two' => $exp, 'three' => $comp])
 			->from('articles')->execute();
 		$this->assertEquals(array('text' => 'First Article Body', 'two' => 2, 'three' => 3), $result->fetch('assoc'));
@@ -220,7 +220,7 @@ class QueryTest extends TestCase {
 		$this->assertEquals(array('title' => 'Second Article', 'name' => 'larry'), $result->fetch('assoc'));
 
 		$query = new Query($this->connection);
-		$conditions = $query->newExpr()->add('author_id = a.id');
+		$conditions = $query->newExpr('author_id = a.id');
 		$result = $query
 			->select(['title', 'name'])
 			->from('articles')
@@ -258,7 +258,7 @@ class QueryTest extends TestCase {
 		$this->assertEquals(array('title' => 'Second Article', 'name' => 'nate'), $result->fetch('assoc'));
 
 		$query = new Query($this->connection);
-		$conditions = $query->newExpr()->add('author_id = a.id');
+		$conditions = $query->newExpr('author_id = a.id');
 		$result = $query
 			->select(['title', 'name'])
 			->from('articles')
@@ -280,6 +280,123 @@ class QueryTest extends TestCase {
 	}
 
 /**
+ * Tests the leftJoin method
+ *
+ * @return void
+ */
+	public function testSelectLeftJoin() {
+		$query = new Query($this->connection);
+		$time = new \DateTime('2007-03-18 10:45:23');
+		$types = ['created' => 'datetime'];
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->leftJoin(['c' => 'comments'], ['created <' => $time], $types)
+			->execute();
+		$this->assertEquals(array('title' => 'First Article', 'name' => null), $result->fetch('assoc'));
+
+		$query = new Query($this->connection);
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->leftJoin(['c' => 'comments'], ['created >' => $time], $types)
+			->execute();
+		$this->assertEquals(
+			['title' => 'First Article', 'name' => 'Second Comment for First Article'],
+			$result->fetch('assoc')
+		);
+	}
+
+/**
+ * Tests the innerJoin method
+ *
+ * @return void
+ */
+	public function testSelectInnerJoin() {
+		$query = new Query($this->connection);
+		$time = new \DateTime('2007-03-18 10:45:23');
+		$types = ['created' => 'datetime'];
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->innerJoin(['c' => 'comments'], ['created <' => $time], $types)
+			->execute();
+		$this->assertCount(0, $result->fetchAll());
+
+		$query = new Query($this->connection);
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->leftJoin(['c' => 'comments'], ['created >' => $time], $types)
+			->execute();
+		$this->assertEquals(
+			['title' => 'First Article', 'name' => 'Second Comment for First Article'],
+			$result->fetch('assoc')
+		);
+	}
+
+/**
+ * Tests the rightJoin method
+ *
+ * @return void
+ */
+	public function testSelectRightJoin() {
+		$this->skipIf(
+			$this->connection->driver() instanceof \Cake\Database\Driver\Sqlite,
+			'SQLite does not support RIGHT joins'
+		);
+		$query = new Query($this->connection);
+		$time = new \DateTime('2007-03-18 10:45:23');
+		$types = ['created' => 'datetime'];
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->rightJoin(['c' => 'comments'], ['created <' => $time], $types)
+			->execute();
+		$this->assertCount(6, $result);
+		$this->assertEquals(
+			['title' => null, 'name' => 'First Comment for First Article'],
+			$result->fetch('assoc')
+		);
+	}
+
+/**
+ * Tests that it is possible to pass a callable as conditions for a join
+ *
+ * @return void
+ */
+	public function testSelectJoinWithCallback() {
+		$query = new Query($this->connection);
+		$types = ['created' => 'datetime'];
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->innerJoin(['c' => 'comments'], function($exp, $q) use ($query, $types) {
+				$this->assertSame($q, $query);
+				$exp->add(['created <' => new \DateTime('2007-03-18 10:45:23')], $types);
+				return $exp;
+			})
+			->execute();
+		$this->assertCount(0, $result->fetchAll());
+
+		$query = new Query($this->connection);
+		$types = ['created' => 'datetime'];
+		$result = $query
+			->select(['title', 'name' => 'c.comment'])
+			->from('articles')
+			->innerJoin('comments', function($exp, $q) use ($query, $types) {
+				$this->assertSame($q, $query);
+				$exp->add(['created >' => new \DateTime('2007-03-18 10:45:23')], $types);
+				return $exp;
+			})
+			->execute();
+		$this->assertEquals(
+			['title' => 'First Article', 'name' => 'Second Comment for First Article'],
+			$result->fetch('assoc')
+		);
+	}
+
+/**
  * Tests it is possible to filter a query by using simple AND joined conditions
  *
  * @return void
@@ -921,7 +1038,7 @@ class QueryTest extends TestCase {
 			->where(function($exp, $q) {
 				return $exp->in(
 					'created',
-					$q->newExpr()->add("'2007-03-18 10:45:23'"),
+					$q->newExpr("'2007-03-18 10:45:23'"),
 					'datetime'
 				);
 			})
@@ -936,7 +1053,7 @@ class QueryTest extends TestCase {
 			->where(function($exp, $q) {
 				return $exp->notIn(
 					'created',
-					$q->newExpr()->add("'2007-03-18 10:45:23'"),
+					$q->newExpr("'2007-03-18 10:45:23'"),
 					'datetime'
 				);
 			})
@@ -1124,8 +1241,7 @@ class QueryTest extends TestCase {
 		$this->assertEquals(['id' => 2], $result->fetch('assoc'));
 		$this->assertEquals(['id' => 3], $result->fetch('assoc'));
 
-		$expression = $query->newExpr()
-			->add(['(id + :offset) % 2']);
+		$expression = $query->newExpr(['(id + :offset) % 2']);
 		$result = $query
 			->order([$expression, 'id' => 'desc'], true)
 			->bind(':offset', 1, null)
@@ -1862,8 +1978,7 @@ class QueryTest extends TestCase {
 	public function testUpdateWithExpression() {
 		$query = new Query($this->connection);
 
-		$expr = $query->newExpr();
-		$expr->add('title = author_id');
+		$expr = $query->newExpr('title = author_id');
 
 		$query->update('articles')
 			->set($expr)
@@ -2334,7 +2449,7 @@ class QueryTest extends TestCase {
 		$this->assertQuotedQuery('SELECT <1 \+ 1> AS <foo>$', $sql);
 
 		$query = new Query($this->connection);
-		$sql = $query->select(['foo' => $query->newExpr()->add('1 + 1')])->sql();
+		$sql = $query->select(['foo' => $query->newExpr('1 + 1')])->sql();
 		$this->assertQuotedQuery('SELECT \(1 \+ 1\) AS <foo>$', $sql);
 
 		$query = new Query($this->connection);
@@ -2358,7 +2473,7 @@ class QueryTest extends TestCase {
 		$this->assertQuotedQuery('FROM <something> AS <foo>$', $sql);
 
 		$query = new Query($this->connection);
-		$sql = $query->select('*')->from(['foo' => $query->newExpr()->add('bar')])->sql();
+		$sql = $query->select('*')->from(['foo' => $query->newExpr('bar')])->sql();
 		$this->assertQuotedQuery('FROM \(bar\) AS <foo>$', $sql);
 	}
 
@@ -2390,7 +2505,7 @@ class QueryTest extends TestCase {
 		$this->assertQuotedQuery('JOIN <something> <foo>', $sql);
 
 		$query = new Query($this->connection);
-		$sql = $query->select('*')->join(['foo' => $query->newExpr()->add('bar')])->sql();
+		$sql = $query->select('*')->join(['foo' => $query->newExpr('bar')])->sql();
 		$this->assertQuotedQuery('JOIN \(bar\) <foo>', $sql);
 	}
 
@@ -2406,7 +2521,7 @@ class QueryTest extends TestCase {
 		$this->assertQuotedQuery('GROUP BY <something>', $sql);
 
 		$query = new Query($this->connection);
-		$sql = $query->select('*')->group([$query->newExpr()->add('bar')])->sql();
+		$sql = $query->select('*')->group([$query->newExpr('bar')])->sql();
 		$this->assertQuotedQuery('GROUP BY \(bar\)', $sql);
 
 		$query = new Query($this->connection);
@@ -2453,7 +2568,7 @@ class QueryTest extends TestCase {
 		$this->assertQuotedQuery('INSERT INTO <foo> \(<bar>, <baz>\)', $sql);
 
 		$query = new Query($this->connection);
-		$sql = $query->insert([$query->newExpr()->add('bar')])
+		$sql = $query->insert([$query->newExpr('bar')])
 			->into('foo')
 			->where(['something' => 'value'])
 			->sql();