Додавання підтримки СУБД Firebird в фреймворк Laravel

Під час написання прикладу (пізніше буде посилання) веб-додатків на PHP з використанням СУБД Firebird постало питання вибору фреймворку для розробки з використанням архітектурної моделі MVC. Вибір фреймворків під PHP дуже великий, але найбільш зручним, простим і легко розширюваним здався Laravel. Однак цей фреймворк не підтримував з коробки СУБД Firebird. Laravel використовує для роботи з базою даних драйвера PDO. Оскільки для Firebird існує драйвер PDO, то це наштовхнуло мене на думку, що можна з деякими зусиллями змусити працювати Laravel c Firebird.

Laravel — безкоштовний веб-фреймворк з відкритим кодом, призначений для розробки з використанням архітектурної моделі MVC (англ. Model View Controller — модель-представлення-контролер). Laravel – це зручний і легко розширюваний фреймворк для побудови ваших веб-додатків. З коробки фреймворк Laravel підтримує 4 СУБД: MySQL, Postgres, SQLite і MS SQL Server. У цій статті я розповім як додати ще одну СУБД Firebird.

Клас підключення FirebirdConnection
Кожен раз, коли ви підключаєтеся до бази даних, з допомогою фабрики Illuminate\Database\Connectors\ConnectionFactory створюється конкретний екземпляр підключення залежно від типу СУБД, який реалізує інтерфейс Illuminate\Database\ConnectionInterface. Крім того, фабрика створює якийсь коннектор, який на основі параметрів конфігурації формує рядок підключення і передає її в конструктор підключення PDO. Конектор створюється в методі createConnector. Трохи модифікуємо його так, щоб він міг створювати коннектор для Firebird.

public function createConnector(array $config)
{
if (! isset($config['driver'])) {
throw new InvalidArgumentException('A driver must be specified.');
}

if ($this->container->bound($key = "db.connector.{$config['driver']}")) {
return $this->container->make($key);
}

switch ($config['driver']) {
case 'mysql':
return new MySqlConnector;
case 'pgsql':
return new PostgresConnector;
case 'sqlite':
return new SQLiteConnector;
case 'sqlsrv':
return new SqlServerConnector;
case 'firebird': // Add support Firebird
return new FirebirdConnector; 
}

throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
}

Сам клас коннектора Illuminate\Database\Connectors\FirebirdConnector визначимо трохи пізніше. А поки модифікуємо метод createConnection призначений для створення підключення реалізує інтерфейс Illuminate\Database\ConnectionInterface.

protected function createConnection($driver, $connection, $database, $prefix = ", 
array $config = [])
{
if ($this->container->bound($key = "db.connection.{$driver}")) {
return $this->container->make($key, [$connection, $database, $prefix, $config]);
}

switch ($driver) {
case 'mysql':
return new MySqlConnection($connection, $database, $prefix, $config);
case 'pgsql':
return new PostgresConnection($connection, $database, $prefix, $config);
case 'sqlite':
return new SQLiteConnection($connection, $database, $prefix, $config);
case 'sqlsrv':
return new SqlServerConnection($connection, $database, $prefix, $config);
case 'firebird': // Add support Firebird
return new FirebirdConnection($connection, $database, $prefix, $config); 
}

throw new InvalidArgumentException("Unsupported driver [$driver]");
}

Тепер перейдемо до створення коннектора для Firebird – класу Illuminate\Database\Connectors\FirebirdConnector. За основу можна взяти будь-який з існуючих конекторів, наприклад коннектор для Postgres і переробимо його під Firebird.

Для початку змінимо метод для формування рядка підключення у відповідності з форматом рядка описаної в документації:

protected function getDsn(array $config) {
$dsn = "firebird:dbname=";
if (isset($config['host'])) {
$dsn .= $config['host'];
}
if (isset($config['port'])) {
$dsn .= "/" . $config['port'];
}
$dsn .= ":" . $config['database'];
if (isset($config['charset'])) {
$dsn .= ";charset=" . $config['charset'];
} 
if (isset($config['role'])) {
$dsn .= ";role=" . $config['role'];
} 

return $dsn;
}

Метод, що створює PDO підключення, в даному випадку буде максимально спрощено

public function connect(array $config) {
$dsn = $this->getDsn($config);

$options = $this->getOptions($config);

// We need to grab the PDO options that should be used while making the brand
// new connection instance. The PDO options control various aspects of the
// connection's behavior, and some might be specified by the developers.
$connection = $this->createConnection($dsn, $config, $options);

return $connection;
}

