Skip to content
This repository has been archived by the owner on Jan 29, 2020. It is now read-only.

#186 [RFC][WIP] Sequence Support #187

Open
wants to merge 18 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions doc/book/table-gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,3 +215,99 @@ There are a number of features built-in and shipped with zend-db:
$artistRow->name = 'New Name';
$artistRow->save();
```
- `SequenceFeature`: the ability to integrate with Oracle, PostgreSQL,
(TODO: and SqlServer 2016) sequence objects. Sequences are used for generating sequential integers.
These are usually used for automatically generating Primary Key IDs (similar to MySQL's
`AUTO_INCREMENT` but without performance penalties), or ensuring uniqueness of entity IDs
across multiple tables following some business rule (for example, unique invoice numbers across
multiple order systems). Sequence objects are exclusively used for incrementing an integer
by specified interval (1 by default) and are not tied to table values. Therefore,
they need to be created manually prior to inserting data into tables requiring PKs and `UNIQUE`
columns using DDL
```sql
CREATE SEQUENCE album_id;
```

Sequence's `NEXTVAL` SQL construct can be used either as a default value for a column specified in
table's `CREATE` DDL,
or ran manually at every `insert` operation to have next available integer captured and inserted
in a table along with the rest of the values.

Unless need to guarantee uniqueness across all tables, thus calling `sequence_name.nextval` on every `insert`
query across entire codebase, usually a separate sequence is created per table. Every `insert`
statement would have `album_id.nextval`, `artist_id.nextval` etc. as one of the values along with
the actual data.

To be able to do these operations at the DB abstraction level, `TableGateway` needs to be informed
what columns should be managed by what sequence.

If developer chooses to manually create a sequence for each table's autoincremented column (in Oracle
prior to *12c* this was the only way), then the name of sequence responsible for particular table
would known and can be applied to `TableGateway` right away.

```php
$table = new TableGateway('artist', $adapter, new Feature\SequenceFeature('id', 'artist_id_sequence'));

