データの正確性と一貫性の問題は、小さな不便から大きな企業を揺るがす問題にまで発展する可能性があります。データベース内のデータを安全に保存、変更、消去するコードを構築することは、非常に重要です。
そこで、Laravelのトランザクション処理の登場です。
トランザクション処理は、データの整合性を確保するのに便利なアプローチです。Laravelを使えば、さまざまなデータベースでこのトランザクション処理をシンプルに実行できます。
では、トランザクション処理とは?Laravelで実際にどのように使うことができるのでしょうか?
この記事を読み終える頃には、Laravelのトランザクション処理の概要と、プロジェクトでの効果的な使用方法を習得できているはずです。
Laravelのトランザクション処理とは
技術的な側面に進む前に、まずLaravelの(データベースを扱う)トランザクション処理がどんなものであるか、そしてどのようにその恩恵を受けることができるかを理解しましょう。
トランザクション処理は、アプリケーションのデータベース構造内で安全に実行できる操作で、具体的には、データを修正するSQLクエリなど(更新、削除、挿入など)が挙げられます。
どの時点でも、トランザクションのすべてのクエリをロールバックすることができます。さらに、実行したクエリは、データベースによって1つのアクションとして扱われます。
この例を見てみましょう。
アカウントを作成することができるアプリがあるとします。各アカウントには1人または多数のユーザーが紐付けられる可能性があります。アカウントと最初のユーザーの作成時、アカウントが正しく生成されたものの、ユーザーの方がうまくいかなかった場合、対処が必要になります。
このサンプルコードをご覧下さい。
// Create Account
$newAcct = Account::create([
'accountname' => Input::get('accountname'),
]);
// Create User
$newUser = User::create([
'username' => Input::get('username'),
'account_id' => $newAcct->id,
]);
ここには2つのシナリオがあり、厄介な問題が発生する可能性があります。
- アカウントが生成されない
- ユーザーの作成に失敗する
後者の状況について考えてみましょう。
利用可能なユーザーがいないアカウントを作成すると、データベースのデータ不整合につながります。これを解決するためには、いくつかの方法が考えられます。コーディングで回避するという、気の遠くなるような作業もありますし、コードを削除していくことも、または、トランザクション処理で対処することもできます。
トランザクション処理は、ほとんどのSQLデータベースに存在しますが、主にその実装と効率に違いがあります。MySQL、SQLite、PostgreSQL、Oracleなどの人気のシステムがトランザクション処理をサポートしているので、好みに応じたSQLデータベースの展開に困ることはないでしょう。
マイグレーション
マイグレーションは、Laravelの重要な要素で、データベース内にテーブルを構築し、修正を加え、アプリケーションのデータベーススキーマを共有することができます。Laravelのマイグレーションを使用すると、新しいカラムを追加したり、既存のカラムを削除したりして、テーブルを編集できます。
チームとアイデアを議論し、テーブル調整の必要性が生じたとします。そんな時には、チームの誰かが、SQLファイルを共有、インポートする必要があります。SQLファイルをインポートし忘れると、アプリケーションの動作に問題が発生する可能性があります。
そこで、Laravelのマイグレーションが活躍します。データベースに新しいカラムを追加したり、既にあるカラムに影響を与えることなくエントリーを削除したりすることができます。
シーダー(Seeder)
Laravelの開発者向けの機能に、シーダーというものがあります。これを使うと、様々なデータ型のテスト、バグの修正、パフォーマンスのチューニングが容易になります。1つのコマンドで、データベースシーダーを経由して、データベーステーブルに複数行のダミーデータを追加することができます。
その結果、データベース復元のたびに一つ一つの情報を入力することなく、新しいデータベースとサンプル値を使って作業を繰り返すことができます。
Laravelのトランザクション処理のオプション
Laravelでは、Adminerなどの様々なデータ管理ツールが利用できます。トランザクション処理については、データベース側に3つのメソッドがあり、手動でトランザクションを開始し、事細かに制御可能です。
多くのケースで、トランザクションをコミットまたはロールバックするタイミングを正確に定義するために、この手法が好まれています。
- トランザクションを開始:
DB::beginTransaction();
コマンドを使用してトランザクションを開始します。 - トランザクションをロールバック:変更または操作を取り消す場合は、
DB::rollBack();
コマンドを使用します。 - トランザクションをコミット:すべてが計画通りに進んだ場合、
DB::commit();
コマンドを使用します。
トランザクションはすべて、コミットまたはロールバックのどちらかのアクションで終了させる点をお忘れ無く(特にループで重要)。そうしないと、同期がとれず、レコードが更新されなくなります。
Laravelデータベースとの連携
マイグレーションとシーダーは、前述したように、Laravel開発者向けの洗練されたソリューションで、アプリケーションのデータベースを迅速にデプロイ、削除、リストアすることができます。特に、複数の開発者が同じアプリで作業している場合に便利です。
このセクションでは、artisanコマンドを使用して、Laravelデータベースで簡単にマイグレーションとシーダーを使用する方法をご紹介します。
前提条件
必要なものは以下のとおりです。
- Ubuntu 18.04のローカルコンピュータまたは開発サーバーで、sudo権限を持つ非rootユーザー。リモートサーバーを使用している場合は、有効なファイアウォールを設定しておくのが得策です。
- マシンにLEMPがインストールされていること。DockerとDocker Composeをインストールすることで、より快適にアプリケーションを実行することができます。
ちなみに、Kinstaが開発したオリジナルソフトウェアであるDevKinstaではDockerが採用されており、現在、60,000+を超える開発者やデザイナーの方々に、WordPressサイト作成や開発にご活用いただいています。
もちろん、スキルやコーディングの要件に応じて、他にもウェブ開発ツールはたくさんありますので、お好みのものをお使いください。
Laravelのマイグレーション
マイグレーションクラスには、upとdownの2つのメソッドがあります。upメソッドは、データベースに新しいテーブル、インデックス、またはカラムを作成するのに使用します。ダウンメソッドは、アップメソッドによる操作を元に戻すために使われます。
Laravelのスキーマビルダを使用して、それぞれのメソッドで自由にテーブルを構築、編集することができます。例えば、このマイグレーションでは、フライトのテーブルを生成しています。
use IlluminateDatabaseMigrationsMigration;
use IlluminateDatabaseSchemaBlueprint;
use IlluminateSupportFacadesSchema;
class CreateFlightsTable extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up() {
Schema::create('flights', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('airline');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down() {
Schema::drop('flights');
}
make:migration
コマンドでは、テーブルの名前を明確にする必要があります。そのため、table_name
が希望するものと一致するようにしてください。
[cta]
--table
と--create
を使って、以下のように、テーブルの名前と、マイグレーションで新しいテーブルを作成するかどうかを指定することができます。
php artisan make:migration create_users_table --create=users
php artisan make:migration add_votes_to_users_table --table=users
database/migrationsディレクトリに、新しいマイグレーションが含まれるようになります。各マイグレーションファイル名にはタイムスタンプが付され、Laravelはこのタイムスタンプを使ってマイグレーションの順番を判断します。
また、実際のルートディレクトリに対するものとして、--path
を定義することもできます。具体的には、次のコマンドを使用します。
php artisan migrate:make foo --path=app/migrations
マイグレーションの実行
移行を実行するときに便利なコマンドがたくさんあります。そのうちのいくつかを見ていきましょう。
php artisan migrate
:このコマンドは、すべてのスキーマをデータベースに公開します。また、データベース内にテーブルを生成します。php artisan migrate --path=app/foo/migrations
:このコマンドは、あるディレクトリの下ですべてのマイグレーションを実行します。もし、「Nothing to migrate」というエラーメッセージが表示されたら、php artisan migrate --path=database/migrations/foo
コマンドを(appディレクトリなしで)実行してください。php artisan migrate --package=vendor/package
:パッケージのマイグレーションを実行したい場合は、このコマンドを使用します。
時々、migrations の実行中に “Class not found” というエラーが発生することがあります。その場合は、composer dump-autoload
コマンドを実行してください。
マイグレーションの中には、危険なものもあり、データを失ってしまう可能性があります。そのため、Laravelでは、データを保護するために、コマンドの実行を確認するよう求められます。
プロンプトを表示させたくない場合は、以下のように--force flag
を使って強制的にコマンドを実行可能です。
php artisan migrate --force
マイグレーションをロールバックする
前回のマイグレーションを元に戻すには、次のようにロールバックコマンドを使用できます。
php artisan migrate:rollback
その他のロールバックコマンドもご紹介します。
php artisan migrate:reset
:このコマンドは、前回だけでなく、すべてのマイグレーションを逆転させます。php artisan migrate:fresh
:データベースを新規にインストールする場合は、このコマンドを使用します。既存のテーブルをすべて削除し、migration
コマンドを実行します。php artisan migrate:refresh
:これは、ロールバックとマイグレーション実行の両方を行う、二つの操作が一つになったコマンドです。php artisan migrate:fresh --seed
:migrate:fresh
コマンドを実行してから、データベースのシーディングを行います。新しいホストにアプリをインストールする際に、このコマンドを利用してデータベースにシーディング(=データをアップロード)することができます。
Laravelのシーディング
シーダーとは、データサンプル(シード)を作成し、データベースに挿入するためのクラスです。Laravelでは、database/seedsディレクトリでシードクラスを使って、データベースにテストデータを簡単に入れることができます。
シードクラスの名前は自由に決めることができます。しかし「UsersTableSeeder」のような、明確なパターンに従うことをお勧めします。その後、DatabaseSeeder
クラスがデフォルトで作成されます。
Laravelのデータベースシードクラスの例が以下の通りです。
class DatabaseSeeder extends Seeder {
public function run() {
$this->call('UserTableSeeder');
$this->command->info('User table seeded!');
}
}
class UserTableSeeder extends Seeder {
public function run() {
DB::table('users')->delete();
User::create(array('email' => '[email protected]'));
}
}
シーダーを作成する
シーダーの生成は、とても簡単です。目をつぶっていてもできます(とは言え…やらないでください)。
make:seeder
artisanコマンドを実行し、シーダーを作成します。これで、database/seedsディレクトリに、フレームワークにより生成されたすべてのシーダーが揃います。
php artisan make:seeder UsersTableSeeder
シーダークラスのデフォルトメソッドが実行されます。db:seed
artisanコマンドを適用すると、この処理が行われます。run機能を使って、好きな方法でデータベースにデータを入れることができます。さらに、EloquentのモデルファクトリやQuery Builderを使用してデータを挿入していくことも十分に可能です。
いずれにせよ、大事な点として、データベースのシーディング時には、一括割り当て保護が自動で解除されることを覚えておいてください。
ここでは、基本的なDatabaseSeeder
クラスに修正を加え、runメソッドにデータベース挿入文を追加します。
<?php
use IlluminateDatabaseSeeder;
use IlluminateSupportFacadesDB;
use IlluminateSupportFacadesHash;
use IlluminateSupportStr;
class DatabaseSeeder extends Seeder {
/**
* Run the database seeds.
*
* @return void
*/
public function run() {
DB::table('users')->insert([
'name' => Str::random(10),
'email' => Str::random(10).'@gmail.com',
'password' => Hash::make('password'),
]);
}
}
runメソッドのコード内で依存関係をタイプヒントしたい場合は、Laravelサービスコンテナが自動的に解決してくれます。
さらに、call
関数を使用すると、このクラスから別のシードクラスを実行することができ、シードの順序のカスタマイズが可能になります。データベースのシーディングを複数ファイルに分割し、特定のシーダークラスが過剰に拡張しないようにすることができます。
以下のように、使用したいシーダークラスの名前を入力します。
/**
* Run the database seeds.
*
* @return void
*/
public function run() {
$this->call([
UsersTableSeeder::class,
PostsTableSeeder::class,
CommentsTableSeeder::class,
]);
}
シーダーの実行
シーダーを生成した後、場合によっては、dump-autoload
コマンドを使用してComposerのオートローダを再作成する必要があるかもしれません。
composer dump-autoload
次に、db:seed
artisanコマンドを実行して、データベースのシードを作成します。
php artisan db:seed
このコマンドは、DatabaseSeeder
クラスをプロキシで実行するもので、他のシーダークラスを実行するために使用することができます。ただし、次のように--class
パラメータを使用して、特定のシーダークラスを個別に実行することも可能です。
php artisan db:seed --class=UserTableSeeder
すべてのテーブルを削除し、すべてのマイグレーションをもう一度実行するなど、データベースをゼロから作り直したい場合はこちら。migrate:fresh
コマンドを使用して、データベースのシーディングを行います。
php artisan migrate:fresh --seed
マイグレーションと同様に、シード処理によってはデータの損失や不要な変更が発生する可能性があります。このため、プライマリデータベースでシーディングコマンドを実行しないようにするため、シーディングを実行する前に承認を求めるプロンプトが表示されます。
十分な自信があり、この保護策を邪魔に感じる場合には、以下の--force
フラグを使用してください。
php artisan db:seed --force
Laravelで生のデータベースクエリを使用する(その他の5つの方法)
LaravelにはEloquentやQuery Builderなどの便利なツールがありますが、SQLを使用して生のクエリを実行することもできます。5つの方法をご紹介します。
ここで一点、注意事項があります。生のクエリは自動で保護されませんので、リスクの高いアプローチであることをお忘れ無く。したがって、クエリに何らかのパラメータを与える場合は、テキストではなく数値など、正しい形式と正しい値であることを確認する必要があります。
[cta]
平均値/合計値/個数計算
GROUP BY ()
を使うことができます。次の例に示すようにCount()
、SUM()
、AVG()
、MIN()
、MAX()
などを使って、MySQL集約関数を実行する際には、生のクエリが使用可能です。
$users = DB::table('users')
->selectRaw('count(*) as user_count, status')
->where('status', '<>', 1)
->groupBy('status')
->get();
同じSQLクエリでcount()
とavg()
の両方を実行することも可能です。
$salaries = DB::table('salaries')
->selectRaw('companies.name as company_name, avg(salary) as avg_salary, count(*) as people_count')
->join('companies', 'salaries.company_id', '=', 'companies.id')
->groupBy('companies.id')
->orderByDesc('avg_salary')
->get();
年のフィルタリング
GROUP BY
やORDER BY
の中でSQLの計算を行うために、groupByRaw()
やorderByRaw()
クエリを使用することができます。グループ化した後、having
SQLクエリを組み込みことで(havingRaw ()
)、where
ステートメントを利用することもできます。
例えば、以下のコマンドは、日付/時刻フィールドを年単位でグループ化する方法を示したものです。
$results = User::selectRaw('YEAR(birth_date) as year, COUNT(id) as amount')
->groupByRaw('YEAR(birth_date)')
->havingRaw('YEAR(birth_date) > 2000')
->orderByRaw('YEAR(birth_date)')
->get();
単一フィールドの計算 (サブクエリ)
あるカラムから別のカラムを計算し、その結果をSQLクエリで返したい場合を考えてみましょう。さて、どうすればいいでしょうか。
答えは、以下の通りです。
$products = Product::select('id', 'name')
->selectRaw('price - discount_price AS discount')
->get();
以下が、SQLのCASE
ステートメントを使った例です。
$users = DB::table('users')
->select('name', 'surname')
->selectRaw("(CASE WHEN (gender = 1) THEN 'M' ELSE 'F' END) as gender_text")
->get();
古いSQLを変換する
SQL文を、EloquentやQuery Builderに変換する必要性が生じるのは、よくあることで、特に昔のプロジェクトからのSQL文となると、多いにあり得る話です。
しかし、そんなことをする必要はありません。代わりに、DB::select()
ステートメントを使用することができます。
$results = DB::select('select * from users where id=?', [1]);
結果なしでクエリを実行する
DB::statement
では、SQLクエリを実行し、結果が無しということもあり得ます。例えば、INSERT
やUPDATE
などです(変数なし)。
これは、データベースのマイグレーション時に、テーブルの構造を変更し、古いデータを新しいものに変更しなければならないときによく使われます。
DB::statement('UPDATE users SET role_id = 1 WHERE role_id IS NULL AND YEAR(created_at) > 2020');
さらに、DB::statement()
は、値や列に限定されないスキーマを持つ任意の SQLクエリを実行することができます。以下がその例です。
DB::statement('DROP TABLE users');
DB::statement('ALTER TABLE projects AUTO_INCREMENT=123');
まとめ
ここまでで、Laravelのトランザクション処理とその実装方法について、理解を深めることができたはずです。データの整合性を助けるだけでなく、Laravelのパフォーマンスを最適化しながら、開発プロセスを容易にすることができますので、是非ともご活用ください。