Самі підключення для різних СУБД, які використовуються в Laravel, успадковують клас Illuminate\Database\Connection. Саме цей клас інкапсулює в собі всі можливості по роботі з БД, які використовуються в Laravel, зокрема в ORM Eloquent. У класах спадкоємців (під кожен тип СУБД) реалізовані методи повертають екземпляр класу з описом граматики, який потрібні при побудові DML запитів, і примірник граматики для DDL запитів (використовується в міграції), також помічники для додавання імені схеми до таблиці. Цей клас буде виглядати наступним чином:

namespace Illuminate\Database;

use Illuminate\Database\Query\Processors\FirebirdProcessor;
use Doctrine\DBAL\Driver\PDOFirebird\Driver as DoctrineDriver;
use Illuminate\Database\Query\Grammars\FirebirdGrammar as QueryGrammar;
use Illuminate\Database\Schema\Grammars\FirebirdGrammar as SchemaGrammar;

class FirebirdConnection extends Connection
{

/**
* Get the default query grammar instance.
*
* @return \Illuminate\Database\Query\Grammars\FirebirdGrammar
*/
protected function getDefaultQueryGrammar()
{
return $this->withTablePrefix(new QueryGrammar);
}

/**
* Get the default schema grammar instance.
*
* @return \Illuminate\Database\Schema\Grammars\FirebirdGrammar
*/
protected function getDefaultSchemaGrammar()
{
return $this->withTablePrefix(new SchemaGrammar);
}

/**
* Get the default post processor instance.
*
* @return \Illuminate\Database\Query\Processors\FirebirdProcessor
*/
protected function getDefaultPostProcessor()
{
return new FirebirdProcessor;
}

/**
* Get the Doctrine DBAL driver.
*
* @return \Doctrine\DBAL\Driver\PDOFirebird\Driver
*/
protected function getDoctrineDriver()
{
return new DoctrineDriver;
}
}

Метод для повернення примірника драйвера Doctrine формально потрібно, тому ми вводимо його, але у мене не було мети працювати з Doctrine (тільки з Eloquent), тому реалізацію я не робив. При бажанні ви можете це зробити самостійно.

Постпроцесор Illuminate\Database\Query\Processors\FirebirdProcessor призначений для додаткової обробки результатів запитів, зокрема він допомагає витягувати ідентифікатор запису з INSERT запиту. Визначення його реалізація повністю скопійована з Illuminate\Database\Query\Processors\PostgresProcessor.

Клас граматики для побудови DML запитів
Тепер переходимо до самого цікавого і важливого, а саме до опису граматики для побудови DML запитів. Саме цей клас відповідає за перетворення виразу, записаного для побудовника запитів Laravel, діалект мови SQL, що використовується у вашій СУБД. Знаючи, що робить певна конструкція в одній СУБД, ви без праці можете записати цю конструкцію для Firebird. Насправді DML частина мови SQL досить стандартна і не значно відрізняється для різних СУБД, принаймні, з точки зору тих запитів, які можна побудувати з допомогою Laravel.

Граматики DML запитів успадковуються від класу Illuminate\Database\Query\Grammars\Grammar. У захищеному властивості $selectComponents перераховані частини SELECT запиту, з яких збирається запит построителем \Illuminate\Database\Query\Builder. У методі compileComponents відбувається обхід цих частин і для кожної з них викликається метод з ім'ям, яке складається з імені частині запиту і префікса compile.

/**
* Compile the components necessary for a select clause.
*
* @param \Illuminate\Database\Query\Builder $query
* @return array
*/
protected function compileComponents(Builder $query)
{
$sql = [];

foreach ($this->selectComponents as $component) {
// To compile the query, we'll spin through each component of the query and
// see if that component exists. If it does we'll just call the compiler
// function for the component which is responsible for making the SQL.
if (! is_null($query->$component)) {
$method = 'compile'.ucfirst($component);

$sql[$component] = $this->$method($query, $query->$component);
}
}

return $sql;
}

Знаючи цей факт, стає зрозуміло, що і де треба модифікувати. Тепер настав час створити спадкоємець класу Illuminate\Database\Query\Grammars\Grammar для визначення граматики Firebird — Illuminate\Database\Query\Grammars\FirebirdGrammar. Визначимо основні відмінні особливості Firebird.

Для простого INSERT запиту основна відмінність полягає у можливості повернення щойно доданої рядки з допомогою пропозиції RETURNING. У Laravel це використовується для повернення ідентифікатора, щойно доданої рядка. Однак MySQL не має такої можливості, а тому метод compileInsertGetId виглядає по-різному для різних СУБД. Firebird підтримує пропозицію RETURNING, як і СКБД Postgres, а тому цей метод можна взяти з граматики для Postgres. Виглядати він буде наступним чином:

