/** * Gather columns and fk constraints that are required for one part of relationship. * * @param array $joinColumns * @param \Doctrine\DBAL\Schema\Table $theJoinTable * @param ClassMetadata $class * @param array $mapping * @param array $primaryKeyColumns * @param array $uniqueConstraints */ private function _gatherRelationJoinColumns($joinColumns, $theJoinTable, $class, $mapping, &$primaryKeyColumns, &$uniqueConstraints) { $localColumns = array(); $foreignColumns = array(); $fkOptions = array(); $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); foreach ($joinColumns as $joinColumn) { list($definingClass, $referencedFieldName) = $this->getDefiningClass($class, $joinColumn['referencedColumnName']); if (!$definingClass) { throw new \Doctrine\ORM\ORMException("Column name `" . $joinColumn['referencedColumnName'] . "` referenced for relation from " . $mapping['sourceEntity'] . " towards " . $mapping['targetEntity'] . " does not exist."); } $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $class, $this->platform); $primaryKeyColumns[] = $quotedColumnName; $localColumns[] = $quotedColumnName; $foreignColumns[] = $quotedRefColumnName; if (!$theJoinTable->hasColumn($quotedColumnName)) { // Only add the column to the table if it does not exist already. // It might exist already if the foreign key is mapped into a regular // property as well. $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); $columnDef = null; if (isset($joinColumn['columnDefinition'])) { $columnDef = $joinColumn['columnDefinition']; } else { if (isset($fieldMapping['columnDefinition'])) { $columnDef = $fieldMapping['columnDefinition']; } } $columnOptions = array('notnull' => false, 'columnDefinition' => $columnDef); if (isset($joinColumn['nullable'])) { $columnOptions['notnull'] = !$joinColumn['nullable']; } if (isset($fieldMapping['options'])) { $columnOptions['options'] = $fieldMapping['options']; } if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) { $columnOptions['length'] = $fieldMapping['length']; } else { if ($fieldMapping['type'] == "decimal") { $columnOptions['scale'] = $fieldMapping['scale']; $columnOptions['precision'] = $fieldMapping['precision']; } } $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions); } if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) { $uniqueConstraints[] = array('columns' => array($quotedColumnName)); } if (isset($joinColumn['onDelete'])) { $fkOptions['onDelete'] = $joinColumn['onDelete']; } } $theJoinTable->addUnnamedForeignKeyConstraint($foreignTableName, $localColumns, $foreignColumns, $fkOptions); }
private function getUniqueAndRequiredAssociations(ClassMetadata $meta, $entity) { $associations = array(); foreach ($meta->getAssociationNames() as $associationName) { $mapping = $meta->getAssociationMapping($associationName); if (!empty($mapping['id']) && $meta->usesIdGenerator()) { // autogenerated id continue; } if (!($mapping['type'] & ClassMetadata::TO_ONE)) { // is not to one relation continue; } if (empty($mapping['isOwningSide'])) { // is not owning side continue; } foreach ($mapping['joinColumns'] as $joinColumn) { if (!empty($joinColumn['nullable']) && empty($joinColumn['unique'])) { // is nullable and is not unique continue; } $targetColumn = $joinColumn['referencedColumnName']; $targetClass = $this->em->getClassMetadata($mapping['targetEntity']); $newVal = $meta->getFieldValue($entity, $associationName); if ($newVal !== NULL) { $newValId = $this->uow->getEntityIdentifier($newVal); } switch (TRUE) { case $newVal === NULL: $value = NULL; break; case $targetClass->containsForeignIdentifier: $value = $newValId[$targetClass->getFieldForColumn($targetColumn)]; break; default: $value = $newValId[$targetClass->fieldNames[$targetColumn]]; break; } $sourceColumn = $joinColumn['name']; $quotedColumn = $this->quotes->getJoinColumnName($joinColumn, $meta, $this->platform); $associations[$sourceColumn]['value'] = $value; $associations[$sourceColumn]['quotedColumn'] = $quotedColumn; $associations[$sourceColumn]['type'] = $targetClass->getTypeOfColumn($targetColumn); } } return $associations; }
/** * Walks down a JoinAssociationDeclaration AST node, thereby generating the appropriate SQL. * * @param AST\JoinAssociationDeclaration $joinAssociationDeclaration * @param int $joinType * * @return string * * @throws QueryException */ public function walkJoinAssociationDeclaration($joinAssociationDeclaration, $joinType = AST\Join::JOIN_TYPE_INNER) { $sql = ''; $associationPathExpression = $joinAssociationDeclaration->joinAssociationPathExpression; $joinedDqlAlias = $joinAssociationDeclaration->aliasIdentificationVariable; $indexBy = $joinAssociationDeclaration->indexBy; $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(), $associationPathExpression->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. switch (true) { case $assoc['type'] & ClassMetadata::TO_ONE: $conditions = array(); foreach ($assoc['joinColumns'] as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); if ($relation['isOwningSide']) { $conditions[] = $sourceTableAlias . '.' . $quotedSourceColumn . ' = ' . $targetTableAlias . '.' . $quotedTargetColumn; continue; } $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $quotedSourceColumn; } // Apply remaining inheritance restrictions $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); if ($discrSql) { $conditions[] = $discrSql; } // Apply the filters $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); if ($filterExpr) { $conditions[] = $filterExpr; } $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); break; case $assoc['type'] == ClassMetadata::MANY_TO_MANY: // Join relation table $joinTable = $assoc['joinTable']; $joinTableAlias = $this->getSQLTableAlias($joinTable['name'], $joinedDqlAlias); $joinTableName = $sourceClass->getQuotedJoinTableName($assoc, $this->platform); $conditions = array(); $relationColumns = $relation['isOwningSide'] ? $assoc['joinTable']['joinColumns'] : $assoc['joinTable']['inverseJoinColumns']; foreach ($relationColumns as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; } $sql .= $joinTableName . ' ' . $joinTableAlias . ' ON ' . implode(' AND ', $conditions); // Join target table $sql .= $joinType == AST\Join::JOIN_TYPE_LEFT || $joinType == AST\Join::JOIN_TYPE_LEFTOUTER ? ' LEFT JOIN ' : ' INNER JOIN '; $conditions = array(); $relationColumns = $relation['isOwningSide'] ? $assoc['joinTable']['inverseJoinColumns'] : $assoc['joinTable']['joinColumns']; foreach ($relationColumns as $joinColumn) { $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); $conditions[] = $targetTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableAlias . '.' . $quotedSourceColumn; } // Apply remaining inheritance restrictions $discrSql = $this->_generateDiscriminatorColumnConditionSQL(array($joinedDqlAlias)); if ($discrSql) { $conditions[] = $discrSql; } // Apply the filters $filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias); if ($filterExpr) { $conditions[] = $filterExpr; } $sql .= $targetTableName . ' ' . $targetTableAlias . ' ON ' . implode(' AND ', $conditions); break; } // FIXME: these should either be nested or all forced to be left joins (DDC-XXX) if ($targetClass->isInheritanceTypeJoined()) { $sql .= $this->_generateClassTableInheritanceJoins($targetClass, $joinedDqlAlias); } // Apply the indexes if ($indexBy) { // For Many-To-One or One-To-One associations this obviously makes no sense, but is ignored silently. $this->rsm->addIndexBy($indexBy->simpleStateFieldPathExpression->identificationVariable, $indexBy->simpleStateFieldPathExpression->field); } else { if (isset($relation['indexBy'])) { $this->rsm->addIndexBy($joinedDqlAlias, $relation['indexBy']); } } return $sql; }
/** * Builds the left-hand-side of a where condition statement. * * @param string $field * @param array|null $assoc * * @return string * * @throws \Doctrine\ORM\ORMException */ protected function getSelectConditionStatementColumnSQL($field, $assoc = null) { if (isset($this->class->columnNames[$field])) { $className = (isset($this->class->fieldMappings[$field]['inherited'])) ? $this->class->fieldMappings[$field]['inherited'] : $this->class->name; return $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform); } if (isset($this->class->associationMappings[$field])) { if ( ! $this->class->associationMappings[$field]['isOwningSide']) { throw ORMException::invalidFindByInverseAssociation($this->class->name, $field); } $joinColumn = $this->class->associationMappings[$field]['joinColumns'][0]; $className = (isset($this->class->associationMappings[$field]['inherited'])) ? $this->class->associationMappings[$field]['inherited'] : $this->class->name; return $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); } if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { // very careless developers could potentially open up this normally hidden api for userland attacks, // therefore checking for spaces and function calls which are not allowed. // found a join column condition, not really a "field" return $field; } throw ORMException::unrecognizedField($field); }
/** * Builds the left-hand-side of a where condition statement. * * @param string $field * @param array|null $assoc * * @return string[] * * @throws \Doctrine\ORM\ORMException */ private function getSelectConditionStatementColumnSQL($field, $assoc = null) { if (isset($this->class->columnNames[$field])) { $className = isset($this->class->fieldMappings[$field]['inherited']) ? $this->class->fieldMappings[$field]['inherited'] : $this->class->name; return array($this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)); } if (isset($this->class->associationMappings[$field])) { $association = $this->class->associationMappings[$field]; // Many-To-Many requires join table check for joinColumn $columns = array(); $class = $this->class; if ($association['type'] === ClassMetadata::MANY_TO_MANY) { if (!$association['isOwningSide']) { $association = $assoc; } $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); $joinColumns = $assoc['isOwningSide'] ? $association['joinTable']['joinColumns'] : $association['joinTable']['inverseJoinColumns']; foreach ($joinColumns as $joinColumn) { $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); } } else { if (!$association['isOwningSide']) { throw ORMException::invalidFindByInverseAssociation($this->class->name, $field); } $className = isset($association['inherited']) ? $association['inherited'] : $this->class->name; foreach ($association['joinColumns'] as $joinColumn) { $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); } } return $columns; } if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) { // very careless developers could potentially open up this normally hidden api for userland attacks, // therefore checking for spaces and function calls which are not allowed. // found a join column condition, not really a "field" return array($field); } throw ORMException::unrecognizedField($field); }
/** * Gathers columns and fk constraints that are required for one part of relationship. * * @param array $joinColumns * @param Table $theJoinTable * @param ClassMetadata $class * @param array $mapping * @param array $primaryKeyColumns * @param array $addedFks * @param array $blacklistedFks * * @return void * * @throws \Doctrine\ORM\ORMException */ private function gatherRelationJoinColumns($joinColumns, $theJoinTable, $class, $mapping, &$primaryKeyColumns, &$addedFks, &$blacklistedFks) { $localColumns = array(); $foreignColumns = array(); $fkOptions = array(); $foreignTableName = $this->quoteStrategy->getTableName($class, $this->platform); $uniqueConstraints = array(); foreach ($joinColumns as $joinColumn) { list($definingClass, $referencedFieldName) = $this->getDefiningClass($class, $joinColumn['referencedColumnName']); if (!$definingClass) { throw new \Doctrine\ORM\ORMException("Column name `" . $joinColumn['referencedColumnName'] . "` referenced for relation from " . $mapping['sourceEntity'] . " towards " . $mapping['targetEntity'] . " does not exist."); } $quotedColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); $quotedRefColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $class, $this->platform); $primaryKeyColumns[] = $quotedColumnName; $localColumns[] = $quotedColumnName; $foreignColumns[] = $quotedRefColumnName; if (!$theJoinTable->hasColumn($quotedColumnName)) { // Only add the column to the table if it does not exist already. // It might exist already if the foreign key is mapped into a regular // property as well. $fieldMapping = $definingClass->getFieldMapping($referencedFieldName); $columnDef = null; if (isset($joinColumn['columnDefinition'])) { $columnDef = $joinColumn['columnDefinition']; } elseif (isset($fieldMapping['columnDefinition'])) { $columnDef = $fieldMapping['columnDefinition']; } $columnOptions = array('notnull' => false, 'columnDefinition' => $columnDef); if (isset($joinColumn['nullable'])) { $columnOptions['notnull'] = !$joinColumn['nullable']; } if (isset($fieldMapping['options'])) { $columnOptions['options'] = $fieldMapping['options']; } if ($fieldMapping['type'] == "string" && isset($fieldMapping['length'])) { $columnOptions['length'] = $fieldMapping['length']; } elseif ($fieldMapping['type'] == "decimal") { $columnOptions['scale'] = $fieldMapping['scale']; $columnOptions['precision'] = $fieldMapping['precision']; } $theJoinTable->addColumn($quotedColumnName, $fieldMapping['type'], $columnOptions); } if (isset($joinColumn['unique']) && $joinColumn['unique'] == true) { $uniqueConstraints[] = array('columns' => array($quotedColumnName)); } if (isset($joinColumn['onDelete'])) { $fkOptions['onDelete'] = $joinColumn['onDelete']; } } // Prefer unique constraints over implicit simple indexes created for foreign keys. // Also avoids index duplication. foreach ($uniqueConstraints as $indexName => $unique) { $theJoinTable->addUniqueIndex($unique['columns'], is_numeric($indexName) ? null : $indexName); } $compositeName = $theJoinTable->getName() . '.' . implode('', $localColumns); if (isset($addedFks[$compositeName]) && ($foreignTableName != $addedFks[$compositeName]['foreignTableName'] || 0 < count(array_diff($foreignColumns, $addedFks[$compositeName]['foreignColumns'])))) { foreach ($theJoinTable->getForeignKeys() as $fkName => $key) { if (0 === count(array_diff($key->getLocalColumns(), $localColumns)) && ($key->getForeignTableName() != $foreignTableName || 0 < count(array_diff($key->getForeignColumns(), $foreignColumns)))) { $theJoinTable->removeForeignKey($fkName); break; } } $blacklistedFks[$compositeName] = true; } elseif (!isset($blacklistedFks[$compositeName])) { $addedFks[$compositeName] = array('foreignTableName' => $foreignTableName, 'foreignColumns' => $foreignColumns); $theJoinTable->addUnnamedForeignKeyConstraint($foreignTableName, $localColumns, $foreignColumns, $fkOptions); } }
/** * Gets the list of columns to put in the INSERT SQL statement. * * Subclasses should override this method to alter or change the list of * columns placed in the INSERT statements used by the persister. * * @return array The list of columns. */ protected function _getInsertColumnList() { $columns = array(); foreach ($this->_class->reflFields as $name => $field) { if ($this->_class->isVersioned && $this->_class->versionField == $name) { continue; } if (isset($this->_class->associationMappings[$name])) { $assoc = $this->_class->associationMappings[$name]; if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) { foreach ($assoc['joinColumns'] as $joinColumn) { $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->_class, $this->_platform); } } } else { if ($this->_class->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->_class->identifier[0] != $name) { $columns[] = $this->quoteStrategy->getColumnName($name, $this->_class, $this->_platform); $this->_columnTypes[$name] = $this->_class->fieldMappings[$name]['type']; } } } return $columns; }
/** * @param DoctrineSqlFilter $sqlFilter * @param QuoteStrategy $quoteStrategy * @param ClassMetadata $targetEntity * @param string $targetTableAlias * @param string $targetEntityPropertyName * @return string * @throws InvalidQueryRewritingConstraintException * @throws \Exception */ protected function getSqlForManyToOneAndOneToOneRelationsWithPropertyPath(DoctrineSqlFilter $sqlFilter, QuoteStrategy $quoteStrategy, ClassMetadata $targetEntity, $targetTableAlias, $targetEntityPropertyName) { $subselectQuery = $this->getSubselectQuery($targetEntity, $targetEntityPropertyName); $associationMapping = $targetEntity->getAssociationMapping($targetEntityPropertyName); $subselectConstraintQueries = array(); foreach ($associationMapping['joinColumns'] as $joinColumn) { $rootAliases = $subselectQuery->getQueryBuilder()->getRootAliases(); $subselectQuery->getQueryBuilder()->select($rootAliases[0] . '.' . $targetEntity->getFieldForColumn($joinColumn['referencedColumnName'])); $subselectSql = $subselectQuery->getSql(); foreach ($subselectQuery->getParameters() as $parameter) { $parameterValue = $parameter->getValue(); if (is_object($parameterValue)) { $parameterValue = $this->persistenceManager->getIdentifierByObject($parameter->getValue()); } $subselectSql = preg_replace('/\\?/', $this->entityManager->getConnection()->quote($parameterValue, $parameter->getType()), $subselectSql, 1); } $quotedColumnName = $quoteStrategy->getJoinColumnName($joinColumn, $targetEntity, $this->entityManager->getConnection()->getDatabasePlatform()); $subselectIdentifier = 'subselect' . md5($subselectSql); $subselectConstraintQueries[] = $targetTableAlias . '.' . $quotedColumnName . ' IN (SELECT ' . $subselectIdentifier . '.' . $joinColumn['referencedColumnName'] . '0 FROM (' . $subselectSql . ') AS ' . $subselectIdentifier . ' ) '; } return ' (' . implode(' ) AND ( ', $subselectConstraintQueries) . ') '; }
/** * @param array $assoc * @param object $sourceEntity * @param int|null $offset * @param int|null $limit * * @return \Doctrine\DBAL\Driver\Statement * * @throws \Doctrine\ORM\Mapping\MappingException */ private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null) { $this->switchPersisterContext($offset, $limit); $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']); $class = $sourceClass; $association = $assoc; $criteria = array(); $parameters = array(); if (!$assoc['isOwningSide']) { $class = $this->em->getClassMetadata($assoc['targetEntity']); $association = $class->associationMappings[$assoc['mappedBy']]; } $joinColumns = $assoc['isOwningSide'] ? $association['joinTable']['joinColumns'] : $association['joinTable']['inverseJoinColumns']; $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); foreach ($joinColumns as $joinColumn) { $sourceKeyColumn = $joinColumn['referencedColumnName']; $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); switch (true) { case $sourceClass->containsForeignIdentifier: $field = $sourceClass->getFieldForColumn($sourceKeyColumn); $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); if (isset($sourceClass->associationMappings[$field])) { $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]]; } break; case isset($sourceClass->fieldNames[$sourceKeyColumn]): $field = $sourceClass->fieldNames[$sourceKeyColumn]; $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); break; default: throw MappingException::joinColumnMustPointToMappedField($sourceClass->name, $sourceKeyColumn); } $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value; $parameters[] = array('value' => $value, 'field' => $field, 'class' => $sourceClass); } $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); list($params, $types) = $this->expandToManyParameters($parameters); return $this->conn->executeQuery($sql, $params, $types); }