叫ぶうさぎの悪ふざけ

うさぎが目印のWebエンジニアが、得たことや思ったことを言の葉に乗せて叫ぶ場所です。

学習記録:12月8日(土):【輪読会まとめ 前編】Laravel Webアプリケーション開発 Chapter5 データベース

これは 俺のインプットアウトプット記録 Advent Calendar 2018 の8日目のエントリーです。

先日、PHPフレームワーク Laravel Webアプリケーション開発 バージョン5.5 LTS対応 を購入して、輪読会に参加してきました。 僕の担当章だった 第二部 Chapter5 データベース を事前にまとめ、発表してきたのですが、実際にコードを実行しきれてませんでした。

そこで今日は、コードを実際に実行した結果も添えて、まとめを更新して公開したいと思います。(分量が多くなってしまったので前編、後編に分けます。)

やったこと

大したことはやってないんですが、以下を実施しました。もし読み進めているかたのためになればいいなあ。

  • 章を読み進め、大事な部分と自分なりの意訳を書き出した
    • インプットしたこと全てをアウトプット
    • なるべくわかりやすく解説できるように
  • コードはほぼ全て実際に実行した
    • そのまま実行してもつまらないので、多少工夫
  • MySQL文字コード設定した

事前準備

まず事前準備から。MySQLを使う前提ですが、テストデータに日本語を使いたかったこと、自分なりの文字コードのこだわりがあったことで、 character-set を設定し、Dockerコンテナをリビルドしました。

  • my.cnfを以下に編集し、buildし直しておくこと
    • じゃないと文字コードがlatin1のままで文字化けする
    • 対象ファイルは laradock/mysql/my.cnf

my.cnfの文字コード設定

utf8mb4 に統一します。

これで、データベース、テーブル、カラム別に文字コードを設定する必要がなくなります。むしろ文字コードをコードで個別に指定するのは、個人的にはバグの原因になるだけだと思っています。

[mysql]
default-character-set=utf8mb4

[mysqld]
character-set-server=utf8mb4

[client]
loose-default-character-set=utf8mb4

dockerのMySQLをbuildし直す

以下のコマンドでビルドし直し、Dockerコンテナを起動します。

$ pwd
/Users/mamy1326/dev/laravel_docker/laradock
$ docker-compose build mysql
(中略)
$ docker-compose down
(中略)
$ docker-compose up -d nginx mysql workspace

イントロダクション

というわけで書籍を読み進めていきます。まずは導入部分から。

マイグレーションとEloquent、クエリビルダによるDB操作

5-1 マイグレーション

5-1-0 導入部

5-1-1 マイグレーション処理の流れ

図5.1.1.1:マイグレーションの流れ

ただ読んで見るのもなんなので、書き起こしてみました。 この書き起こしがあったので、先日書いた 学習記録:12月7日(金):Webフレームワークに依存しない、PHP製のシンプルなSQL マイグレーションツール「Mig」を使ってみた でも理解がスムーズだったなあと思います。

image.png (602.5 kB)

5-1-2 マイグレーションファイルの作成

リスト5.1.2.1:マイグレーションファイルの作成

$ php artisan make:migration [ファイル名] [オプション]

解説

  • ファイル名には作成するマイグレーションファイルの名前
  • ファイル名には年月日時分秒が付く
    • 2014_10_12_000000_create_users_table.php
    • database/migrations フォルダに作成される
  • ファイル命名規則は特にないが、管理しやすい名前にしよう
    • create_users_table
    • add_column_to_users_table
    • add_column_username_kana_to_users_table

オプション

表5.1.2.2:migrationコマンドのオプション

オプション 機能
--create=[テーブル名] 新規テーブル作成のためのコードが付与される
--table=[テーブル名] 指定されたテーブルを操作するためのコードが付与される(テーブル設定の変更などで使用、ALTER TABLEなど)
--path=[パス]  指定されたパスにマイグレーションファイルを配置する。(アプリケーションのベースパスからの相対で指定する)

マイグレーションファイルの作成

  • 書籍管理プログラムを想定
    • 著者(authors)
    • 出版社(publishers)
    • 書籍(books)
    • 書籍詳細(bookdetails)

リスト5.1.2.3:マイグレーションファイルの作成

$ php artisan make:migration create_authors_table
$ php artisan make:migration create_publishers_table
$ php artisan make:migration create_books_table
$ php artisan make:migration create_bookdetails_table
  • 実際にやってみた内容

booksテーブル

$ php artisan make:migration create_books_table
Created Migration: 2018_11_12_155051_create_books_table

マイグレーションファイルの確認

  • database/migrations を確認
  • books_tableを開く

リスト5.1.2.4:マイグレーションファイル(CreateBooksTableクラス)

<?php

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

class CreateBooksTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->increments('id');
            $table->timestamps();
        });
    }

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

解説

  • Illuminate\Database\Migrations\Migration クラスを継承
  • upメソッドとdownメソッドを持つ
    • upメソッド:データベース定義の追加変更を行う処理
      • テーブル新規作成の Schema::create メソッド
    • downメソッド:rollback処理
      • テーブル削除の Schema::dropIfExists メソッド