/**
* Compile insert an and get ID statement into SQL.
*
* @param \Illuminate\Database\Query\Builder $query
* @param array $values
* @param string $sequence
* @return string
*/
public function compileInsertGetId(Builder $query, $values, $sequence) {
if (is_null($sequence)) {
$sequence = 'id';
}

return $this->compileInsert($query, $values) . 'returning' . $this->wrap($sequence);
}

Мабуть, самим основним відзнакою SELECT запиту є обмеження на кількість записів повернутих запитом, яке часто використовується при посторінкової навігації. Наприклад, наступний вираз:

DB::table('goods')->orderBy('name')->skip(10)->take(20)->get();

У різних СУБД буде виглядати на мові SQL дуже по різному. В MySQL це виглядає так:

SELECT * 
FROM goods
ORDER BY name
LIMIT 10, 20

у Postgres так:

SELECT * 
FROM goods
ORDER BY name
LIMIT 20 OFFSET 10

У Firebird відразу три варіанти. Починаючи з версії 1.5:

SELECT FIRST(10) SKIP(20) * 
FROM goods
ORDER BY name

Починаючи з версії 2.0 додається ще одна конструкція:
SELECT * 
FROM goods
ORDER BY name
ROWS 21, 30

Починаючи з версії 3.0 ще додана конструкція зі стандарту SQL-2011:

SELECT * 
FROM color
ORDER BY name
OFFSET 20 ROWS
FETCH FIRST 10 ROWS ONLY

Найбільш зручним і правильним варіантом, звичайно, є варіант зі стандарту, але хотілося щоб Laravel підтримував одночасно Firebird 2.5 і 3.0, тому ми оберемо другий варіант. У цьому випадку методи compileLimit і compileOffset будуть виглядати наступним чином:

/**
* Compile the "limit" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param int $limit
* @return string
*/
protected function compileLimit(Builder $query, $limit) {
if ($query->offset) {
$first = (int) $query->offset + 1;
return 'rows' . (int) $first;
} else {
return 'rows' . (int) $limit;
}
}

/**
* Compile the "offset" portions of the query.
*
* @param \Illuminate\Database\Query\Builder $query
* @param int $offset
* @return string
*/
protected function compileOffset(Builder $query, $offset) {
if ($query->limit) {
if ($offset) {
$end = (int) $query->limit + (int) $offset;
return 'to' . $end;
} else {
return ";
}
} else {
$begin = (int) $offset + 1;
return 'rows' . $begin . 'to 2147483647';
}
}

Наступне чим відрізняються запити так це витягом частин дати, це робиться за допомогою методу dateBasedWhere. У Firebird для цього використовується стандартна функція EXTRACT. З урахуванням цього наш метод буде виглядати наступним чином:

/**
* Compile a date based where clause.
*
* @param string $type
* @param \Illuminate\Database\Query\Builder $query
* @param array $where
* @return string
*/
protected function dateBasedWhere($type, Builder $query, $where) {
$value = $this->parameter($where['value']);

return 'extract(' . $type . 'from' . $this->wrap($where['column']) . ')' 
. $where['operator'] . '' . $value;
}

Ось власне і все, всі основні відмітні особливості були враховані. Повністю реалізований клас Illuminate\Database\Query\Grammars\FirebirdGrammar ви можете знайти у вихідних кодах доданих до статті.

Клас граматики для побудови DDL запитів
Тепер переходимо до більш складної граматики, що використовується при побудові схем БД. Ця граматика використовується для так званих міграцій (в термінах Laravel). Тут відмінностей між різними СУБД набагато більше, починаючи від типів даних, закінчуючи автоинкрементными полями. Крім того, тут потрібно тут потрібно переписати запити до низки системних таблиць для визначення наявності або стовпця таблиці.

Граматики DDL запитів успадковуються від класу Illuminate\Database\Schema\Grammars\ Grammar. Створимо свою граматику Illuminate\Database\Schema\Grammars\ FirebirdGrammar. У захищеному властивості $modifiers перераховані модифікатори полів таблиці, для кожного з параметрів, перерахованих в масиві повинні бути метод, який починається з modify, після чого йде назва модифікатора. Будуємо ці методи за аналогією з граматикою для MySQL, але з урахуванням специфіки Firebird.

