LaravelでFirebird DBMSを操作するためのパッケージ

firebird-logo こんにちはHabr! 前の記事で、 FirebirdサポートをLaravelに追加する方法について説明しました。 当時、私はjacquestvanzuydam / laravel-firebirdパッケージの存在を知らず、Firebirdサポートをゼロから追加しました。 これは、Laravelコアファイルを修正することで行われました。 jacquestvanzuydam / laravel-firebirdパッケージを見て、その機能が私に合わないことに気づき、拡張することにしました。



この記事では、 sim1984 / laravel-firebirdパッケージとjacquestvanzuydam / laravel-firebirdパッケージの主な機能の違いについて説明します。



自動インクリメント列のサポート



元のパッケージの最も重要な欠点は、移行時に自動インクリメント列がサポートされていないことです。 私のパッケージでは、自動インクリメント列のサポートは2つの方法で実装されています。 最初の方法は、Firebirdの古典的な方法です。 このメソッドでは、自動インクリメント列を作成するときに、シーケンス(ジェネレーター)とBEFORE INSERTトリガーが自動的に作成されます。 次のPHPスクリプト



Schema::create('users', function (Blueprint $table) { $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });
      
      





次のSQLステートメントを生成して実行します



  CREATE TABLE "users" ( "id" INTEGER NOT NULL 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"); CREATE SEQUENCE "seq_users"; CREATE OR ALTER TRIGGER "tr_users_bi" FOR "users" ACTIVE BEFORE INSERT AS BEGIN IF (NEW."id" IS NULL) THEN NEW."id" = NEXT VALUE FOR "seq_users"; END
      
      





2番目の方法は、Firebird 3.0以降で機能します。 この場合、シーケンスとトリガーの代わりに、IDENTITYフィールドが使用されます。 次のPHPスクリプト



  Schema::create('users', function (Blueprint $table) { $table->useIdentity(); // only Firebird 3.0 $table->increments('id'); $table->string('name'); $table->string('email')->unique(); $table->string('password'); $table->rememberToken(); $table->timestamps(); });
      
      





以下のSQLステートメントを生成して実行します。



  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");
      
      





INSERTサポート... RETURNING



Firebird \ Schema \ Grammars \ FirebirdGrammar文法は、compileInsertGetIdメソッドで拡張されています。このメソッドは、追加された行の戻り識別子でINSERT要求を作成することを目的としています。



  /** * Compile an insert 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); }
      
      





Fireird 3.0では、INSERT ... RETURNINGがPDOドライバーを介して機能しないことが判明しました。 以前は、これはhttps://bugs.php.net/bug.php?id=72931として現れました。 最新のスナップショットでは、動作が変更され、ドライバーはエラーSQLSTATE [HY000]をスローします:一般エラー:-502カーソルが開いていません。 明らかに、PDOはカーソルからのみデータを返すことができます(間接的に、PDOStatement :: fetch()メソッドはこれを示唆しています)。 興味深いことに、これはfirebird 2.5で機能しました。 そのため、互換性があるはずのAPIのどこかに、パフォーマンスに影響する変更がありました。



カーソルが返されるようにリクエストを処理してPDOをだましてみることにしました。 これを行うには、INSERT ... RETURNINGステートメントを匿名PSQLブロック(EXECUTE BLOCK)でラップします。 不快な機能が1つあります。 実際、名前付きパラメーターをサポートするために、PDOはVARNAMEの形式のすべての変数を「?」に置き換えます。 これは、匿名ブロックの本体のコンテンツを台無しにします。 このような置換は、EXECUTE BLOCKキーワードとASキーワードの間でのみ行われた場合、正しく機能します。 別の方法は、他のアクセスコンポーネントで行われているように、変数トークンを置き換えることです。 残念ながら、PDOは変数マーカーを変更できません。 したがって、ブロック本体内の「:」記号を回避する方法を探す必要がありました。 これはFirebird 3.0でのみ行う必要があるため、FirebirdGrammar30文法を別に割り当て、Firebirdのバージョンを判別する特別な方法を追加しました。 さらに、個別の文法により、Firebird 3.0の新機能をより適切に使用できます。 INSERT ... RETURNINGのバグを修正するコードを提供します



  /** * Fix PDO driver bug for 'INSERT ... RETURNING' * See https://bugs.php.net/bug.php?id=72931 * Reproduced in Firebird 3.0 only * Remove when the bug is fixed! * * @param \Illuminate\Database\Query\Builder $query * @param array $values * @param string $sequence * @param string $sql */ private function fixInsertReturningBug(Builder $query, $values, $sequence, $sql) { /* * Since the PDO Firebird driver bug because of which is not executed * sql query 'INSERT ... RETURNING', then we wrap the statement in * the block and execute it. PDO may not recognize the colon (:) within * a block properly, so we will not use it. The only way I found * buyout perform a query via EXECUTE STATEMENT. */ if (!is_array(reset($values))) { $values = [$values]; } $table = $this->wrapTable($query->from); $columns = array_map([$this, 'wrap'], array_keys(reset($values))); $columnsWithTypeOf = []; foreach ($columns as $column) { $columnsWithTypeOf[] = " {$column} TYPE OF COLUMN {$table}.{$column} = ?"; } $ret_column = $this->wrap($sequence); $columns_str = $this->columnize(array_keys(reset($values))); $new_sql = "EXECUTE BLOCK (\n"; $new_sql .= implode(",\n", $columnsWithTypeOf); $new_sql .= ")\n"; $new_sql .= "RETURNS ({$ret_column} TYPE OF COLUMN {$table}.{$ret_column})\n"; $new_sql .= "AS\n"; $new_sql .= " DECLARE STMT VARCHAR(8191);\n"; $new_sql .= "BEGIN\n"; $new_sql .= " STMT = '{$sql}';\n"; $new_sql .= " EXECUTE STATEMENT (STMT) ({$columns_str})\n"; if (!$query->getConnection()->getPdo()->inTransaction()) { // For some unknown reason, there is a ROLLBACK. Probably due to the COMMIT RETAINING. $new_sql .= " WITH AUTONOMOUS TRANSACTION\n"; } $new_sql .= " INTO {$ret_column};\n"; $new_sql .= " SUSPEND;\n"; $new_sql .= "END"; return $new_sql; } /** * Compile an insert 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) { $sql = parent::compileInsertGetId($query, $values, $sequence); // Fix PDO driver bug for 'INSERT ... RETURNING' // See https://bugs.php.net/bug.php?id=72931 $sql = $this->fixInsertReturningBug($query, $values, $sequence, $sql); return $sql; }
      
      





発言



この方法は絶対に嫌いです。 将来バグが修正され、この一時的な解決策が削除されることを願っています。

シーケンスを操作する



Laravelの移行ではなく、生成されたシーケンスを使用する必要がある場合があります。たとえば、既製のデータベースを使用できます。 場合によっては、同じシーケンスを複数のテーブルで使用できます。 拡張Firebird \ Eloquent \ Modelを使用して、モデルで任意のシーケンス名を使用できます。 このモデルには、必要なシーケンスの名前を保持する$ sequenceという追加のプロパティがあります。



例:



 use Firebird\Eloquent\Model; class Customer extends Model { /** *     * @var string */ protected $table = 'CUSTOMER'; /** *    * @var string */ protected $primaryKey = 'CUSTOMER_ID'; /** * Indicates if the model should be timestamped. * @var bool */ public $timestamps = false; /** *       * @var string */ protected $sequence = 'GEN_CUSTOMER_ID'; }
      
      





基本モデルでは、insertAndSetIdメソッドが再定義され、INSERT ... RETURNINGは使用されませんが、次のシーケンス番号を受け取り、通常のINSERT要求で使用します。 このモデルを使用すると、Firebird 3.0であまり美しくないINSERT ... RETURNINGソリューションを使用しないこともできます。



前に言ったように、Firebirdではシーケンスは完全に独立したメタデータオブジェクトなので、Laravelの移行を通じてそれらを管理できると便利です。 これを行うために、Firebird \ Schema \ SequenceBlueprintクラスが作成されました。 このような移行の例でどのように機能するかを見てみましょう。



 <?php use Firebird\Schema\Blueprint; use Firebird\Schema\SequenceBlueprint; use Illuminate\Database\Migrations\Migration; class CreateUsersTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::createSequence('seq_users_id'); Schema::create('users', function (Blueprint $table) { $table->integer('id')->primary(); $table->string('name'); $table->string('email')->unique(); $table->string('password'); $table->rememberToken(); $table->timestamps(); }); Schema::sequence('seq_users_id', function (SequenceBlueprint $sequence) { $sequence->increment(5); $sequence->restart(10); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropSequence('seq_users_id'); Schema::drop('users'); } }
      
      





このような移行をロールバックすると、次のSQLステートメントが実行されます。



 CREATE SEQUENCE "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"); ALTER SEQUENCE "seq_users_id" RESTART WITH 10 INCREMENT BY 5;
      
      





ロールバック移行では、次のステートメントが実行されます。



 DROP SEQUENCE "seq_users_id"; DROP TABLE "users";
      
      





高度な構成オプション



接続を構成するための2つの追加構成パラメーターがパッケージに追加されます。





例:



  'connections' => [ 'firebird' => [ 'driver' => 'firebird', 'host' => env('DB_HOST', 'localhost'), 'port' => env('DB_PORT', '3050'), 'database' => env('DB_DATABASE', 'examples'), 'username' => env('DB_USERNAME', 'BOSS'), 'password' => env('DB_PASSWORD', 'qw897tr'), 'role' => 'RDB$ADMIN', 'charset' => env('DB_CHARSET', 'UTF8'), 'engine_version' => '3.0.0', ], ],
      
      





composerによるインストールの機能



私のパッケージはjacquestvanzuydam / laravel-firebirdパッケージのフォークであるため、そのインストールは少し異なります。 元のパッケージのインストールと同様に、composer.jsonでdevと等しい最小安定性パラメーターを設定することを忘れないでください。 次に、リポジトリにリンクを追加する必要があります。



  "repositories": [ { "type": "package", "package": { "version": "dev-master", "name": "sim1984/laravel-firebird", "source": { "url": "https://github.com/sim1984/laravel-firebird", "type": "git", "reference": "master" }, "autoload": { "classmap": [""] } } } ],
      
      





次に、requireパラメーターに次の行を追加します。



 "sim1984/laravel-firebird": "dev-master"
      
      





おわりに



Firebird DBMSを使用してアプリケーションを開発するのに十分な機能が私のパッケージにあることを願っています。 sim1984 / laravel-firebirdパッケージを改善するための質問や提案があれば、 個人のメールに書いてください。私は間違いなく答えます。 次の記事では、LaravelとFirebird DBMSを使用して小さなアプリケーションを作成する方法について説明します。



All Articles