5-1-3 定義の記述

5-1-3-1 テーブル作成処理

  • Schema::create メソッド
  • upメソッド に定義を記載していく

表5.1.3.1:テーブル定義(authorsテーブル)

カラム 備考
id AUTO_INCREMENT -
name varchar(100) 著者氏名
kana varchar(100) 著者氏名(カナ)
created_at timestamp 作成日時
updated_at timestamp 更新日時

リスト5.1.3.5-1:authorsテーブル作成のためのコード

<?php
class CreateAuthorsTable extends Migration
{
    public function up()
    {
        Schema::create('authors', function (Blueprint $table) {
            $table->increments('id');       // ①auto increment
            $table->string('name', 100);    // ②型メソッド(ここはstring)
            $table->string('kana', 100);    // 同上
            $table->timestamps();           // ③created_at, updated_atを自動作成
        });
    }

    public function down()
    {
        Schema::dropIfExists('authors');
    }
}

表5.1.3.2:テーブル定義(booksテーブル)

カラム 備考
id AUTO_INCREMENT -
name varchar(100) 書籍名
bookdetail_id integer 書籍詳細ID
author_id integer 著者ID
publisher_id integer 出版社ID
created_at timestamp 作成日時
updated_at timestamp 更新日時

リスト5.1.3.5-2:booksテーブル作成のためのコード

<?php
class CreateBooksTable extends Migration
{
    public function up()
    {
        Schema::create('books', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 100);
            $table->integer('bookdetail_id');
            $table->integer('author_id');
            $table->integer('publisher_id');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('books');
    }
}

表5.1.3.3:テーブル定義(bookdetailsテーブル)

カラム 備考
id AUTO_INCREMENT -
isbn varchar(100) ISBNコード
published_date date 出版日
price integer 価格
created_at timestamp 作成日時
updated_at timestamp 更新日時

リスト5.1.3.5-3:bookdetailsテーブル作成のためのコード