Підтримка модифікаторів стовпців
class FirebirdGrammar extends Grammar {

/**
* The possible column modifiers.
*
* @var array
*/
protected $modifiers = ['Charset', 'Collate', 'Increment', 'Nullable', 'Default'];

/**
* The columns available as serials.
*
* @var array
*/
protected $serials = ['bigInteger', 'integer', 
'mediumInteger', 'smallInteger', 'tinyInteger'];

// ........................ пропущено ..................

/**
* Get the SQL for a character set column modifier.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $column
* @return string|null
*/
protected function modifyCharset(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->charset)) {
return ' character set '.$column->charset;
}
} 

/**
* Get the SQL for a collation column modifier.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $column
* @return string|null
*/
protected function modifyCollate(Blueprint $blueprint, Fluent $column)
{
if (! is_null($column->collation)) {
return ' collate '.$column->collation;
}
} 

/**
* Get the SQL for a nullable column modifier.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $column
* @return string|null
*/
protected function modifyNullable(Blueprint $blueprint, Fluent $column) {
return $column->nullable ? ": 'not null';
}

/**
* Get the SQL for a default column modifier.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $column
* @return string|null
*/
protected function modifyDefault(Blueprint $blueprint, Fluent $column) {
if (!is_null($column->default)) {
return 'default' . $this->getDefaultValue($column->default);
}
}

/**
* Get the SQL for an auto-increment column modifier.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $column
* @return string|null
*/
protected function modifyIncrement(Blueprint $blueprint, Fluent $column) {
if (in_array($column->type, $this->serials) && $column->autoIncrement) {
return 'primary key';
}
}

// ........................ пропущено ..................

}


В масиві $serials перераховані типи (доступні Laravel) для яких доступний модифікатор Increment (автоінкрементний стовпець). На типах доступних в Laravel варто зупинитися окремо. Типи доступні в Laravel перераховані в документації з міграції параграф «Доступні типи стовпців». Для перетворення типу доступного в Laravel в тип даних конкретної СУБД всередині граматики використовуються методи, які починаються зі слова type, після якого слідує ім'я типу.

Підтримка типів даних
/**
* Create the column definition for a char type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeChar(Fluent $column) {
return "char({$column->length})";
}

/**
* Create the column definition for a string type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeString(Fluent $column) {
return "varchar({$column->length})";
}

/**
* Create the column definition for a text type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeText(Fluent $column) {
return 'BLOB SUB_TYPE TEXT';
}

/**
* Create the column definition for a medium text type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeMediumText(Fluent $column) {
return 'BLOB SUB_TYPE TEXT';
}

/**
* Create the column definition for a long text type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeLongText(Fluent $column) {
return 'BLOB SUB_TYPE TEXT';
}

/**
* Create the column definition for a integer type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeInteger(Fluent $column) {
return $column->autoIncrement ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
}

/**
* Create the column definition for a big integer type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeBigInteger(Fluent $column) {
return $column->autoIncrement ? 'BIGINT GENERATED BY DEFAULT AS IDENTITY' : 'BIGINT';
}

/**
* Create the column definition for a medium integer type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeMediumInteger(Fluent $column) {
return $column->autoIncrement ? 'INTEGER GENERATED BY DEFAULT AS IDENTITY' : 'INTEGER';
}

/**
* Create the column definition for a tiny integer type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTinyInteger(Fluent $column) {
return $column->autoIncrement ? 'SMALLINT GENERATED BY DEFAULT AS IDENTITY' : 'SMALLINT';
}

/**
* Create the column definition for a small integer type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeSmallInteger(Fluent $column) {
return $column->autoIncrement ? 'SMALLINT GENERATED BY DEFAULT AS IDENTITY' : 'SMALLINT';
}

/**
* Create the column definition for a float type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeFloat(Fluent $column) {
return $this->typeDouble($column);
}

/**
* Create the column definition for a double type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeDouble(Fluent $column) {
return 'double precision';
}

/**
* Create the column definition for a decimal type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeDecimal(Fluent $column) {
return "decimal({$column->total}, {$column->places})";
}

/**
* Create the column definition for a boolean type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeBoolean(Fluent $column) {
return 'boolean';
}

/**
* Create the column definition for an enum type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeEnum(Fluent $column) {
$allowed = array_map(function ($a) {
return "'" . $a . "'";
}, $column->allowed);

return "varchar(255) check (\"{$column->name}\" in (" . implode(', ', $allowed) . '))';
}

/**
* Create the column definition for a json type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeJson(Fluent $column) {
return 'varchar(8191)';
}

/**
* Create the column definition for a jsonb type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeJsonb(Fluent $column) {
return 'varchar(8191)';
}

/**
* Create the column definition for a date type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeDate(Fluent $column) {
return 'date';
}

/**
* Create the column definition for a date-time type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeDateTime(Fluent $column) {
return 'timestamp';
}

/**
* Create the column definition for a date-time type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeDateTimeTz(Fluent $column) {
return 'timestamp';
}

/**
* Create the column definition for a time type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTime(Fluent $column) {
return 'time';
}

/**
* Create the column definition for a time type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTimeTz(Fluent $column) {
return 'time';
}

/**
* Create the column definition for a timestamp type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTimestamp(Fluent $column) {
if ($column->useCurrent) {
return 'timestamp default CURRENT_TIMESTAMP';
}

return 'timestamp';
}

/**
* Create the column definition for a timestamp type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeTimestampTz(Fluent $column) {
if ($column->useCurrent) {
return 'timestamp default CURRENT_TIMESTAMP';
}

return 'timestamp';
}

/**
* Create the column definition for a binary type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeBinary(Fluent $column) {
return 'varchar(8191) CHARACTER SET OCTETS';
}

/**
* Create the column definition for a job type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeUuid(Fluent $column) {
return 'char(36)';
}

/**
* Create the column definition for an IP address type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeIpAddress(Fluent $column) {
return 'varchar(45)';
}

/**
* Create the column definition for a MAC address type.
*
* @param \Illuminate\Support\Fluent $column
* @return string
*/
protected function typeMacAddress(Fluent $column) {
return 'varchar(17)';
}

