. */ namespace Doctrine\DBAL\Sharding\SQLAzure\Schema; use Doctrine\DBAL\Schema\Visitor\Visitor, Doctrine\DBAL\Schema\Table, Doctrine\DBAL\Schema\Schema, Doctrine\DBAL\Schema\Column, Doctrine\DBAL\Schema\ForeignKeyConstraint, Doctrine\DBAL\Schema\Constraint, Doctrine\DBAL\Schema\Sequence, Doctrine\DBAL\Schema\Index; /** * Converts a single tenant schema into a multi-tenant schema for SQL Azure * Federations under the following assumptions: * * - Every table is part of the multi-tenant application, only explicitly * excluded tables are non-federated. The behavior of the tables being in * global or federated database is undefined. It depends on you selecting a * federation before DDL statements or not. * - Every Primary key of a federated table is extended by another column * 'tenant_id' with a default value of the SQLAzure function * `federation_filtering_value('tenant_id')`. * - You always have to work with `filtering=On` when using federations with this * multi-tenant approach. * - Primary keys are either using globally unique ids (GUID, Table Generator) * or you explicitly add the tenent_id in every UPDATE or DELETE statement * (otherwise they will affect the same-id rows from other tenents as well). * SQLAzure throws errors when you try to create IDENTIY columns on federated * tables. * * @author Benjamin Eberlei */ class MultiTenantVisitor implements Visitor { /** * @var array */ private $excludedTables = array(); /** * @var string */ private $tenantColumnName; /** * @var string */ private $tenantColumnType = 'integer'; /** * Name of the federation distribution, defaulting to the tenantColumnName * if not specified. * * @var string */ private $distributionName; public function __construct(array $excludedTables = array(), $tenantColumnName = 'tenant_id', $distributionName = null) { $this->excludedTables = $excludedTables; $this->tenantColumnName = $tenantColumnName; $this->distributionName = $distributionName ?: $tenantColumnName; } /** * @param Table $table */ public function acceptTable(Table $table) { if (in_array($table->getName(), $this->excludedTables)) { return; } $table->addColumn($this->tenantColumnName, $this->tenantColumnType, array( 'default' => "federation_filtering_value('". $this->distributionName ."')", )); $clusteredIndex = $this->getClusteredIndex($table); $indexColumns = $clusteredIndex->getColumns(); $indexColumns[] = $this->tenantColumnName; if ($clusteredIndex->isPrimary()) { $table->dropPrimaryKey(); $table->setPrimaryKey($indexColumns); } else { $table->dropIndex($clusteredIndex->getName()); $table->addIndex($indexColumns, $clusteredIndex->getName()); $table->getIndex($clusteredIndex->getName())->addFlag('clustered'); } } private function getClusteredIndex($table) { foreach ($table->getIndexes() as $index) { if ($index->isPrimary() && ! $index->hasFlag('nonclustered')) { return $index; } else if ($index->hasFlag('clustered')) { return $index; } } throw new \RuntimeException("No clustered index found on table " . $table->getName()); } /** * @param Schema $schema */ public function acceptSchema(Schema $schema) { } /** * @param Column $column */ public function acceptColumn(Table $table, Column $column) { } /** * @param Table $localTable * @param ForeignKeyConstraint $fkConstraint */ public function acceptForeignKey(Table $localTable, ForeignKeyConstraint $fkConstraint) { } /** * @param Table $table * @param Index $index */ public function acceptIndex(Table $table, Index $index) { } /** * @param Sequence $sequence */ public function acceptSequence(Sequence $sequence) { } }