<?php
class CreateBookdetailsTable extends Migration
{
    public function up()
    {
        Schema::create('bookdetails', function (Blueprint $table) {
            $table->increments('id');
            $table->string('isbn', 100);
            $table->date('published_date');
            $table->integer('price');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('bookdetails');
    }
}

表5.1.3.4:テーブル定義(publishersテーブル)

カラム 備考
id AUTO_INCREMENT -
name varchar(100) 出版社名
address text 住所
created_at timestamp 作成日時
updated_at timestamp 更新日時

リスト5.1.3.5-4:publishersテーブル作成のためのコード

<?php
class CreatePublishersTable extends Migration
{
    public function up()
    {
        Schema::create('publishers', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name', 100);
            $table->text('address');
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('publishers');
    }
}

表5.1.3.6:生成できるカラムタイプとスキーマビルダの構文

カラムタイプ スキーマビルダ 備考
BOOLEAN型 $table->boolean('カラム名');
CHAR型 $table->char('カラム名', 'サイズ'); 第二引数で文字列長を指定
DATE型 $table->date('カラム名');
DATETIME型 $table->dateTime('カラム名');
DOUBLE型 $table->double('カラム名', '最大桁数', '小数点の右側の桁数'); 第二引数で有効な全体桁数、第三引数で小数点以下の桁数を指定
FLOAT型 $table->float('カラム名', ''); 同上
ID(主キー)カラム $table->increments('カラム名'); オートインクリメントするINT型カラムを生成。bigだと bigIncrements を使う
INTEGER型 $table->integer('カラム名');
JSON $table->json('カラム名');

5-1-3-2 テーブル削除処理

  • Schema::dropIfExists メソッド
    • 自動生成されている
    • あれば削除、なければ何もしない
    • 特にコードを追加することはない

5-1-3-3 そのほかのテーブル定義メソッド

表5.1.3.7:カラムに属性を与えるメソッド

メソッド 内容
$table->string('name_kana', 100)->alter('name'); 指定カラムの直後にカラム追加(MySQLのみ)
$table->date('birthday')->nullable(); NULL許可
$table->integer('blood_type')->default(0); デフォルト値
$table->inclementes('bookdetail_id')->unsigned(); 符号なし

リスト5.1.3.8:VARCHARに対してNULL許容

$table->string('name', 50)->nullable();

リスト5.1.3.9:INTEGERに対してデフォルト

$table->integer('type')->default(0);

表5.1.3.10:インデックス系

メソッド 内容
$table->primary('id'); PK付与
$table->primary(['id', 'parent_id']); 複合PK
$table->unique('book_id'); UK付与。$table->integer('book_id')->unique(); でもOK
$table->index('booldetail_id'); 通常のindex付与
$table->index('bookdetail_id', 'bookdetails_idx'); キー名を指定してindex付与

リスト5.1.3.11:カラムにインデックスを与える

<?php
// idというINTEGER型のカラムにインデックスを付与する
$table->integer('id')->index();

// 分けて書くことも可能
$table->integer('id');
$table->index('id');

// インデックスに名前を付与
$table->index('id', 'books_idx');

5-1-4 マイクレーションの実行とロールバック

  • migrate コマンド
  • database/migrations フォルダにあるマイグレーションファイルが対象
  • データベースに反映されていない内容を実行

リスト5.1.4.1:migrateコマンド実行

$ cd ../laradock
$ docker-compose exec --user=laradock workspace bash
(laradocのdocker内)
$ cd /var/www
$ php artisan migrate # artisanコマンドのある場所で実行
Migrating: 2018_11_12_155051_create_books_table
Migrated:  2018_11_12_155051_create_books_table
Migrating: 2018_11_13_160808_create_authors_table
Migrated:  2018_11_13_160808_create_authors_table
Migrating: 2018_11_13_161537_create_bookdetails_table
Migrated:  2018_11_13_161537_create_bookdetails_table
Migrating: 2018_11_13_161550_create_publishers_table
Migrated:  2018_11_13_161550_create_publishers_table

リスト5.1.4.2:マイグレーション実行結果

$ docker-compose exec mysql bash
(mysqlのdocker内)
$ mysql -udefault -p default
mysql> show tables;
+-------------------+
| Tables_in_default |
+-------------------+
| authors           |
| bookdetails       |
| books             |
| migrations        |
| password_resets   |
| publishers        |
| users             |
+-------------------+
7 rows in set (0.01 sec)

mysql> show create table authors\G
*************************** 1. row ***************************
       Table: authors
Create Table: CREATE TABLE `authors` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `kana` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

mysql> show create table books\G
*************************** 1. row ***************************
       Table: books
Create Table: CREATE TABLE `books` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `bookdetail_id` int(11) NOT NULL,
  `author_id` int(11) NOT NULL,
  `publisher_id` int(11) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

mysql> show create table bookdetails\G
*************************** 1. row ***************************
       Table: bookdetails
Create Table: CREATE TABLE `bookdetails` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `isbn` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `published_date` date NOT NULL,
  `price` int(11) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

mysql> show create table publishers\G
*************************** 1. row ***************************
       Table: publishers
Create Table: CREATE TABLE `publishers` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `address` text COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

リスト5.1.4.3:ロールバックコマンド - 直前のマイグレーションのみロールバックしてくれる

$ php artisan migrate:rollback

リスト5.1.4.4:リセットコマンド - 全てのマイグレーションをまとめて元に戻す

$ php artisan migrate:reset

5-1-4-1 マイグレーションの実行状態を管理するテーブル

mysql> show create table migrations\G
*************************** 1. row ***************************
       Table: migrations
Create Table: CREATE TABLE `migrations` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `batch` int(11) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

mysql> select * from migrations;
+----+------------------------------------------------+-------+
| id | migration                                      | batch |
+----+------------------------------------------------+-------+
|  1 | 2014_10_12_000000_create_users_table           |     1 |
|  2 | 2014_10_12_100000_create_password_resets_table |     1 |
|  3 | 2018_11_12_155051_create_books_table           |     2 |
|  4 | 2018_11_13_160808_create_authors_table         |     2 |
|  5 | 2018_11_13_161537_create_bookdetails_table     |     2 |
|  6 | 2018_11_13_161550_create_publishers_table      |     2 |
+----+------------------------------------------------+-------+
6 rows in set (0.00 sec)

5-2 シーダ

  • アプリケーション実行に必要なデータをシーダー機能を使って投入

導入部分

  • マスタデータなどの初期データを投入する機能
    • テストデータなども
    • これらのデータをコードで実行する仕組みがシーダ
  • Seeder、Factoryクラスでデータ投入
    • Fakerでテストデータを作るところも紹介

5-2-1 シーダーの作成

リスト5.2.1.1:シーダーの作成 - database/seeder にSeederクラスが生成される - ファイル命名規則はmigrationでも言ったように適切にね - Authorsテーブルにデータ投入なら AuthorsTableSeeder

$ php artisan make:seeder (ファイル名)

リスト5.2.1.2:作成直後のSeederクラス - Seeder実行

$ cd ../laradock
$ docker-compose exec --user=laradock workspace bash
(docker内)
$ pwd
/var/www
$ php artisan make:seeder AuthorsTableSeeder
Seeder created successfully.

作成直後のSeederクラス

<?php

use Illuminate\Database\Seeder;

class AuthorsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        //
    }
}

データ登録処理を追加

<?php
(中略)
    public function run()
    {
        //AUthorsテーブルにレコードを10件登録する
        $now = \Carbon\Carbon::now();
        for ($i = 1; $i <= 10; $i++) {
            $author = [
                'name' => '著者名' . $i,
                'kana' => 'チョシャメイ' . $i,
                'created_at' => $now,
                'updated_at' => $now,
            ];
            DB::table('authors')->insert($author);
        }
    }

5-2-2 シーダークラスを利用するための設定