Зауваження про автоинкрементных стовпцях
Identity стовпці доступні починаючи з Firebird 3.0. До версії 3.0 для аналогічної функціональності застосовували створення генератора і INSERT BEFORE тригер. Приклад:

CREATE TABLE USERS (
ID INTEGER GENERATED BY DEFAULT AS IDENTITY,
...
);

Аналогічну функціональність можна отримати так:

CREATE TABLE USERS (
ID INTEGER,
...
);

CREATE SEQUENCE SEQ_USERS;

CREATE TRIGGER TR_USERS_BI FOR USERS
ACTIVE INSERT BEFORE
AS
BEGIN
IF (NEW.ID IS NULL) THEN
NEW.ID = NEXT VALUE FOR SEQ_USERS;
END

Міграції Laravel не підтримують ніяких об'єктів схеми крім таблиць. Тобто створення і модифікація послідовностей, а тим більше тригерів не підтримується. Тим не менш, послідовності є частиною функціоналу досить великої кількості СУБД, в тому числі Postgres і MS SQL (починаючи з 2012). Як додати підтримку послідовностей міграції Laravel буде описано пізніше в цій статті.
Тепер додамо два методи, які повертає запит для перевірки існування таблиці і стовпця усередині таблиці.

/**
* Compile the query to determine if a table exists.
*
* @return string
*/
public function compileTableExists() {
return 'select * from RDB$RELATIONS where RDB$RELATION_NAME = ?';
}


/**
* Compile the query to determine the list of columns.
*
* @param string $table
* @return string
*/
public function compileColumnExists($table) {
return "select TRIM(RDB\$FIELD_NAME) AS \"column_name\" from RDB\$RELATION_FIELDS where RDB\$RELATION_NAME = '$table'";
}

Додамо метод compileCreate призначений для створення оператора CREATE TABLE. Цей же метод використовується для створення тимчасових таблиць GTT. Досить дивно, але навіть для СКБД Postgres створюється тільки один тип GTT — ON COMMIT DELETE ROWS, ми ж реалізуємо підтримку відразу обох типів GTT.

/**
* Compile a create table command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileCreate(Blueprint $blueprint, Fluent $command) {
$columns = implode(', ', $this->getColumns($blueprint));

$sql = $blueprint->temporary ? 'create temporary' : 'create';

$sql .= 'table' . $this->wrapTable($blueprint) . " ($columns)";

if ($blueprint->temporary) {
if ($blueprint->preserve) {
$sql .= 'ON COMMIT DELETE ROWS';
} else {
$sql .= 'ON COMMIT PRESERVE ROWS';
}
}

return $sql;
}

Клас Illuminate\Database\Schema\Blueprint не містить властивості $preserve, тому додамо його, а так само метод для його установки. Клас Blueprint призначений для генерування запиту або набору запитів для підтримки створення, модифікації та видалення метаданих таблиці.

class Blueprint
{
// ............... Пропущено

/**
* Whether a temporary table such as ON COMMIT PRESERVE ROWS
* 
* @var bool 
*/
public $preserve = false;

