INDEX
Laravelを知らない中級(中年)プログラマーがマイグレーションファイルの仕組みを調べてみたけど全くわからない!
- その1「マイグレーションファイルを見てみよう」
- その2「$app['db']って何者?」
- その3「Repositoryクラス」
- その4「Larabelアプリケーションの初期化の流れ」
- その5「リポジトリの読込」
- その6「データベース接続」
- その7「スキーマビルダー」
- その8「migrate コマンドの実行」
- その9「Kernel::handle()」
データベース接続
前回はリポジトリの読込を追いました。今日はデータベースとの接続を読んでみようと思います。
PROJECT_ROOT/config/app.php
のproviders
には、Illuminate\Database\DatabaseServiceProvider
が記述されています。このプロバイダーが初期化時に読み込まれ、register()
メソッドがコールされます。
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
Model::clearBootedModels();
$this->registerConnectionServices();
$this->registerEloquentFactory();
$this->registerQueueableEntityResolver();
}
/**
* Register the primary database bindings.
*
* @return void
*/
protected function registerConnectionServices()
{
// The connection factory is used to create the actual connection instances on
// the database. We will inject the factory into the manager so that it may
// make the connections while they are actually needed and not of before.
$this->app->singleton('db.factory', function ($app) {
return new ConnectionFactory($app);
});
// The database manager is used to resolve various connections, since multiple
// connections might be managed. It also implements the connection resolver
// interface which may be used by other components requiring connections.
$this->app->singleton('db', function ($app) {
return new DatabaseManager($app, $app['db.factory']);
});
$this->app->bind('db.connection', function ($app) {
return $app['db']->connection();
});
}
register()
メソッドから、registerConnectionServices()
メソッドがコールされ、そこで以下がシングルトン結合及びバインドされます。
- db.factory
- db
- db.connection
まず、db.factory
としてIllumination/Database/Connectors/ConnectionFactory
をシングルトン結合します。次にdb
として、Illumination/Database/DatabaseManager
を先程結合したdb.factory
を引数として渡して生成しシングルトン結合します。最後にdb.connection
として先程生成した$app['db']
のconnection()
メソッドを使い生成した接続をバインドします。
これでいよいよデータベース接続の処理に入ります。
DatabaseManager::makeConnection()
メソッドの戻り値のreturn $this->factory->make($config, $name)
のfactory
の正体は、$app['db.factory']
つまり、アプリケーションコンテナを引数として生成したIllumination/Database/Connectors/ConnectionFactory
ということですね。では、ConnectionFactory::make()
メソッドを考察します。なるべく実行される順番通りに追いかけていきます。
ConnectionFactory::make() の処理の流れ
/**
* Establish a PDO connection based on the configuration.
*
* @param array $config
* @param string|null $name
* @return \Illuminate\Database\Connection
*/
public function make(array $config, $name = null)
{
$config = $this->parseConfig($config, $name);
if (isset($config['read'])) {
return $this->createReadWriteConnection($config);
}
return $this->createSingleConnection($config);
}
/**
* Parse and prepare the database configuration.
*
* @param array $config
* @param string $name
* @return array
*/
protected function parseConfig(array $config, $name)
{
return Arr::add(Arr::add($config, 'prefix', ''), 'name', $name);
}
ConnectionFactory::make()
第一引数に接続情報配列を受け取っています。
第二引数にストリング型のデータベース名を受け取ります。デフォルトはnull
です。
戻り値はConnection
インスタンスです。
make()
の処理のはじめに、$config
に parseConfig()
メソッドの戻り値を入れています。parseConfig
は受け取った接続情報にprefix
に空文字を、name
に接続名を加えて戻しています。次に接続情報にread
が含まれているか判定し、含まれていた場合はcreateReadWriteConnection()
メソッドを、含まれない場合はcreateSingleConnection()
メソッドをコールしたものを戻します。
流れを追って一つずつメソッドを追っていきましょう。
/**
* Create a single database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createReadWriteConnection(array $config)
{
$connection = $this->createSingleConnection($this->getWriteConfig($config));
return $connection->setReadPdo($this->createReadPdo($config));
}
ConnectionFactory::createReadWriteConnection()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はConnection
インスタンスです。
受け取った接続情報を引数に、getWriteConfig()
をコールし、戻り値を引数にcreateSingleConnection()
をコールし、戻り値のConnection
インスタンスに対してcreateReadPdo()
に接続情報を引数としてコールした戻り値を引数にsetReadPdo()
メソッドをコールした戻り値を返すというのがこのメソッドの流れです。
/**
* Get the read configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getWriteConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'write')
);
}
ConnectionFactory::getWriteConfig()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値は整形したデータベース接続情報の配列です。
接続情報を受け取り、getReadWriteConfig()
メソッドの戻り値の配列をmergeReadWriteConfig()
でマージして戻しています。
/**
* Get a read / write level configuration.
*
* @param array $config
* @param string $type
* @return array
*/
protected function getReadWriteConfig(array $config, $type)
{
return isset($config[$type][0])
? Arr::random($config[$type])
: $config[$type];
}
ConnectionFactory::getReadWriteConfig()
第一引数は
$config
としてデータベース接続情報を配列型で受け取っています。
第二引数はストリング型で接続タイプを受け取っています。今回は 「write
」 が入ります。
戻り値は整形したデータベース接続情報の配列です。
接続情報配列の接続タイプキー(今回は「write
」) の0番目があるか調べています。存在していた場合はArr::random()
をstatic
でコールしています。これは、「write
」キーが配列である、つまり書き込み用接続が複数ある仕様の時の処理だと思われます。Arr::random()
はPHP
のarray_rand
を強化したstatic
メソッドで、指定した配列から指定した数をランダム取得するものです。ソースはとてもかんたんですので興味があればIlluminate/Support/Arr.php
を参照してみると良いと思います。「write
」キーの0番目が存在しない場合、つまり接続情報が1つの場合は「write
」キーをそのまま返します。
/**
* Merge a configuration for a read / write connection.
*
* @param array $config
* @param array $merge
* @return array
*/
protected function mergeReadWriteConfig(array $config, array $merge)
{
return Arr::except(array_merge($config, $merge), ['read', 'write']);
}
ConnectionFactory::mergeReadWriteConfig()
第一引数に接続情報配列を受け取っています。
第二引数にマージする配列を受け取っています。
戻り値は整形したデータベース接続情報の配列です。
引数として受け取った配列を、array_merge
したものを第一引数に、第二引数に ['read
', 'write
'] としてArr::except()
をコールしています。Arr::except()
は第一引数で渡された配列を対象に、第二引数として渡された配列で指定されたキーの値を全て取り除いた結果を配列として返します。つまり、['read
', 'write
'] の情報を削除したものを戻しています。
まだ読んでいる途中ですので、確証はありませんが、読み込みと書き込みの対象データベースが違う場合は、タイプ名をキーにして接続情報を保持していると思われます。接続時にそれを取得し、必要のない接続情報を削除し、接続に必要な情報のみ配列に直に代入するという処理なのだと思われます。書き込み、読み込みの接続は複数設定することが出来、接続ごとにランダムに対象が切り替わるのでしょう。
/**
* Create a single database connection instance.
*
* @param array $config
* @return \Illuminate\Database\Connection
*/
protected function createSingleConnection(array $config)
{
$pdo = $this->createPdoResolver($config);
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
}
ConnectionFactory::createSingleConnection()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はIlluminate\Database\Connection
型です。
createPdoResolver()
メソッドでPDO接続用Connector
インスタンスを生成し、createConnection()
メソッドで接続を確立したConnection
インスタンスを戻します。
/**
* Create a new Closure that resolves to a PDO instance.
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolver(array $config)
{
return array_key_exists('host', $config)
? $this->createPdoResolverWithHosts($config)
: $this->createPdoResolverWithoutHosts($config);
}
ConnectionFactory::createPdoResolver()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はクロージャー型です。
受け取った接続情報配列のキーに「host
」が存在するか検証し、存在する場合はcreatePdoResolverWithHosts()
メソッド、存在しない場合はcreatePdoResolverWithoutHosts
メソッドの戻り値のクロージャーを返します。
通常は「host
」情報はあると思いますが、SQLite
などの場合かもしれません。
/**
* Create a new Closure that resolves to a PDO instance with a specific host or an array of hosts.
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolverWithHosts(array $config)
{
return function () use ($config) {
foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {
$config['host'] = $host;
try {
return $this->createConnector($config)->connect($config);
} catch (PDOException $e) {
continue;
}
}
throw $e;
};
}
/**
* Parse the hosts configuration item into an array.
*
* @param array $config
* @return array
*
* @throws \InvalidArgumentException
*/
protected function parseHosts(array $config)
{
$hosts = Arr::wrap($config['host']);
if (empty($hosts)) {
throw new InvalidArgumentException('Database hosts array is empty.');
}
return $hosts;
}
ConnectionFactory::createPdoResolverWithHosts()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はクロージャー型です。
受け取った配列情報をparseHosts()
を通してArr::wrap()
で配列型にキャストしたものをArr::shuffle()
で配列をシャッフルしたものをforeach
で回し、接続情報の 「host
」 に代入し、createConnector()->connect()
で接続の確立をTRY
したものを戻すというクロージャーを戻します。接続が確立できなかった場合はPDOException
をスローします。foreach
で回していますが、接続が成功したらreturn
、失敗したら例外をスローするので1回しか回らないように見えます。Arr
クラスの静的メソッドはソースはとてもかんたんですので興味があればIlluminate/Support/Arr.php
を参照してみると良いと思います。
これもホスト情報が配列で複数あった場合の処理と推測できます。
/**
* Create a new Closure that resolves to a PDO instance where there is no configured host.
*
* @param array $config
* @return \Closure
*/
protected function createPdoResolverWithoutHosts(array $config)
{
return function () use ($config) {
return $this->createConnector($config)->connect($config);
};
}
ConnectionFactory::createPdoResolverWithoutHosts()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はクロージャーです。
接続情報に 「host
」が設定されていなかった場合、受け取った接続情報を引数にcreateConnector()
メソッドをコールし、戻ってきたConnecter
インスタンスに接続情報を引数にconnect()
メソッドをコールし戻ってきたPDO
インスタンスを返すクロージャーを戻します。
/**
* Create a connector instance based on the configuration.
*
* @param array $config
* @return \Illuminate\Database\Connectors\ConnectorInterface
*
* @throws \InvalidArgumentException
*/
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;
}
throw new InvalidArgumentException("Unsupported driver [{$config['driver']}]");
}
ConnectionFactory::createConnector()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はConnectorInterface
インターフェースを実装したインスタンスです。
受け取った接続情報配列にキーが「driver
」の情報が存在しなければ「A driver must be specified.
」というメッセージを添えて例外InvalidArgumentException
をスローします。
アプリケーションコンテナに接続ドライバがバインドされている場合はそれを返します。
接続ドライバの設定に合わせて適切なデータベース用のドライバを戻します。デフォルトではMySqlConnector
です。
/**
* Establish a database connection.
*
* @param array $config
* @return \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);
if (! empty($config['database'])) {
$connection->exec("use `{$config['database']}`;");
}
$this->configureEncoding($connection, $config);
// Next, we will check to see if a timezone has been specified in this config
// and if it has we will issue a statement to modify the timezone with the
// database. Setting this DB timezone is an optional configuration item.
$this->configureTimezone($connection, $config);
$this->setModes($connection, $config);
return $connection;
}
MySqlConnector::connect()
引数は$config としてデータベース接続情報を配列型で受け取っています。
戻り値はPDO インスタンスです。
各データベース用のドライバが用意されています。デフォルトのMySQL
用のものを見てみましょう。範囲が広くなりすぎるので、詳細は別の機会に追いたいと思いますので、ざっと見てみます。
DSN
とオプションをセットし接続情報と合わせて、Connection::createConnection()
でPDO
インスタンスを生成します。
設定情報にデータベース名があればそれを使うように設定します。
エンコードとタイムゾーンとモードを設定してて整えたインスタンスを戻します。
/**
* Create a new PDO connection.
*
* @param string $dsn
* @param array $config
* @param array $options
* @return \PDO
*
* @throws \Exception
*/
public function createConnection($dsn, array $config, array $options)
{
[$username, $password] = [
$config['username'] ?? null, $config['password'] ?? null,
];
try {
return $this->createPdoConnection(
$dsn, $username, $password, $options
);
} catch (Exception $e) {
return $this->tryAgainIfCausedByLostConnection(
$e, $dsn, $username, $password, $options
);
}
}
Connector::createConnection()
第一引数はストリング型で
DSN
情報です。
第二引数は配列型で接続情報です。
第三引数は配列型でオプション情報です。
戻り値はPDO
インスタンスです。
接続情報配列からユーザーネームとパスワードを取り出します。存在しなければ null
です。
createPdoConnection()
メソッドで接続を TRY
し、成功の場合は戻り値を返します。
失敗した場合、コネクションロストの可能性を考慮し再度接続を試みます。
/**
* Create a new PDO connection instance.
*
* @param string $dsn
* @param string $username
* @param string $password
* @param array $options
* @return \PDO
*/
protected function createPdoConnection($dsn, $username, $password, $options)
{
if (class_exists(PDOConnection::class) && ! $this->isPersistentConnection($options)) {
return new PDOConnection($dsn, $username, $password, $options);
}
return new PDO($dsn, $username, $password, $options);
}
/**
* Determine if the connection is persistent.
*
* @param array $options
* @return bool
*/
protected function isPersistentConnection($options)
{
return isset($options[PDO::ATTR_PERSISTENT]) &&
$options[PDO::ATTR_PERSISTENT];
}
Connector::createPdoConnection()
第一引数はストリング型で
DSN
情報です。
第二引数はストリング型でユーザーネーム情報です。
第三引数はストリング型でパスワード情報です。
第四引数は配列型で接続情報です。
戻り値はPDO
インスタンスです。
PDOConnection
クラスが定義済みで、PDO::ATTR_PERSISTENT
オプションがtrue
であった場合、PDOConnection
インスタンスを、そうでない場合はPDO
インスタンスを生成して返します。PDO::ATTR_PERSISTENT
はデータベースへの常時接続のパラメーターで、アクセス数が多いサービスなどで毎回接続を確立するよりも常時接続していた方がパフォーマンスを確保できるケースで利用するためのものです。
この、PDOConnection
はDoctrine\DBAL\Driver\PDOConnection
なのですが、通常インストール時にはcomposer
がインストールしていません。少し調べただけなので確実なことは言えませんが、GitHubのページも 404 Not found. となっています。
同じメソッド名で混同しそうですが、createSingleConnection()
でPDO
インスタンスを生成した後の処理に続きます。
/**
* Create a new connection instance.
*
* @param string $driver
* @param \PDO|\Closure $connection
* @param string $database
* @param string $prefix
* @param array $config
* @return \Illuminate\Database\Connection
*
* @throws \InvalidArgumentException
*/
protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])
{
if ($resolver = Connection::getResolver($driver)) {
return $resolver($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);
}
throw new InvalidArgumentException("Unsupported driver [{$driver}]");
}
ConnectionFactory::createConnection()
第一引数はストリング型で接続ドライバー名です。
第二引数はPDO
インスタンスもしくはクロージャーです。
第三引数はストリング型でデータベース名です。
第四引数はストリング型でプレフィックスです。
第五引数は$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はConnection
インスタンスです。
createSingleConnection()
の処理の続きです。
createSingleConnection()
には以下のように記述されていました。
return $this->createConnection(
$config['driver'], $pdo, $config['database'], $config['prefix'], $config
);
Connection::getResolver()
メソッドの内容は以下です。
public static function getResolver($driver)
{
return static::$resolvers[$driver] ?? null;
}
受け取ったドライバー名がstatic
変数Connection::$resolvers[]
に存在する場合はそれを、存在しない場合はnull
を返します。ドライバーを拡張した時に使うのではないかと推測されます。
存在しない場合はドライバー名に従って各データベース用のConnection
クラスを生成し戻します。
指定したドライバーがなければ「Unsupported driver [ドライバー名]
」 というメッセージを添えて 例外InvalidArgumentException
をスローします。
デフォルトでの対応ドライバーは、「mysql
」「pgsql
」「sqlite
」「sqlsrv
」です。
Laravelのデフォルト設定では生成されるのはMySqlConnection
クラスです。このクラスにはコンストラクタがありません。Connection
クラスを継承しているのでそちらを読んでみましょう。
/**
* Create a new database connection instance.
*
* @param \PDO|\Closure $pdo
* @param string $database
* @param string $tablePrefix
* @param array $config
* @return void
*/
public function __construct($pdo, $database = '', $tablePrefix = '', array $config = [])
{
$this->pdo = $pdo;
// First we will setup the default properties. We keep track of the DB
// name we are connected to since it is needed when some reflective
// type commands are run such as checking whether a table exists.
$this->database = $database;
$this->tablePrefix = $tablePrefix;
$this->config = $config;
// We need to initialize a query grammar and the query post processors
// which are both very important parts of the database abstractions
// so we initialize these to their default values while starting.
$this->useDefaultQueryGrammar();
$this->useDefaultPostProcessor();
}
Connection::__construct()
第一引数は
PDO
インスタンス、若しくはPDO
インスタンスを返すクロージャーです。
第二引数はストリング型でデータベース名です。
第三引数はストリング型でプレフィックスです。
第四引数は配列型で接続情報です。
戻り値はありません。
受け取った引数を変数にセットし、useDefaultQueryGrammar()
メソッドとuseDefaultPostProcessor()
メソッドをコールします。
/**
* Set the query grammar to the default implementation.
*
* @return void
*/
public function useDefaultQueryGrammar()
{
$this->queryGrammar = $this->getDefaultQueryGrammar();
}
/**
* Get the default query grammar instance.
*
* @return \Illuminate\Database\Query\Grammars\Grammar
*/
protected function getDefaultQueryGrammar()
{
return new QueryGrammar;
}
Connection::useDefaultQueryGrammar()
引数、戻り値はありません。
useDefaultQueryGrammar()
メソッドは$this->queryGrammar
にgetDefaultQueryGrammar()
メソッドの戻り値を代入します。
getDefaultQueryGrammar()
メソッドはQueryGrammar
インスタンスを生成し戻します。
QueryGrammar
の実体はIlluminate\Database\Query\Grammars\Grammar
です。
Illuminate\Database\Grammar
抽象クラスを継承しています。これはおそらくSQLを生成するための各種メソッドをまとめたものと推測できます。
/**
* Set the query post processor to the default implementation.
*
* @return void
*/
public function useDefaultPostProcessor()
{
$this->postProcessor = $this->getDefaultPostProcessor();
}
/**
* Get the default post processor instance.
*
* @return \Illuminate\Database\Query\Processors\Processor
*/
protected function getDefaultPostProcessor()
{
return new Processor;
}
Connection::useDefaultPostProcessor()
引数、戻り値はありません。
useDefaultPostProcessor()
メソッドは$this->postProcessor
にgetDefaultPostProcessor()
メソッドの戻り値を代入します。
getDefaultPostProcessor()
メソッドはProcessor
インスタンスを生成し戻します。
Processor
の実体はIlluminate\Database\Query\Processors\Processor
です。おそらくSQLを処理するものと推測できます。あとで出てくると思いますので、その時に読みましょう。
ConnectionFactory::createConnection()
メソッドは、接続の確立後に設定するパラメーターやクエリを実行するための準備などをする役割のようです。以上でConnectionFactory::createSingleConnection()
メソッドの処理が読み終わりました。ConnectionFactory::createReadWriteConnection()
の最後の処理を読んでみましょう。以下のように記述されていました。
return $connection->setReadPdo($this->createReadPdo($config));
createReadPdo()
から見てみましょう。
/**
* Create a new PDO instance for reading.
*
* @param array $config
* @return \Closure
*/
protected function createReadPdo(array $config)
{
return $this->createPdoResolver($this->getReadConfig($config));
}
/**
* Get the read configuration for a read / write connection.
*
* @param array $config
* @return array
*/
protected function getReadConfig(array $config)
{
return $this->mergeReadWriteConfig(
$config, $this->getReadWriteConfig($config, 'read')
);
}
ConnectionFactory::createReadPdo()
引数は
$config
としてデータベース接続情報を配列型で受け取っています。
戻り値はクロージャーです。
createPdoResolver()
メソッドは先程追ったのを覚えていると思います。接続情報にホスト情報が含まれているか調べ、PDO
インスタンスを戻すクロージャーを返します。ConnectionFactory::createReadWriteConnection()
メソッドはこの処理の前に書き込み接続の設定を使い接続を確立しています。
getReadConfig()
メソッドを見てみましょう。見覚えのある記述です。getWriteConfig()
メソッドの第二引数を「read
」に変えただけのものですね。読込用の接続情報以外を削除し配列を整える処理をしています。
読込接続用の設定で接続を確立するクロージャーを作り返す処理ということです。では、最後にsetReadPdo()
を見てみましょう。
/**
* Set the PDO connection used for reading.
*
* @param \PDO|\Closure|null $pdo
* @return $this
*/
public function setReadPdo($pdo)
{
$this->readPdo = $pdo;
return $this;
}
Connection::setReadPdo()
第一引数は
PDO
インスタンス若しくはクロージャーもしくはnull
です。
戻り値は$this
です。
受け取った引数をreadPdo
に代入しています。
このメソッドをコールしたのはすでに書込接続が確立されたConnection
インスタンスで、接続はpdo
に代入されています。書込時はpdo
読み込み時はreadPdo
と接続を切り替える仕様だろうと推測できます。
以上で、ConnectionFactory::make()
の処理の流れを読み終わりました。
データベースに接続する仕組みが少しわかりましたね!
次回
データベースの接続まで読めましたので、次回は本筋に戻ってスキーマビルダーについて追っていきたいと思います。
続く☆