  • database\seedsDatabaseSeeder.php を開く
  • runメソッドに以下を追加
    • php artisan make:seeder AuthorsTableSeeder された時点でコメントで記述されているのでコメントアウトするだけでいい

リスト5.2.2.1:Authorテーブルにデータ登録を行う処理 - DatabaseSeeder.phpのrunメソッド

<?php
(中略)
    public function run()
    {
        $this->call(AuthorsTableSeeder::class);
    }

5-2-3 シーディングの実行

リスト5.2.3.1:シーディング実行コマンド - データ投入実行

$ php artisan db:seed
Seeding: AuthorsTableSeeder

リスト5.2.3.2:シーディング実行結果 - authorsテーブルのレコード

mysql> select * from authors;
+----+-------------+----------------------+---------------------+---------------------+
| id | name        | kana                 | created_at          | updated_at          |
+----+-------------+----------------------+---------------------+---------------------+
|  1 | 著者名1     | チョシャメイ1        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  2 | 著者名2     | チョシャメイ2        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  3 | 著者名3     | チョシャメイ3        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  4 | 著者名4     | チョシャメイ4        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  5 | 著者名5     | チョシャメイ5        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  6 | 著者名6     | チョシャメイ6        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  7 | 著者名7     | チョシャメイ7        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  8 | 著者名8     | チョシャメイ8        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
|  9 | 著者名9     | チョシャメイ9        | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
| 10 | 著者名10    | チョシャメイ10       | 2018-11-14 15:35:16 | 2018-11-14 15:35:16 |
+----+-------------+----------------------+---------------------+---------------------+
10 rows in set (0.00 sec)

5-2-4 Fakerの利用

表5.2.4.1:Fakerで作成できる主なダミーデータ

項目名 出力データ
name 氏名
email メールアドレス
safeEmail 安全な電子メール(存在しない)
password パスワード(PVqg5V!{/6MWHzg/FLe]、とか)
country 国名
address 住所
phoneNumber 電話番号
company 企業名
realText テキストデータ

PublishersTableSeeder準備

$ php artisan make:seeder PublishersTableSeeder
Seeder created successfully.

リスト5.2.4.2:Fakerでのデータ投入コード - PublishersTableSeederにFakerを使ったデータ投入

<?php
(中略)
    public function run()
    {
        //Fakerを使ってレコードを10件投入
        $faker = Faker\Factory::create('ja_JP');
        $now = \Carbon\Carbon::now();
        for ($i = 1; $i <= 10; $i++) {
            $publisher = [
                'name' => $faker->company . '出版',
                'address' => $faker->address,
                'created_at' => $now,
                'updated_at' => $now,
            ];
            DB::table('publishers')->insert($publisher);
        }
    }

DatabaseSeeder.phpのrunメソッドにPublishersTableSeeder追加

<?php
(中略)
    public function run()
    {
        $this->call(AuthorsTableSeeder::class);
        $this->call(PublishersTableSeeder::class);
    }

リスト5.2.4.3:実行コマンドと結果 - Seeder実行

$ php artisan db:seed
Seeding: AuthorsTableSeeder
Seeding: PublishersTableSeeder

Fakerでのデータ投入結果

mysql> select * from publishers;
+----+------------------------------+-----------------------------------------------------------------------------------+---------------------+---------------------+
| id | name                         | address                                                                           | created_at          | updated_at          |
+----+------------------------------+-----------------------------------------------------------------------------------+---------------------+---------------------+
|  1 | 有限会社 佐藤出版            | 5493319  富山県伊藤市北区佐藤町宮沢2-2-9 ハイツ桐山104号                          | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  2 | 有限会社 杉山出版            | 7208783  大分県小泉市東区山口町加納2-3-1 ハイツ杉山102号                          | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  3 | 有限会社 工藤出版            | 7078513  埼玉県松本市中央区斉藤町三宅4-10-9 ハイツ喜嶋105号                       | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  4 | 株式会社 杉山出版            | 5177296  三重県中村市西区原田町渡辺8-3-1                                          | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  5 | 株式会社 青山出版            | 2219948  千葉県西之園市東区井上町佐々木4-5-8 ハイツ井高110号                      | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  6 | 有限会社 大垣出版            | 6776246  島根県加納市西区高橋町田辺6-6-4                                          | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  7 | 有限会社 原田出版            | 2796245  福岡県藤本市中央区田中町工藤10-4-10                                      | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  8 | 株式会社 吉本出版            | 9972025  福井県廣川市東区吉本町笹田5-4-2 ハイツ青山104号                          | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
|  9 | 有限会社 佐々木出版          | 8481973  群馬県山田市北区宇野町喜嶋6-2-10                                         | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
| 10 | 株式会社 田辺出版            | 3881619  高知県津田市中央区喜嶋町宮沢9-1-7 ハイツ藤本110号                        | 2018-11-14 15:56:28 | 2018-11-14 15:56:28 |
+----+------------------------------+-----------------------------------------------------------------------------------+---------------------+---------------------+
10 rows in set (0.00 sec)

5-2-5 Factoryを利用する例

  • 大量のデータ投入に便利
  • database\factories\ModelFactory.php 内に

    • Eloquentクラスごとのファクトリーを記述
    • シーダーで利用するダミーデータを簡単生成
  • モデルクラスを作る