// ............... Пропущено

/**
* Indicate that the temporary table as ON COMMIT PRESERVE ROWS.
* 
* @return void
*/
public function preserveRows() {
$this->preserve = true;
}

// ............... Пропущено

}

Повернемося до класу граматики Illuminate\Database\Schema\Grammars\FirebirdGrammar. Додамо в нього метод для створення оператора видалення таблиці.

/**
* Compile a drop table command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDrop(Blueprint $blueprint, Fluent $command) {
return 'drop table' . $this->wrapTable($blueprint);
}

У Laravel є ще один метод, який намагається видалити таблицю, якщо вона існує. Це робиться за допомогою SQL оператора DROP TABLE IF EXISTS. У Firebird немає оператора з подібним функціоналом, проте ми можемо емулювати за допомогою анонімного блоку (EXECUTE BLOCK + EXECUTE STATEMENT).

/**
* Compile a drop table (if exists) command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropIfExists(Blueprint $blueprint, Fluent $command) {
$sql = 'EXECUTE BLOCK' . "\n";
$sql .= 'AS' . "\n";
$sql .= 'BEGIN' . "\n";
$sql .= " IF (EXISTS(select * from RDB\$RELATIONS where RDB\$RELATION_NAME = '" . $blueprint->getTable() . "')) THEN" . "\n";
$sql .= " EXECUTE STATEMENT 'DROP TABLE " . $this->wrapTable($blueprint) . "';" . "\n";
$sql .= 'END';
return $sql;
}

Тепер додамо методи для додавання і видалення стовпців, обмежень та індексів.

Методи для додавання і видалення стовпців, обмежень та індексів
/**
* Compile a column addition command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileAdd(Blueprint $blueprint, Fluent $command) {
$table = $this->wrapTable($blueprint);

$columns = $this->prefixArray('add column', $this->getColumns($blueprint));

return 'alter table' . $table . '' . implode(', ', $columns);
}

/**
* Compile a primary key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compilePrimary(Blueprint $blueprint, Fluent $command) {
$columns = $this->columnize($command->columns);

return 'alter table' . $this->wrapTable($blueprint) . " add primary key ({$columns})";
}

/**
* Compile a unique key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileUnique(Blueprint $blueprint, Fluent $command) {
$table = $this->wrapTable($blueprint);

$index = $this->wrap($command->index);

$columns = $this->columnize($command->columns);

return "alter table $table add constraint {$index} unique ($columns)";
}

/**
* Compile a plain index key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileIndex(Blueprint $blueprint, Fluent $command) {
$columns = $this->columnize($command->columns);

$index = $this->wrap($command->index);

return "create index {$index} on " . $this->wrapTable($blueprint) . " ({$columns})";
}

/**
* Compile a drop column command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropColumn(Blueprint $blueprint, Fluent $command) {
$columns = $this->prefixArray('drop column', $this->wrapArray($command->columns));

$table = $this->wrapTable($blueprint);

return 'alter table' . $table . '' . implode(', ', $columns);
}

/**
* Compile a drop primary key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropPrimary(Blueprint $blueprint, Fluent $command) {
$table = $blueprint->getTable();

$index = $this->wrap("{$table}_pkey");

return 'alter table' . $this->wrapTable($blueprint) . " drop constraint {$index}";
}

/**
* Compile a drop unique key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropUnique(Blueprint $blueprint, Fluent $command) {
$table = $this->wrapTable($blueprint);

$index = $this->wrap($command->index);

return "alter table {$table} drop constraint {$index}";
}

/**
* Compile a drop index command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropIndex(Blueprint $blueprint, Fluent $command){
$index = $this->wrap($command->index);

return "drop index {$index}";
}

/**
* Compile a drop foreign key command.
*
* @param \Illuminate\Database\Schema\Blueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropForeign(Blueprint $blueprint, Fluent $command) {
$table = $this->wrapTable($blueprint);

$index = $this->wrap($command->index);

return "alter table {$table} drop constraint {$index}";
}


Ви можете перевірити працездатність наших класів, створивши і запустивши міграцію з наступним вмістом:

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::drop('users');
}
}

У результаті запуску за допомогою команди:

php artisan migrate

буде створена таблиця з наступним DDL:

CREATE TABLE "users" (
"id" INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
"name" VARCHAR(255) NOT NULL,
"email" VARCHAR(255) NOT NULL,
"password" VARCHAR(255) NOT NULL,
"remember_token" VARCHAR(100),
"created_at" TIMESTAMP,
"updated_at" TIMESTAMP
);

ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE ("email");

Відкотити міграцію можна за допомогою команди:

php artisan migrate:reset

Додавання підтримки послідовностей
За аналогією з класом Blueprint, призначеним для генерування запиту або набору запитів для підтримки створення, модифікації та видалення метаданих таблиці, створимо клас SequenceBlueprint для підтримки тих же дій для послідовностей. Цей клас досить простий, приводити цілком я його не буду, тільки основні відмінності від аналогічного класу Blueprint.

Послідовності в Firebird мають наступні атрибути: ім'я послідовності, початкове значення і інкремент, який використовується оператором NEXT VALUE FOR. Останній атрибут доступний починаючи з версії 3.0. Таким чином, наш клас буде містити наступні властивості:

/**
* The sequence the blueprint describes.
*
* @var string
*/
protected $sequence; 


