1
0
mirror of synced 2025-01-18 06:21:40 +03:00

[PoC] Arbitrary join support

This commit is contained in:
Alexander 2012-06-09 15:50:34 +02:00 committed by Benjamin Eberlei
parent 27b4f58b66
commit e7dfa08756
6 changed files with 253 additions and 165 deletions

View File

@ -40,15 +40,13 @@ class Join extends Node
const JOIN_TYPE_INNER = 3; const JOIN_TYPE_INNER = 3;
public $joinType = self::JOIN_TYPE_INNER; public $joinType = self::JOIN_TYPE_INNER;
public $joinAssociationPathExpression = null; public $joinPathExpression = null;
public $aliasIdentificationVariable = null;
public $conditionalExpression = null; public $conditionalExpression = null;
public function __construct($joinType, $joinAssocPathExpr, $aliasIdentVar) public function __construct($joinType, $joinPathExpr)
{ {
$this->joinType = $joinType; $this->joinType = $joinType;
$this->joinAssociationPathExpression = $joinAssocPathExpr; $this->joinAssociationPathExpression = $joinPathExpr;
$this->aliasIdentificationVariable = $aliasIdentVar;
} }
public function dispatch($sqlWalker) public function dispatch($sqlWalker)

View File

@ -36,11 +36,13 @@ class JoinAssociationPathExpression extends Node
{ {
public $identificationVariable; public $identificationVariable;
public $associationField; public $associationField;
public $aliasIdentificationVariable = null;
public function __construct($identificationVariable, $associationField) public function __construct($identificationVariable, $associationField, $aliasIdentVar)
{ {
$this->identificationVariable = $identificationVariable; $this->identificationVariable = $identificationVariable;
$this->associationField = $associationField; $this->associationField = $associationField;
$this->aliasIdentificationVariable = $aliasIdentVar;
} }
public function dispatch($sqlWalker) public function dispatch($sqlWalker)

View File

@ -0,0 +1,47 @@
<?php
/*
* $Id$
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the LGPL. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace Doctrine\ORM\Query\AST;
/**
* JoinClassPathExpression ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
*
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
* @link www.doctrine-project.org
* @since 2.3
* @author Alexander <iam.asm89@gmail.com>
*/
class JoinClassPathExpression extends Node
{
public $abstractSchemaName;
public $aliasIdentificationVariable;
public function __construct($abstractSchemaName, $aliasIdentificationVar)
{
$this->abstractSchemaName = $abstractSchemaName;
$this->aliasIdentificationVariable = $aliasIdentificationVar;
}
public function dispatch($walker)
{
return $sqlWalker->walkJoinPathExpression($this);
}
}

View File

@ -905,11 +905,62 @@ class Parser
$qComp = $this->_queryComponents[$identVariable]; $qComp = $this->_queryComponents[$identVariable];
$class = $qComp['metadata']; $class = $qComp['metadata'];
if ( ! isset($class->associationMappings[$field])) { if ( ! $class->hasAssociation($field)) {
$this->semanticalError('Class ' . $class->name . ' has no association named ' . $field); $this->semanticalError('Class ' . $class->name . ' has no association named ' . $field);
} }
return new AST\JoinAssociationPathExpression($identVariable, $field); if ($this->_lexer->isNextToken(Lexer::T_AS)) {
$this->match(Lexer::T_AS);
}
$token = $this->_lexer->lookahead;
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
// Building queryComponent
$joinQueryComponent = array(
'metadata' => $this->_em->getClassMetadata($class->associationMappings[$field]['targetEntity']),
'parent' => $identVariable,
'relation' => $class->getAssociationMapping($field),
'map' => null,
'nestingLevel' => $this->_nestingLevel,
'token' => $this->_lexer->lookahead
);
$this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
return new AST\JoinAssociationPathExpression($identVariable, $field, $aliasIdentificationVariable);
}
/**
* JoinClassPathExpression ::= Class alias
*
* @return \Doctrine\ORM\Query\AST\JoinClassPathExpression
*/
public function JoinClassPathExpression()
{
$abstractSchemaName = $this->AbstractSchemaName();
if ($this->_lexer->isNextToken(Lexer::T_AS)) {
$this->match(Lexer::T_AS);
}
$token = $this->_lexer->lookahead;
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
$classMetadata = $this->_em->getClassMetadata($abstractSchemaName);
// Building queryComponent
$queryComponent = array(
'metadata' => $classMetadata,
'parent' => null,
'relation' => null,
'map' => null,
'nestingLevel' => $this->_nestingLevel,
'token' => $token
);
$this->_queryComponents[$aliasIdentificationVariable] = $queryComponent;
return new AST\JoinClassPathExpression($abstractSchemaName, $aliasIdentificationVariable);
} }
/** /**
@ -1570,41 +1621,19 @@ class Parser
$this->match(Lexer::T_JOIN); $this->match(Lexer::T_JOIN);
$joinPathExpression = $this->JoinAssociationPathExpression(); $next = $this->_lexer->glimpse();
if ($next['type'] === Lexer::T_DOT) {
$joinPathExpression = $this->JoinAssociationPathExpression();
} else {
$joinPathExpression = $this->JoinClassPathExpression();
if ($this->_lexer->isNextToken(Lexer::T_AS)) { if (!$this->_lexer->isNextToken(Lexer::T_WITH)) {
$this->match(Lexer::T_AS); $this->syntaxError('WITH');
}
} }
$token = $this->_lexer->lookahead;
$aliasIdentificationVariable = $this->AliasIdentificationVariable();
// Verify that the association exists.
$parentClass = $this->_queryComponents[$joinPathExpression->identificationVariable]['metadata'];
$assocField = $joinPathExpression->associationField;
if ( ! $parentClass->hasAssociation($assocField)) {
$this->semanticalError(
"Class " . $parentClass->name . " has no association named '$assocField'."
);
}
$targetClassName = $parentClass->associationMappings[$assocField]['targetEntity'];
// Building queryComponent
$joinQueryComponent = array(
'metadata' => $this->_em->getClassMetadata($targetClassName),
'parent' => $joinPathExpression->identificationVariable,
'relation' => $parentClass->getAssociationMapping($assocField),
'map' => null,
'nestingLevel' => $this->_nestingLevel,
'token' => $token
);
$this->_queryComponents[$aliasIdentificationVariable] = $joinQueryComponent;
// Create AST node // Create AST node
$join = new AST\Join($joinType, $joinPathExpression, $aliasIdentificationVariable); $join = new AST\Join($joinType, $joinPathExpression);
// Check for ad-hoc Join conditions // Check for ad-hoc Join conditions
if ($this->_lexer->isNextToken(Lexer::T_WITH)) { if ($this->_lexer->isNextToken(Lexer::T_WITH)) {

View File

@ -812,31 +812,138 @@ class SqlWalker implements TreeWalker
? ' LEFT JOIN ' ? ' LEFT JOIN '
: ' INNER JOIN '; : ' INNER JOIN ';
if ($joinVarDecl->indexBy) { $joinPathExpr = $join->joinAssociationPathExpression;
// For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. $joinedDqlAlias = $joinPathExpr->aliasIdentificationVariable;
$this->_rsm->addIndexBy(
$joinVarDecl->indexBy->simpleStateFieldPathExpression->identificationVariable, if ($joinPathExpr instanceof \Doctrine\ORM\Query\AST\JoinClassPathExpression) {
$joinVarDecl->indexBy->simpleStateFieldPathExpression->field $targetClass = $this->_queryComponents[$joinedDqlAlias]['metadata'];
);
$targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
$sql .= $targetClass->getQuotedTableName($this->_platform) . ' '
. $targetTableAlias;
$sql .= ' ON (' . $this->walkConditionalExpression($join->conditionalExpression) . ')';
} else {
$relation = $this->_queryComponents[$joinedDqlAlias]['relation'];
$targetClass = $this->_em->getClassMetadata($relation['targetEntity']);
$sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']);
$targetTableName = $targetClass->getQuotedTableName($this->_platform);
$targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
$sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinPathExpr->identificationVariable);
// Ensure we got the owning side, since it has all mapping info
$assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation;
if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) {
if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) {
throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
}
}
// This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
// be the owning side and previously we ensured that $assoc is always the owning side of the associations.
// The owning side is necessary at this point because only it contains the JoinColumn information.
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ';
$first = true;
foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($relation['isOwningSide']) {
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn;
} else {
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
}
}
} else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) {
// Join relation table
$joinTable = $assoc['joinTable'];
$joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias);
$sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON ';
$first = true;
if ($relation['isOwningSide']) {
foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
} else {
foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
// Join target table
$sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN ';
$sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ';
$first = true;
if ($relation['isOwningSide']) {
foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
} else {
foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
}
// Handle WITH clause
if (($condExpr = $join->conditionalExpression) !== null) {
// Phase 2 AST optimization: Skip processment of ConditionalExpression
// if only one ConditionalTerm is defined
$sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')';
}
} }
$joinAssocPathExpr = $join->joinAssociationPathExpression; // Apply the filters
$joinedDqlAlias = $join->aliasIdentificationVariable; if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) {
$sql .= ' AND ' . $filterExpr;
$relation = $this->_queryComponents[$joinedDqlAlias]['relation'];
$targetClass = $this->_em->getClassMetadata($relation['targetEntity']);
$sourceClass = $this->_em->getClassMetadata($relation['sourceEntity']);
$targetTableName = $targetClass->getQuotedTableName($this->_platform);
$targetTableAlias = $this->getSQLTableAlias($targetClass->getTableName(), $joinedDqlAlias);
$sourceTableAlias = $this->getSQLTableAlias($sourceClass->getTableName(), $joinAssocPathExpr->identificationVariable);
// Ensure we got the owning side, since it has all mapping info
$assoc = ( ! $relation['isOwningSide']) ? $targetClass->associationMappings[$relation['mappedBy']] : $relation;
if ($this->_query->getHint(Query::HINT_INTERNAL_ITERATION) == true && (!$this->_query->getHint(self::HINT_DISTINCT) || isset($this->_selectedClasses[$joinedDqlAlias]))) {
if ($relation['type'] == ClassMetadata::ONE_TO_MANY || $relation['type'] == ClassMetadata::MANY_TO_MANY) {
throw QueryException::iterateWithFetchJoinNotAllowed($assoc);
}
} }
if ($joinVarDecl->indexBy) { if ($joinVarDecl->indexBy) {
@ -849,109 +956,6 @@ class SqlWalker implements TreeWalker
$this->_rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); $this->_rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']);
} }
// This condition is not checking ClassMetadata::MANY_TO_ONE, because by definition it cannot
// be the owning side and previously we ensured that $assoc is always the owning side of the associations.
// The owning side is necessary at this point because only it contains the JoinColumn information.
if ($assoc['type'] & ClassMetadata::TO_ONE) {
$sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ';
$first = true;
foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($relation['isOwningSide']) {
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $sourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn;
} else {
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn;
}
}
} else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) {
// Join relation table
$joinTable = $assoc['joinTable'];
$joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias);
$sql .= $sourceClass->getQuotedJoinTableName($assoc, $this->_platform) . ' ' . $joinTableAlias . ' ON ';
$first = true;
if ($relation['isOwningSide']) {
foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
} else {
foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($sourceClass->containsForeignIdentifier && !isset($sourceClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $sourceClass->getQuotedColumnName($sourceClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
// Join target table
$sql .= ($joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER) ? ' LEFT JOIN ' : ' INNER JOIN ';
$sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ';
$first = true;
if ($relation['isOwningSide']) {
foreach ($assoc['relationToTargetKeyColumns'] as $relationColumn => $targetColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$targetColumn])) {
$quotedTargetColumn = $targetColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$targetColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
} else {
foreach ($assoc['relationToSourceKeyColumns'] as $relationColumn => $sourceColumn) {
if ( ! $first) $sql .= ' AND '; else $first = false;
if ($targetClass->containsForeignIdentifier && !isset($targetClass->fieldNames[$sourceColumn])) {
$quotedTargetColumn = $sourceColumn; // Join columns cannot be quoted.
} else {
$quotedTargetColumn = $targetClass->getQuotedColumnName($targetClass->fieldNames[$sourceColumn], $this->_platform);
}
$sql .= $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $relationColumn;
}
}
}
// Apply the filters
if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) {
$sql .= ' AND ' . $filterExpr;
}
// Handle WITH clause
if (($condExpr = $join->conditionalExpression) !== null) {
// Phase 2 AST optimization: Skip processment of ConditionalExpression
// if only one ConditionalTerm is defined
$sql .= ' AND (' . $this->walkConditionalExpression($condExpr) . ')';
}
$discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias));

View File

@ -135,6 +135,14 @@ class SelectSqlGenerationTest extends \Doctrine\Tests\OrmTestCase
); );
} }
public function testSupportsJoinOnMultipleComponents()
{
$this->assertSqlGeneration(
'SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user',
'SELECT c0_.id AS id0, c0_.status AS status1, c0_.username AS username2, c0_.name AS name3, c1_.phonenumber AS phonenumber4 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON (c0_.id = c1_.user_id)'
);
}
public function testSupportsSelectWithCollectionAssociationJoin() public function testSupportsSelectWithCollectionAssociationJoin()
{ {
$this->assertSqlGeneration( $this->assertSqlGeneration(