  • ファクトリークラスを作成し、データ投入処理を定義
  • シーダークラスのrunメソッドに、ファクトリークラスの利用処理追加
  • DatabaseSeederクラスのrunメソッドで「3.」を呼び出す

リスト5.2.5.1:モデルクラス作成 - app\BookDetail.php

$ php artisan make:model Bookdetail
Model created successfully.
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Bookdetail extends Model
{
    //
}

リスト5.2.5.2:ファクトリークラス作成 - database\factories に作成

$ php artisan make:factory ModelFactory
Factory created successfully.
  • 処理が空なのでfakerで定義
  • $factory->define の第一引数に記述したクラスにこの内容がインジェクションされる?
<?php

use Faker\Generator as Faker;

$factory->define(App\Bookdetail::class, function (Faker $faker) {
    $faker->locale('ja_JP');
    $now = \Carbon\Carbon::now();
    return [
        'isbn' => $faker->isbn13,
        'published_date' => $faker->date($format = 'Y-m-d', $max = 'now'),
        'price' => $faker->randomNumber(4),
        'created_at' => $now,
        'updated_at' => $now,
    ];
});

リスト5.2.5.3:bookdetailsテーブルに50件追加 - BookdetailsTableSeeder作成

$ php artisan make:seeder BookdetailsTableSeeder
Seeder created successfully.

BookdetailsTableSeeder編集

<?php

use Illuminate\Database\Seeder;

class BookdetailsTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(\App\Bookdetail::class, 50)->create();
    }
}

リスト5.2.5.4:DatabaseSeederクラスへの定義追加

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->call(AuthorsTableSeeder::class);
        $this->call(PublishersTableSeeder::class);
        $this->call(BookdetailsTableSeeder::class);
    }
}

リスト5.2.5.5:Seeder実行後のbookdetailsテーブル

$ php artisan db:seed
Seeding: AuthorsTableSeeder
Seeding: PublishersTableSeeder
Seeding: BookdetailsTableSeeder
  • 一括投入されたことがわかる
  • Fakerクラスでデータが作られている
  • SeederとFakerを組み合わせてデータ投入できる
  • マイグレーションと合わせて、バージョン管理しておくといいよ

実際のテーブルの中身

mysql> select * from bookdetails;
+----+---------------+----------------+-------+---------------------+---------------------+
| id | isbn          | published_date | price | created_at          | updated_at          |
+----+---------------+----------------+-------+---------------------+---------------------+
|  1 | 9795923865058 | 1980-04-24     |  6129 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  2 | 9794940757186 | 1987-06-05     |  7514 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  3 | 9789270927361 | 1998-06-08     |  2773 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  4 | 9792653154490 | 2008-05-31     |  2996 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  5 | 9798697951064 | 1972-11-14     |  5773 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  6 | 9794957103815 | 1982-06-29     |  2381 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  7 | 9782603321232 | 2003-03-02     |  5667 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  8 | 9784540122101 | 2005-06-15     |  9117 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
|  9 | 9798658650449 | 1983-05-03     |  7808 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 10 | 9792355798435 | 1996-03-03     |  7392 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
(中略)
| 40 | 9797964095913 | 1971-05-12     |  7286 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 41 | 9796726059019 | 1999-02-25     |  7591 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 42 | 9785517103987 | 2006-05-12     |  9723 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 43 | 9784238339682 | 2009-02-24     |  9908 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 44 | 9784171697207 | 1986-05-02     |  3931 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 45 | 9785986632216 | 1973-02-12     |  9888 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 46 | 9794838164485 | 1975-02-12     |  7932 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 47 | 9797802033428 | 1984-02-28     |  3756 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 48 | 9783292365408 | 2007-12-22     |    66 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 49 | 9788521682677 | 2000-11-14     |  9093 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
| 50 | 9780480668365 | 2007-02-26     |   586 | 2018-11-14 16:36:18 | 2018-11-14 16:36:18 |
+----+---------------+----------------+-------+---------------------+---------------------+
50 rows in set (0.00 sec)

5-3 Eloquent

  • LaravelのORM
  • 1つのテーブルに1つのEloquent
    • リレーション定義のメソッドを使えば複数テーブルも操作可能

5-3-1 クラスの作成

リスト5.3.1.1:Eloquentのクラスファイル作成コマンド

$ php artisan make:model (クラス名)

リスト5.3.1.2:作成直後のクラス - App\Author に出力される

$ php artisan make:model Author
Model created successfully.
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    //
}

5-3-2 規約とプロパティ

  • 対応するテーブル名、主キーのカラム名などルールがある
    • プロパティを指定することで任意の設定にできる

5-3-2-1 テーブルとの関連づけ

テーブル名が1単語

  • テーブル名を複数形で作成
  • クラス名を単数形で作成
  • 暗黙的に関連づけされる
    • authors テーブルは Author クラス

テーブル名が複数単語

  • スネークケースのテーブル名
  • クラス名はキャメルケース
  • book_sample テーブルは BookSample クラス
    • 複数形の話はどこいった?
      • 試してないです

それ以外のケースに対応するには

  • protected $table に定義
<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // t_author テーブルを関連づける
    protected $table = 't_author';
}

5-3-2-2 プライマリキーの定義

  • PKをEloquentクラスに定義する
  • Eloquentのメソッドにキー値を与えるだけでレコード取得できる
  • PKはデフォルト id
    • 明示的に設定するには以下