/**
* Initial sequence value
* 
* @var int 
*/
protected $start_with = 0;

/**
* Increment for sequence
* 
* @var int 
*/
protected $increment = 1;

/**
* Restart flag that indicates that the sequence should be reset
* 
* @var bool 
*/
protected $restart = false;

Методи для отримання значень і установки цих властивостей елементарні, тому ми не будемо приводити їх. Наведу лише метод restart, який буде використаний при генерації пропозиції RESTART оператора ALTER SEQUENCE.

/**
* Restart sequence and set initial value
* 
* @param int $startWith
*/
public function restart($startWith = null) {
$this->restart = true;
$this->start_with = $startWith;
}

За аналогією з класом Blueprint якщо не надано команди create або drop, то виконується команда alter sequence.

/**
* Determine if the blueprint has a create command.
*
* @return bool
*/
protected function creating()
{
foreach ($this->commands as $command) {
if ($command->name == 'createSequence') {
return true;
}
}

return false;
} 

/**
* Determine if the blueprint has a drop command.
*
* @return bool
*/
protected function dropping()
{
foreach ($this->commands as $command) {
if ($command->name == 'dropSequence') {
return true;
}
if ($command->name == 'dropSequenceIfExists') {
return true;
} 
}

return false;
} 

/**
* Add the commands that are implied by the blueprint.
*
* @return void
*/
protected function addImpliedCommands()
{
if (($this->restart || ($this->increment !== 1)) && 
! $this->creating() &&
! $this->dropping()) {
array_unshift($this->commands, $this->createCommand('alterSequence'));
}
} 

/**
* Get the raw SQL statements for the blueprint.
*
* @param \Illuminate\Database\Connection $connection
* @param \Illuminate\Database\Schema\Grammars\Grammar $grammar
* @return array
*/
public function toSql(Connection $connection, Grammar $grammar)
{
$this->addImpliedCommands();

$statements = [];

// Each type of command has a corresponding compiler function on the schema
// grammar which is used to build the necessary SQL statements to build
// the sequence blueprint element, so we'll just call that компілятори function.
foreach ($this->commands as $command) {
$method = 'compile'.ucfirst($command->name);

if (method_exists($grammar, $method)) {
if (! is_null($sql = $grammar->$method($this, $command, $connection))) {
$statements = array_merge($statements, (array) $sql);
}
}
}

return $statements;
}

Повний код класу Illuminate\Database\Schema\SequenceBlueprint ви можете знайти у вихідних текстах доданих до статті.

Тепер настав час повернутися до класу граматики Illuminate\Database\Schema\Grammars\ FirebirdGrammar і додати в нього методи для створення операторів {CREATE | ALTER | DROP} SEQUENCE.

