はじめに
Laravelのテストでsqliteのメモリ内データベースを使うと、途中でテーブルが存在しないというエラーがでる問題をなんとかしようとして、LumenでsqliteのURI Filenamesを使いたいでは、PDOがそもそもURI filenamesをサポートしてないというオチで玉砕しましたが、やっぱりテストは速い方がいいのでもう少し頑張ってなんとかならないか調べて見ました。
どうしてテーブルが存在しないエラーが出るのか
Laravelは基本的にはデータベースへの接続は使い回してくれるはずで、なぜ新しい接続が作られるのかをちゃんと調べて見ます。
すると、テスト中でrefreshApplication
を行った後にエラーが出る(=新しい接続がつくられる)ことがわかりました。
つまり以下のようなことが起きたと考えられます
-
refreshApplication
が実行されたので、サービスコンテナは空の状態から再出発 - 作成済みのデータベース接続を管理する
\Illuminate\Database\DatabaseManager
もシングルトンとしてサービスコンテナに登録されているが、これに伴って作り直し - 既にあったsqliteへの接続は引き継がれていないので次に必要になったタイミングで新たに接続される
わかってみれば当たり前です。普通はDBへの新しい接続がつくられるだけでDBの中身はかわらないから問題がないですが、:memory:
の時には空になってしまうからエラーになるわけです。
対策
いろいろな対処方法があるとは思いますが、ここでは一度作ったin memoryデータベースに接続しているPDOをなんとかして使い回せばいいだろうと考えて、SQLiteConnectorを改造するパターンで行きます。
:shared-memory:
という特別な名前を用意して、これが指定されたときは最初に作ったPDOを保持して以後それを使い回すようにします。
作成した\App\Database\SqliteConnector
です。
<?php
namespace App\Database;
class SqliteConnector extends \Illuminate\Database\Connectors\SQLiteConnector
{
protected static $shared_memory_connection = null;
/**
* Establish a database connection.
*
* databaseが:shared-memory:のときは作成したPDOを保存して以後はそれを返すようにする
*
* @param array $config
* @return \PDO
*
* @throws \InvalidArgumentException
*/
public function connect(array $config)
{
if ($config['database'] === ':shared-memory:') {
if (self::$shared_memory_connection === null) {
$options = $this->getOptions($config);
self::$shared_memory_connection = $this->createConnection('sqlite::memory:', $config, $options);
}
return self::$shared_memory_connection;
}
return parent::connect($config);
}
}
あとはAppServiceProvider
のregister
メソッドで登録してやります。
public function register()
{
$this->app->bind('db.connector.sqlite', SqliteConnector::class);
}
.env.testing
で新たに使えるようになった:shared-memory:
を指定します。
DB_CONNECTION=sqlite
DB_DATABASE=:shared-memory:
おわりに
テストの実行時間はこんな感じです。Docker for macなのでファイルアクセスが遅いのですが、やはりメモリは速いです。
使用したDB | 実行時間 |
---|---|
MySQL | 74.1sec |
SQLite(ファイル) | 23.4sec |
SQLite(メモリ) | 7.3sec |
refreshApplication
は影響が大きいので使うのはどうかというのはありますが、追加したモックをバッサリ戻すのに便利なんですよね。