リスト5.3.2.2:任意のカラム名を主キーに設定

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // 主キーを定義
    protected $primaryKey = 'authors_id';
}

5-3-2-3 タイムスタンプの定義

  • デフォルトは
    • created_at に作成日時
    • updated_at に更新日時
  • 必要としない設定も可能

リスト5.3.2.3:タイムスタンプを記録しない設定

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // タイムスタンプを記録しない(デフォルトはtrue)
    protected $timestamps = false;
}

5-3-2-4 Mass Assignment による脆弱性への対策

  • 後述の created update メソッドの引数に連想配列を渡す
    • データ登録が可能
  • Mass Assignmentという便利機能
    • ユーザー権限操作など、アプリケーションで変更が想定されていないカラムの値が渡った場合も更新しちゃう
    • 脆弱性に繋がる
    • 脆弱性を防ぐためにMass Assignmentはデフォルトで無効になっている
  • 有効にするには以下

リスト5.3.2.4:編集可能なカラムを設定(ホワイトリスト

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // nameとkanaを指定可能にする
    protected $fillable = [
        'name',
        'kana',
    ];
}

リスト5.3.2.5:編集を認めないカラムを設定(ブラックリスト

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    // id, created_at, updated_atを編集不可
    protected $guarded = [
        'id',
        'created_at',
        'updated_at',
    ];
}

表5.3.2.6:その他のEloquentプロパティ

プロパティ 説明 デフォルト
$connection データベース接続 設定ファイル database.php で設定されたデフォルト
$dateFormat タイムスタンプのフォーマット Y-m-d H:i:s
$incrementing PKが自動増加かどうか true

5-3-3 データ検索・データ更新の基本

5-3-3-1 全権抽出 - all

  • all メソッド
    • テーブルの全レコード取得
    • 戻り値はCollectionクラス(Illuminate\Database\Eloquent\Collection)のインスタンス

リスト5.3.3.1:レコード全件取得

<?php

$authors = \App\Author::all();

foreach ($authors as $author) {
    echo $author->name;    // nameカラム表示
}

リスト5.3.3.2:レコード数の取得

<?php

$authors = \App\Author::all();

$count = $authors->count();

リスト5.3.3.3:filterメソッドでの絞り込み

<?php

$authors = \App\Author::all();

$filtered_authors = $authors->filter(function ($author) {
    // idが5より大きいレコード抽出
    return $author->id > 5;
});

// 絞り込み結果を取得
foreach ($filtered_authors as $author) {
    echo $author->name;    // nameカラム表示
}

5-3-3-2 プライマリキー指定による抽出 - find, findOrFail

  • PK指定でレコード取得
  • 戻り値は Illuminate\Database\Eloquent\Modelインスタンス

リスト5.3.3.4:findメソッドで利用

<?php

// authorsテーブルの id=10 のレコード取得
$authors = \App\Author::find(10);

リスト5.3.3.5:findOrFailメソッドで利用

<?php

try {
    // authorsテーブルの id=10 のレコード取得
    $authors = \App\Author::findOrFail(10);
} catch (Illuminate\Database\Eloquent\ModelNotFoundException $e) {
    // 見つからなかった時の処理
}

5-3-3-3 条件指定による抽出 -whereXXX

リスト5.3.3.6:whereXXXメソッドで条件指定

<?php

// authorsテーブルの name='山田太郎' のレコード取得
$authors = \App\Author::whereName('山田太郎')->get();

5-3-3-4 新しいレコードの登録 - create, save

  • 配列を引数
    • create
  • Eloquentモデルのインスタンス作成
    • 各カラムの値を設定して登録
    • save
  • 自動的にcreated_at, updated_atに値が入るよ

リスト5.3.3.7:createメソッドで登録

<?php

\App\Author::create([
    'name' => '著者A',
    'kana' => 'チョシャエー',
]);

リスト5.3.3.8:saveメソッドで登録

<?php

$author = new \App\Author();
$author->name =  '著者A',
$author->kana =  'チョシャエー',

$author->save();

5-3-3-5 データ更新 - update

  • findで指定して、create, saveと同じ感じ

リスト5.3.3.9:updateメソッドで更新

<?php

$author = \App\Author::find(1)->update(['name' => '著者B']);

リスト5.3.3.10:saveメソッドで更新

<?php

$author = new \App\Author::find(1);

$author->name =  '著者B',
$author->kana =  'チョシャビー',

$author->save();

5-3-3-6 データ削除 - delete, destroy

リスト5.3.3.11:deleteメソッドで削除

<?php
$author = \App\Author::find(1)
$author->delete();

リスト5.3.3.12:destroyメソッドで削除 - PKがわかってる場合

<?php
$author = new \App\Author::destroy(1);

$author = new \App\Author::destroy([2, 5, 7]);

// 以下も同じ動作
$author = new \App\Author::destroy(2, 5, 7);

5-3-4 データ操作の応用

  • クエリビルダという機能がある
  • SQL文を書くことなくPHPコードでデータ抽出