Підтримка операторів {CREATE | ALTER | DROP} SEQUENCE
/**
* Compile create a sequence command.
*
* @param \Illuminate\Database\Schema\SequenceBlueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileCreateSequence(SequenceBlueprint $blueprint, Fluent $command) {
$sql = 'create sequence ';
$sql .= $this->wrapSequence($blueprint);
if ($blueprint->getInitialValue() !== 0) {
$sql .= 'start with' . $blueprint->getInitialValue();
}
if ($blueprint->getIncrement() !== 1) {
$sql .= 'increment by' . $blueprint->getIncrement();
}
return $sql;
}

/**
* Compile a alter sequence command.
*
* @param \Illuminate\Database\Schema\SequenceBlueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileAlterSequence(SequenceBlueprint $blueprint, Fluent $command) {
$sql = 'alter sequence ';
$sql .= $this->wrapSequence($blueprint);
if ($blueprint->isRestart()) {
$sql .= 'restart';
if ($blueprint->getInitialValue() !== null) {
$sql .= 'with' . $blueprint->getInitialValue();
}
}
if ($blueprint->getIncrement() !== 1) {
$sql .= 'increment by' . $blueprint->getIncrement();
}
return $sql;
}

/**
* Compile a drop sequence command.
* 
* @param \Illuminate\Database\Schema\SequenceBlueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropSequence(SequenceBlueprint $blueprint, Fluent $command) {
return 'drop sequence' . $this->wrapSequence($blueprint);
}

/**
* Compile a drop sequence command.
* 
* @param \Illuminate\Database\Schema\SequenceBlueprint $blueprint
* @param \Illuminate\Support\Fluent $command
* @return string
*/
public function compileDropSequenceIfExists(SequenceBlueprint $blueprint, Fluent $command){
$sql = 'EXECUTE BLOCK' . "\n";
$sql .= 'AS' . "\n";
$sql .= 'BEGIN' . "\n";
$sql .= " IF (EXISTS(select * from RDB\$GENERATORS where RDB\$GENERATOR_NAME = '" . $blueprint->getSequence() . "')) THEN" . "\n";
$sql .= " EXECUTE STATEMENT 'DROP SEQUENCE " . $this->wrapSequence($blueprint) . "';" . "\n";
$sql .= 'END';
return $sql;
}

/**
* Wrap a sequence in keyword identifiers.
*
* @param mixed $sequence
* @return string
*/
public function wrapSequence($sequence) {
if ($sequence instanceof SequenceBlueprint) {
$sequence = $sequence->getSequence();
}

if ($this->isExpression($sequence)) {
return $this->getValue($sequence);
}

return $this->wrap($this->tablePrefix . $sequence, true);
}


Ну ось, тепер додавання підтримки послідовностей міграції Laravel завершена. Давайте подивимося, як це працює, для цього злегка модифікуємо міграцію наведену раніше.

Міграція для тестування DDL над послідовностями
class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// виконає оператор 
// CREATE SEQUENCE "seq_users_id"
Schema::createSequence('seq_users_id');
// виконає оператори
// CREATE TABLE "users" (
// "id" INTEGER NOT NULL,
// "name" VARCHAR(255) NOT NULL,
// "email" VARCHAR(255) NOT NULL,
// "password" VARCHAR(255) NOT NULL,
// "remember_token" VARCHAR(100),
// "created_at" TIMESTAMP,
// "updated_at" TIMESTAMP
// );
// ALTER TABLE "users" ADD PRIMARY KEY ("id");
// ALTER TABLE "users" ADD CONSTRAINT "users_email_unique" UNIQUE ("email");
Schema::create('users', function (Blueprint $table) {
//$table->increments('id');
$table->integer('id')->primary();
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
// виконає оператор
// ALTER SEQUENCE "seq_users_id" RESTART WITH 10 INCREMENT BY 5
Schema::sequence('seq_users_id', function (SequenceBlueprint $sequence) {
$sequence->increment(5);
$sequence->restart(10);
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
// виконає оператор 
// DROP SEQUENCE "seq_users_id"
Schema::dropSequence('seq_users_id');
// виконає оператор 
// DROP TABLE "users" 
Schema::drop('users');
}
}


Можна піти далі і зробити підтримку створення INSERT BEFORE тригера і послідовності (генератора) для підтримки автоинкрементных полів в Firebird 2.5 і нижче. Але в більшості випадків достатньо отримувати таке значення послідовності і передавати її в INSERT запит.

Висновок
Наведених змін достатньо для розробки веб додатків з використанням СУБД Firebird з використання фреймворку Laravel. Звичайно, було б добре оформити це як пакет і підключати його для розширення функціоналу, так як це робиться з іншими модулями Laravel. Однак розробники фреймворку явно не розраховували на те, що кількість підтримуваних СУБД може розширюватися за рахунок сторонніх пакетів. Про це говорить те, що в класі фабрик підключень Illuminate\Database\Connectors\ConnectionFactory імена класів підключень перераховані явно. Таким чином, ті класи, які там не вказані явно, не можуть бути створені, а це призводить до того, що необхідно правити даний клас, який є частиною ядра Laravel.

У наступній статті я розповім про те, як створити невеликий додаток з використанням Laravel і СУБД Firebird. Якщо вас зацікавила інтеграція Firebird в фреймворк Laravel або ви знайшли помилку, пишіть в лічку обов'язково відповім. Змінені і додані до Laravel файли для підтримки Firebird ви можете завантажити звідси.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.