$table->insert(['name'] => 'New Name';
$nextId = $table->nextSequenceId('id');
$lastInsertId = $table->lastSequenceId('id');
```

However, PostgreSQL (TODO: and Oracle since *12c*) allows automatic creation of sequences during `CREATE TABLE`
or `ALTER TABLE` operation by specifying column type `SERIAL`:

```sql
CREATE TABLE artist
{
id SERIAL,
name CHARACTER VARYING (30)
};
````

Or using Zend's `Db\Sql\DDL`

```php
$table = new CreateTable('artist');

$idColumn = new Serial('id');
$nameColumn = new Char('name');

$table->addColumn($idColumn);
$table->addColumn($nameColumn);
```

In this case, sequence is created automatically. `TableGateway` still has to be aware of what column
is getting autoincrement treatment but without knowing exactly what the sequence name is, second parameter
should be left blank:

```php
$table = new TableGateway('artist' $adapter, new Feature\SequenceFeature('id');
```

With second parameter left null, `TableGateway` will generate sequence name based on same rule
PostgreSQL uses (*tablename_columnname_seq* but if the resultant name is determined to be greater than
63 bytes, an additional query will be made to database schema to find what PostgreSQL has created
instead, since transaction rules are more complex.

This is important to know if you have long table and column names, and do not want
to run an extra metadata query on every `TableGateway` object construction. If that is the case,
take note of what PostgreSQL created using
```sql
pg_get_serial_sequence('artist', 'id');
```

and add it to `SequenceFeature` constructor.

There could be complex business rules needing multiple sequences in a table. `TableGateway` can have
multiple sequences added in an array:

```php
$table = new TableGateway('wholesales', $adapter, [
new Feature\Sequence('id', 'sale_id_sequence'),
new Feature\Sequence('invoice_id', 'invoice_id_sequence)'
]);
```

Then calls to `$table->lastSequenceId('invoice_id')` will find the appropriate sequence instance to
get ID from.

23 changes: 23 additions & 0 deletions src/Sql/Ddl/Column/Serial.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

/**
* Zend Framework (http://framework.zend.com/).
*
* @link http://github.com/zendframework/zf2 for the canonical source repository
*
* @copyright Copyright (c) 2005-2016 Zend Technologies USA Inc. (http://www.zend.com)
* @license http://framework.zend.com/license/new-bsd New BSD License
*/

namespace Zend\Db\Sql\Ddl\Column;

/**
* Column for PostgreSQL to automatically increment column.
*
* Similar to MySQL's autoincrement, but without performance issues.
* Used in conjunction with SequenceFeature.
*/
class Serial extends Column
{
protected $type = 'SERIAL';
}
4 changes: 1 addition & 3 deletions src/TableGateway/AbstractTableGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
*
* @property AdapterInterface $adapter
* @property int $lastInsertValue
* @property string $table
*/
abstract class AbstractTableGateway implements TableGatewayInterface
{
Expand Down Expand Up @@ -83,7 +82,6 @@ public function isInitialized()
* Initialize
*
* @throws Exception\RuntimeException
* @return null
*/
public function initialize()
{
Expand Down Expand Up @@ -122,7 +120,7 @@ public function initialize()
/**
* Get table name
*
* @return string
* @return string|array|TableIdentifier
*/
public function getTable()
{
Expand Down
58 changes: 37 additions & 21 deletions src/TableGateway/Feature/FeatureSet.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class FeatureSet
protected $tableGateway = null;

/**
* @var AbstractFeature[]
* @var string[]AbstractFeature[]
*/
protected $features = [];

Expand All @@ -38,22 +38,26 @@ public function __construct(array $features = [])
public function setTableGateway(AbstractTableGateway $tableGateway)
{
$this->tableGateway = $tableGateway;
foreach ($this->features as $feature) {
$feature->setTableGateway($this->tableGateway);
foreach ($this->features as $featureClass => $featureSet) {
foreach ($featureSet as $feature) {
$feature->setTableGateway($this->tableGateway);
}
}
return $this;
}

public function getFeatureByClassName($featureClassName)
{
$feature = false;
foreach ($this->features as $potentialFeature) {
if ($potentialFeature instanceof $featureClassName) {
$feature = $potentialFeature;
break;
}
if (!array_key_exists($featureClassName, $this->features)) {
return false;
}

$featuresByType = $this->features[$featureClassName];
if (count($featuresByType) == 1) {
return $featuresByType[0];
} else {
return $featuresByType;
}
return $feature;
}

public function addFeatures(array $features)
Expand All @@ -69,17 +73,19 @@ public function addFeature(AbstractFeature $feature)
if ($this->tableGateway instanceof TableGatewayInterface) {
$feature->setTableGateway($this->tableGateway);
}
$this->features[] = $feature;
$this->features[get_class($feature)][] = $feature;
return $this;
}

public function apply($method, $args)
{
foreach ($this->features as $feature) {
if (method_exists($feature, $method)) {
$return = call_user_func_array([$feature, $method], $args);
if ($return === self::APPLY_HALT) {
break;
foreach ($this->features as $featureClass => $featureSet) {
foreach ($featureSet as $feature) {
if (method_exists($feature, $method)) {
$return = call_user_func_array([$feature, $method], $args);
if ($return === self::APPLY_HALT) {
break;
}
}
}
}
Expand Down Expand Up @@ -132,8 +138,8 @@ public function callMagicSet($property, $value)
public function canCallMagicCall($method)
{
if (!empty($this->features)) {
foreach ($this->features as $feature) {
if (method_exists($feature, $method)) {
foreach ($this->features as $featureClass => $featuresSet) {
if (method_exists($featureClass, $method)) {
return true;
}
}
Expand All @@ -149,9 +155,19 @@ public function canCallMagicCall($method)
*/
public function callMagicCall($method, $arguments)
{
foreach ($this->features as $feature) {
if (method_exists($feature, $method)) {
return $feature->$method($arguments);
foreach ($this->features as $featureClass => $featuresSet) {
if (method_exists($featureClass, $method)) {

// iterator management instead of foreach to avoid extra conditions and indentations
reset($featuresSet);
$featureReturn = null;
while ($featureReturn === null) {
$current = current($featuresSet);
$featureReturn = $current->$method($arguments);
next($featuresSet);
}

return $featureReturn;
}
}

Expand Down
Loading