5-3-4-1 クエリビルダによるデータ操作を行う

  • 次章で詳述
  • 本項では where orderBy メソッド

リスト5.3.4.1:クエリビルダーによるデータ抽出

<?php

// id=1 or 2 を取得
$authors = \App\Author::where('id', 1)->orWhere('id', 2)->get();

// idが5以上を並び替え
$authors = \App\Author::where('id', '>=', 5)
                ->orderBy('id')
                ->get();

5-3-4-2 結果をJSONで取得する

  • APIで返す結果とかで重宝しそう

リスト5.3.4.2:抽出結果をJSONで取得

<?php

$author = \App\Author::find(1);

return $author->toJson();

リスト5.3.4.3:toJsonメソッドの結果

{"id":1,"name":"著者名1","kana":"チョシャメイイチ","created_at":"2018-07-18 14:27:09","updated_at":"2018-07-18 14:27:09"}

5-3-4-3 カラムの値に対して固定の編集を加える

  • 金額に3桁ごとにカンマ、半角カナを全角、などを自動実行できる
  • アクセサ
    • Eloquentのカラムに get[カラム名]Attribute の名前でメソッド追加
    • 編集処理(mb_convert系など)を書く
    • SELECTしたカラムに対し実行される
    • 発火はカラムを参照したとき、変数に入れたりechoされたりのタイミング
  • ミューテータ
    • Eloquentのカラムに set[カラム名]Attribute の名前でメソッド追加
    • DBへ更新をかけるときに実行される、Eloquentインスタンスの各カラムに値が代入された時

リスト5.3.4.4:アクセサとミューテータを定義

<?php
declare(strict_types=1);

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public function getKanaAttribute(string $value): string
    {
        // kanaカラムを半角カナに変換、DBから値を取得して表示した際
        return mb_convert_kana($value, "k");
    }

    public function setKanaAttribute(string $value): string
    {
        // kanaカラムを全角カナに変換、Eloquentインスタンスのカラム値に代入された際
        $this->attributes['kana'] = mb_convert_kana($value, "KV");
    }
}

リスト5.3.4.5:アクセサとミューテータが定義されたカラムを利用

<?php

// DBからデータ取得時
$authors = \App\Author::all();
foreach ($authors as $author) {
    echo $author->kana;    // getKanaAttributeで全角半角変換され表示される
}

// DBへデータ登録時
$author = \App\Author::find(Input::get('id'));
$author->kana = Input::get('kana');    // setKanaAttributeで半角から全角に変換される
$author->save();

5-3-4-4 「データがない場合のみ登録」をシンプルに実装

  • firstOrCreate, firstOrNew メソッド

リスト5.3.4.6:データがない場合のみデータ登録(通常)

<?php

$author = \App\Author::where('name', '=', '著者A')->first();
if (empty($author)) {
    $author = \App\Author::create(['name' => '著者A']);
}

リスト5.3.4.7:firstOrCreate でデータがない場合のみデータ登録

<?php

$author = \App\Author::firstOrCreate(['name' => '著者A']);

リスト5.3.4.8:firstOrNew でデータがない場合のみデータ登録

<?php

$author = \App\Author::firstOrNew(['name' => '著者A']);
$author->save();

5-3-4-5 論理削除を利用する

  • delete, destroyは物理削除
  • deleted_atカラムを追加し、使えるようにすれば日付が入り、論理削除となる

  • migrationでalter table用のファイル追加

  • migrationファイルに定義変更処理追加
  • migration実行でカラム追加
  • ModelにSoftDeleteトレイト定義
  • 論理削除データを扱う

リスト5.3.4.9:alter table用のmigrationファイル追加

$ php artisan make:migration softdelete_authors_table --table=authors
Created Migration: 2018_11_15_174305_softdelete_authors_table

リスト5.3.4.10:deteted_atを追加

<?php

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

class SoftdeleteAuthorsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->softDeletes();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->dropColumn('deleted_at');
        });
    }
}

リスト5.3.4.11:migrationでカラム追加

$ php artisan migrate
Migrating: 2018_11_15_174305_softdelete_authors_table
Migrated:  2018_11_15_174305_softdelete_authors_table
mysql> show create table authors\G
*************************** 1. row ***************************
       Table: authors
Create Table: CREATE TABLE `authors` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `kana` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `deleted_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.00 sec)

リスト5.3.4.12:SoftDeletesトレイトの定義 - ここまでで論理削除できるようになる

<?php
declare(strict_types=1);

namespace App;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Author extends Model
{
    use SoftDeletes;
}

リスト5.3.4.13:論理削除も含めたデータ抽出 - ここまでで、Authorモデルのdelete, destroyメソッドは論理削除になる - deleted_atに日付が入る、NULLなら有効レコード - 論理削除されたデータを扱う場合は withTrashed, onlyTrashed

<?php

// 削除済みも一緒に取得
$authors = App\Author::withTrashed()->get();

// 削除済みだけ取得
$deleted_authors = App\Author::onlyTrashed()->get();

5-3-5 関連性を持つテーブル群の値をまとめて操作する(リレーション)

  • authors(著者)は複数のbooks(書籍)を書いている
  • publishers(出版社)は複数のbooks(書籍)を販売している
  • 1対1、1対多、多対多

5-3-5-1 一対一の定義 - hasOne, belongsTo

  • hasOne
    • 上から下、正引き
  • belongsTo
    • 下から上、逆引き
  • パラメータ
    • 第一
      • 関連づけるModel
    • 第二
      • リレーションの外部キーがモデル名に基づいている場合
      • 自動的にModelは外部キーを持っている
      • この規約をオーバーライドしたければ、hasOneメソッドの第2引数を指定
    • 第三
      • リレーションで他のidを使う場合、カスタムキーを指定

リスト5.3.5.1:hasOneによる正引きリレーション

<?php
declare(strict_types=1);

namespace App;

use Illuminate\Database\Eloquent\Model;

class Book extends Model
{
    public function detail()
    {
        return $this->hasOne('\App\Bookdetail');
    }
}

リスト5.3.5.2:belongsToによる逆引きリレーション

<?php
declare(strict_types=1);

namespace App;

use Illuminate\Database\Eloquent\Model;

class Bookdetail extends Model
{
    public function book()
    {
        return $this->belongsTo('\App\Book');
    }
}

リスト5.3.5.3:リレーションされたカラム呼び出し

<?php

$book = \App\Book::fine(1);

echo $book->detail->isbn;    // リレーションされた書籍詳細の情報を取得できる

5-3-5-2 一対多の関係の定義 - hasMany

リスト5.3.5.4:hasManyによる一対多のリレーション

<?php
declare(strict_types=1);

namespace App;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
    public function books()
    {
        return $this->hasMany('\App\Book');
    }
}

リスト5.3.5.5:リレーションされたカラムの呼び出し - 逆引きは belongsTo を使う

<?php

$books = \App\Author::find(1)->books();

// Authorしか取得してないのに、著者に紐づく書籍が全て取得できる
foreach ($books as $book) {
    echo $book->name;
}

5-3-6 実行されるSQLの確認

  • ORMだからといって発行されるSQLを知らなくていいわけではない
  • 発行されるSQLによってはパフォーマンスに影響与えるよね
  • じゃあ実際に実行するSQLを出力してみよう

リスト5.3.6.1:適用されるSQL取得

<?php

$sql = \App\Author::where('name', '=', '著者A')->toSql();

リスト5.3.6.2:取得できるSQL - プリペアドステートメント化されている

select * from authors where name = ?

リスト5.3.6.3:getQueryLogでSQL取得

<?php

// SQL出力を有効化
DB::enableQueryLog();

// データ操作実行
$authors = \App\Author::find([1, 3, 5]);

// SQL取得
$queries = DB::getQueryLog();

// SQL出力を無効化
DB::disableQueryLog();

リスト5.3.6.4:getQueryLogで取得できるSQL配列 - findの中身はIN句に変換されている - 検索対象のテーブル、レコード数によってはパフォーマンスに影響する可能性がわかる - SQLを確認してより効率的な処理をしよう

<?php

array:1 [
    0 => array:3 [
        "query" => "select * from authors where authors.id in (?, ?, ?)"
        "bindings" => array:3 [
            0 => 1
            1 => 3
            2 => 5
        ]
        "time" => 11.55
    ]
]

中間まとめ

この後、クエリビルダ、リポジトリパターンと続きますが、分量が多い(現時点で3.7万字)ので、2つに分割します。Eloquentは便利だしOOPで実装するにはこっちのほうがいいのかなって思ったりしますが、次章のクエリビルダでも十分じゃないかな、と思ったりもします。

とはいえ開発現場や実装済みのプロダクト・サービスでは使われていること多いでしょうし、覚えておいて損はないし、いったんLaravelの横道に従って書いてみて、 Laravelで最速のコードを書ける ところまで行ければそれはそれで素晴らしいとも思いました。

データベースを取り扱う場合に、可能な限り抽象化されているというのはやはり利点の方が勝るとも思うので、どうにかしてEloquent、クエリビルダの両方を使ってみて、結果を検証したいなって思いました。

migration、Seeder、Fakerについてはもう、便利だなの一言に尽きます。いくら上手にERDを作っても、後からの追加変更は必ず起きると思います。

そんな時、コードで世代管理されていれば歴史も辿りやすいし、理解も早い。僕は実際にコマンドラインでデータベースに接続して見る派なのですが、そうでない場合はコードでサクッと見れる方が便利だよな、とも思いました。

テストデータもそれらしいのがサクッと作れ、こちらもまたコードで世代管理できる。むかーしC言語でスタブを書きまくっていた時代を振り返ると、便利になったもんだなあ、と感慨深いです。

僕は恥ずかしながら今まで、フレームワークの機能を正しく使って開発をした経験がほぼありません。なので本書を読んで、データベースの章でmigrationを実行し、Seeder、Fakerでデータを作ってクエリを実行してみる。

道具はやはり、使い方と使い道を間違えなければ非常に便利だな、って思いました。

この後も読み進め、終わった段階で自分の作りたいサービスをLaravelで作ってみようと思っています。

というわけで後半に続きます。