叫ぶうさぎの悪ふざけ

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

学習記録:12月13日(木):【MySQL8.0アップグレード】5.7.19->5.7.24->8.0.11 アップグレード手順【5.7.24 アップグレード前編】

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

MySQL8.0.11を触る必要が出てきて、じゃあ手元の環境をアップグレードしよう!と思い立ったけど一発でうまくいかなかったので、未来の自分のためにエントリーに残します。

現状

$ cat /etc/redhat-release
CentOS Linux release 7.3.1611 (Core) 
$ mysql --version
mysql  Ver 14.14 Distrib 5.7.19, for Linux (x86_64) using  EditLine wrapper

5.7.19->5.7.24

というわけでまず、5.7の最新版にアップグレードしましょう。

MySQL停止

忘れずに。

$ sudo systemctl stop mysqld.service

依存性のアップグレード

yumってなんぞ?って話はさらっと触れるんですが、一言ていうと パッケージの統合管理システム です。

パッケージの!統合管理システム!

なるほどよくわからん。

というわけでWikipedia先生から引用

パッケージ管理システム

パッケージ管理システムとは、OSというひとつの環境で、各種のソフトウェアの導入と削除、そしてソフトウェア同士やライブラリとの依存関係を管理するシステムである。

つまるところ、

  • OSや各種ソフトウェアが 必要としているライブラリを管理 していて
  • 不必要なライブラリをインストールしない ことで
  • ちゃんと動くようにパッケージを管理している

ってことかな(自信なし)

yumとは

そんなわけで…

依存関係(これからインストールしようとしている、MySQLコミュニティ版がどういうライブラリで構成されているのか)の情報を最新にするために、以下のコマンドを実行します。

$ sudo yum upgrade mysql-community-server
読み込んだプラグイン:fastestmirror
base                                                                                                                                                  | 3.6 kB  00:00:00     
epel/x86_64/metalink                                                                                                                                  | 4.9 kB  00:00:00     
epel                                                                                                                                                  | 3.2 kB  00:00:00     
extras                                                                                                                                                | 3.4 kB  00:00:00     
mysql-connectors-community                                                                                                                            | 2.5 kB  00:00:00     
mysql-tools-community                                                                                                                                 | 2.5 kB  00:00:00     
mysql57-community                                                                                                                                     | 2.5 kB  00:00:00     
remi-safe                                                                                                                                             | 3.0 kB  00:00:00     
updates                                                                                                                                               | 3.4 kB  00:00:00     
(1/5): epel/x86_64/updateinfo                                                                                                                         | 940 kB  00:00:00     
(2/5): extras/7/x86_64/primary_db                                                                                                                     | 156 kB  00:00:00     
(3/5): epel/x86_64/primary                                                                                                                            | 3.6 MB  00:00:00     
(4/5): updates/7/x86_64/primary_db                                                                                                                    | 1.3 MB  00:00:00     
(5/5): remi-safe/primary_db                                                                                                                           | 1.4 MB  00:00:04     
Loading mirror speeds from cached hostfile
 * base: ftp.tsukuba.wide.ad.jp
 * epel: ftp.yz.yamagata-u.ac.jp
 * extras: ftp.tsukuba.wide.ad.jp
 * remi-safe: ftp.riken.jp
 * updates: ftp.tsukuba.wide.ad.jp
epel                                                                                                                                                             12748/12748
依存性の解決をしています
--> トランザクションの確認を実行しています。
---> パッケージ mysql-community-server.x86_64 0:5.7.19-1.el7 を 更新
---> パッケージ mysql-community-server.x86_64 0:5.7.24-1.el7 を アップデート
--> 依存性の処理をしています: mysql-community-common(x86-64) = 5.7.24-1.el7 のパッケージ: mysql-community-server-5.7.24-1.el7.x86_64
--> トランザクションの確認を実行しています。
---> パッケージ mysql-community-common.x86_64 0:5.7.19-1.el7 を 更新
---> パッケージ mysql-community-common.x86_64 0:5.7.24-1.el7 を アップデート
--> 依存性解決を終了しました。

依存性を解決しました

=============================================================================================================================================================================
 Package                                          アーキテクチャー                 バージョン                              リポジトリー                                 容量
=============================================================================================================================================================================
更新します:
 mysql-community-server                           x86_64                           5.7.24-1.el7                            mysql57-community                           165 M
依存性関連での更新をします:
 mysql-community-common                           x86_64                           5.7.24-1.el7                            mysql57-community                           274 k

トランザクションの要約
=============================================================================================================================================================================
更新  1 パッケージ (+1 個の依存関係のパッケージ)

総ダウンロード容量: 165 M
Is this ok [y/d/N]: y
Downloading packages:
Delta RPMs disabled because /usr/bin/applydeltarpm not installed.
(1/2): mysql-community-common-5.7.24-1.el7.x86_64.rpm                                                                                                 | 274 kB  00:00:00     
(2/2): mysql-community-server-5.7.24-1.el7.x86_64.rpm                                                                                                 | 165 MB  00:00:07     
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
合計                                                                                                                                          23 MB/s | 165 MB  00:00:07     
Running transaction check
Running transaction test
Transaction test succeeded
Running transaction
  更新します              : mysql-community-common-5.7.24-1.el7.x86_64                                                                                                   1/4 
  更新します              : mysql-community-server-5.7.24-1.el7.x86_64                                                                                                   2/4 
  整理中                  : mysql-community-server-5.7.19-1.el7.x86_64                                                                                                   3/4 
  整理中                  : mysql-community-common-5.7.19-1.el7.x86_64                                                                                                   4/4 
  検証中                  : mysql-community-server-5.7.24-1.el7.x86_64                                                                                                   1/4 
  検証中                  : mysql-community-common-5.7.24-1.el7.x86_64                                                                                                   2/4 
  検証中                  : mysql-community-common-5.7.19-1.el7.x86_64                                                                                                   3/4 
  検証中                  : mysql-community-server-5.7.19-1.el7.x86_64                                                                                                   4/4 

更新:
  mysql-community-server.x86_64 0:5.7.24-1.el7                                                                                                                               

依存性を更新しました:
  mysql-community-common.x86_64 0:5.7.24-1.el7                                                                                                                               

完了しました!

無事に5.7.19 -> 5.7.24 へ、依存関係が更新されたようです。

MySQL 5.7.24 へのアップグレードを実行

以下のコマンドで、アップグレードを実行します。

何をしているかというと、MySQLの全データベースの全テーブルをチェックし、必要であれば修復し、アップグレードしています。

mysql_upgradeとは

まず公式を読んでみます。英語だけど頑張って翻訳機にかけて読むのだ。

公式から引用すると、以下の記述があります。

現在とこれからの非互換性を調べる

mysql_upgradeは、現在のバージョンのMySQL Serverとの非互換性について、すべてのデータベース内のすべてのテーブルを調べます

  • 現在のバージョン、つまり5.7.19と
  • これからインストールしようとする5.7.24で
  • 非互換性について
  • 全てのデータベース内の全てのテーブルを調べる

とあります。 つまり、廃止になった機能や型などがないかを全部チェックしてくれると解釈しました。

ここでもしエラーになれば、それを解決しないとバージョンアップできない、もしくは修復などできなかったテーブルは手動であとで修復してね、と僕は理解しました。

ちなみに MySQL 5.7.24 のリリースノート をみてみると、色々興味深い内容が書いてありますが、主題と大きくそれるので割愛します。

1つだけ言えるのは、アップグレードする際は、リリースノート読んで、影響範囲を事前に調べようね、ってことかなって思います。

システムテーブルもアップグレード

mysql_upgradeは、追加された可能性のある新しい特権や機能を利用できるように、システムテーブルもアップグレードします

思えばslow_logも今ではテーブルで管理できるようになっているわけで、そのほかにもたくさんの管理がシステムテーブルで管理できるようになっていると思います。

そういった、バージョンアップに伴いシステムテーブルで管理できるようになった内容なども、最新に対応するためにアップグレードしてくれるわけだな、と解釈しました。

互換性のないテーブルをチェック・修復

mysql_upgradeがテーブルに互換性がない可能性があると判断した 場合は、テーブルチェックを実行し、問題が見つかった場合はテーブル修復を試みます。表を修復できない場合は、2.11.3項「表または索引の再構築または修復」を参照して、手動表修復方法を確認してください

もし互換性がないテーブル(なくなった機能や非推奨のものなどが含まれる場合かな)がある場合は、

  • テーブルチェックを実行
  • 問題があればテーブル修復を試みる
  • 修復できない場合は手動修復

とあります。 実際にやってみたら、でかいテーブル(数百万〜1000万レコードのテーブルでした)でエラーが出ていたので、何かしら問題があったことがわかります。(アップグレード自体は完了しましたが、修復はされなかったようです)

これはまた mysql_upgrade を実行した後の問題解決として書いていきますので、まずは進めます。

アップグレードの都度、mysql_upgradeが必要

MySQLをアップグレード するたびにmysql_upgradeを実行する必要があります

公式に書いてある通りですね。マイナーバージョンアップであっても毎回 mysql_upgrade を使いましょうということでしょうか。

mysql_upgrade コマンド実行

というわけで、アップグレードするためにコマンドを実行し、各種チェックや必要であれば修復を行なっていきます。

わかる範囲でコメントを入れてますので、実際は # で始まるコメントはありません。

$ mysql_upgrade -uroot -p
Enter password: 
Checking if update is needed.
Checking server version.
Running queries to upgrade MySQL server.

# システムデータベースのチェックを実施
Checking system database.
mysql.columns_priv                                 OK
mysql.db                                           OK
mysql.engine_cost                                  OK
mysql.event                                        OK
mysql.func                                         OK
mysql.general_log                                  OK
mysql.gtid_executed                                OK
mysql.help_category                                OK
mysql.help_keyword                                 OK
mysql.help_relation                                OK
mysql.help_topic                                   OK
mysql.innodb_index_stats                           OK
mysql.innodb_table_stats                           OK
mysql.ndb_binlog_index                             OK
mysql.plugin                                       OK
mysql.proc                                         OK
mysql.procs_priv                                   OK
mysql.proxies_priv                                 OK
mysql.server_cost                                  OK
mysql.servers                                      OK
mysql.slave_master_info                            OK
mysql.slave_relay_log_info                         OK
mysql.slave_worker_info                            OK
mysql.slow_log                                     OK
mysql.tables_priv                                  OK
mysql.time_zone                                    OK
mysql.time_zone_leap_second                        OK
mysql.time_zone_name                               OK
mysql.time_zone_transition                         OK
mysql.time_zone_transition_type                    OK
mysql.user                                         OK

# システムスキーマはアップグレードに成功しました
The sys schema is already up to date (version 1.5.1).

# 次に自分のデータベースをチェック
Checking databases.
mamy1326.ag_media                                  OK
mamy1326.bukken_bad                                OK

# おっ、エラーが出ている。これはテスト用に突っ込んだ1000万レコードのテーブル
mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt

mamy1326.debug_log                                 OK
mamy1326.dr_log                                    OK
mamy1326.error_log                                 OK
mamy1326.hoge1                                     OK
mamy1326.item_details                              OK
mamy1326.items                                     OK
mamy1326.m_ad                                      OK
mamy1326.m_ad_base                                 OK
mamy1326.m_ad_group                                OK
mamy1326.m_agent                                   OK
mamy1326.m_client                                  OK
mamy1326.m_company                                 OK
mamy1326.m_contact                                 OK
mamy1326.m_log                                     OK
mamy1326.m_maintenance                             OK
mamy1326.m_media                                   OK
mamy1326.m_prefecture                              OK
mamy1326.m_status                                  OK
mamy1326.migrations                                OK
mamy1326.ng_log                                    OK
mamy1326.no_inc                                    OK
mamy1326.paths                                     OK
mamy1326.re_log                                    OK
mamy1326.report                                    OK
mamy1326.report_tmp                                OK
mamy1326.t_access_ip                               OK
mamy1326.t_ad_base_change                          OK
mamy1326.t_ad_change                               OK
mamy1326.t_alert                                   OK
mamy1326.t_carrier_ip                              OK
mamy1326.t_error_log                               OK
mamy1326.t_login                                   OK
mamy1326.t_mail_spool                              OK
mamy1326.t_resign                                  OK
mamy1326.t_retry                                   OK
mamy1326.t_session                                 OK

# おっ、ここもエラーが出ている。これもテスト用に突っ込んだ数百万レコードのテーブル
mamy1326.t_session_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/t_session_log.
Error    : Tablespace is missing for table `mamy1326`.`t_session_log`.
error    : Corrupt

mamy1326.time_stamp                                OK
mamy1326.time_stamp2                               OK
mamy_test.addr_test                                OK
mamy_test.bukken_bad                               OK
mamy_test.bukken_good                              OK
mamy_test.deleted_sites                            OK
mamy_test.m_user                                   OK
mamy_test.products                                 OK
mamy_test.products_status                          OK
mamy_test.tags                                     OK
mamy_test.timestamp_test                           OK
mamy_test.unique_test                              OK
mamy_test.wishlist                                 OK
sys.sys_config                                     OK

# 修復した結果を表示してくれる
Repairing tables
mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt
mamy1326.t_session_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/t_session_log.
Error    : Tablespace is missing for table `mamy1326`.`t_session_log`.
error    : Corrupt

# 基本的にはこのメッセージが出ていれば、アップグレードは成功
Upgrade process completed successfully.

# おや、何かファイルに書き込めないと言われている
Could not create the upgrade info file '/var/lib/mysql/mysql_upgrade_info' in the MySQL Servers datadir, errno: 13

まずは簡単そうなエラーから解決していきます。

Could not create the upgrade info file '/var/lib/mysql/mysql_upgrade_info' in the MySQL Servers datadir, errno: 13

/var/lib/mysql/mysql_upgrade_info ファイルが書き込めないと言われているようです。

ここは素直に /var/lib/mysql/パーミッションを変更して再実行してみます。

$ sudo chmod 775 /var/lib/mysql
$ ls -lda /var/lib/mysql
drwxrwxr-x 7 mysql mysql 4096 1213 22:53 /var/lib/mysql

再実行

Checking if update is needed. と表示され、無事エラーが出なくなっていることが確認できます。

$ mysql_upgrade -uroot -p
Enter password: 
(中略)
Upgrade process completed successfully.
Checking if update is needed.

テーブル修復を試みた原因

先ほどのmysql_upgradeで、テーブルに問題があり、修復を試みた結果、どうやら失敗していたテーブルが2つありました。エラーはどちらも同じ内容でした。

mamy1326.c_log
Warning  : InnoDB: Tablespace is missing for table mamy1326/c_log.
Error    : Tablespace is missing for table `mamy1326`.`c_log`.
error    : Corrupt

試しにSELECTしてみます。

mysql> select * from c_log limit 1;
ERROR 1812 (HY000): Tablespace is missing for table `mamy1326`.`c_log`.

mysql_upgradeで出ていたエラーと同じ内容が表示され、SELECTできないことがわかります。

ここの調査などについては、明日に持ち越したいと思います。

やってみて

マイナーバージョンアップ、実は初めてやりました。

やってみて、これ結構ハマるんだな、古いテーブルだったりすると結構クリティカルだな、と思うと共に、

  • 正確な実行方法
  • リリースノートの内容からの影響範囲調査

をちゃんとやっとかないといきなり躓くよね、と強く思いました。

正直マイナーバージョンアップは軽い気持ちで実行して、サクッと8.0まであげちゃうぞ!とか思ってたんですが、色々と腰を据える必要がありそうです。

しかし、こういうのを1つずつ調べて進めていく、わからなかった原因やエラーがわかっていく。

ほんと楽しいですね。

色々調べているうちに、時間を忘れて2時間ほど経過していて、アドベントカレンダーに間に合わないところでしたw

というわけでこれも継続していくぞ!

学習記録:12月12日(水):【輪読会まとめ 後編パート2・完結編】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン)

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

そして昨日のエントリー 学習記録:12月11日(火):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン) の続きで、データベースの章の読了完結編です。

昨日はリポジトリパターンの実装で、

  • データ周りの要求変更に負けない設計パターンを取り入れる
  • ビジネスロジックからデータ操作を別レイヤへ

という内容を読み進め、まずは サービスクラスとデータベースアクセスが密結合 な実装をするところまでで終わっていました。

よって今日は リファクタリングで他のデータストアへの変更、モック差し替え のところまで進め、本章を読了したいと思います。

4. リファクタリング

ビジネスロジックを受け持つFavoriteServiceクラスを改めて確認してみるところから。

  • 「いいね」のデータ登録部分は EloquentであるFavoriteクラスに依存
    • つまりMySQLに接続できるEloquentが前提になっている

ビジネスロジックから特定のデータベース操作を取り除く

以下の順序で本質部分以外を取り除き、抽象化を進めていきます。

  1. Repositoryを抽象化するインターフェースを作成
  2. データベース操作を担当するRepositoryクラス作成
  3. Serviceクラスはインターフェースを参照
  4. インターフェースと具象クラスを紐付ける

「いいね」データのON・OFFが可能である、というのがビジネスロジック(Serviceクラス)の本質だったので、「ON・OFF切り替え操作を持つ、いいねデータのリポジトリ」にデータベースの処理を抽象化します。

で、データベース処理の抽象化を表現したインターフェースとして、次のコードを作成します。

リスト5.5.2.13:リポジトリインターフェース

というわけでインターフェースのコードです。このインターフェースの役割を表すということにもなるので、具体的な内容は書かず、メソッド定義のみになります。

具体的な内容は、それこそ具象クラスである、この後の具象クラスに実装します。

インターフェースのファイル名は app/DataProvider/FavoriteProviderInterface.php です。

<?php
declare(strict_types=1);

namespace App\DataProvider;

interface FavoriteRepositoryInterface
{
    public function switch(int $bookId, int $userId, string $createdAt): int;
}

余談

余談ですが、インターフェースはちょうど先日のぺちオブでも出てきて、実際のコードや具体例と共に解説を受けてきました。

そのあとでこの部分を実装してみると、わかりみが深いです。いずれ自分の言葉でSOLID原則をきっちり説明する試みを実行したいと思っています。

インプットはいただいたので、あとはアウトプットだ。

リスト5.5.2.14:リポジトリインターフェースを実装した具象クラス

  • app/DataProvider/FavoriteRepository.php を作成
  • コンストラクタインジェクション(依存性の注入)でデータベースアクセスを行うFavoriteクラスを注入
    • 注入自体はサービスクラスで実施
  • これでデータ操作の実処理はリポジトリクラスに移動
<?php
declare(strict_types=1);

namespace App\DataProvider;

use \App\DataProvider\Eloquent\Favorite;

class FavoriteRepository implements FavoriteRepositoryInterface
{
    private $favorite;

    public function __construct(Favorite $favorite)
    {
        $this->favorite = $favorite;
    }

    public function switch(int $bookId, int $userId, string $createdAt) : int
    {
        return \DB::transaction(
            // いいねがなければ作り、あれば削除、結果を0/1で返す
            function () use ($bookId ,$userId, $createdAt) {
                $count = $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    $this->favorite->create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                $this->favorite->where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
    }
}

リスト5.5.2.15:サービスクラスのリファクタリング

インターフェースと、その中身を表す具象クラスを作成しました。

次は実際のサービスクラス(Controller/Actionから利用されるビジネスロジック)をリファクタリングします。

前回までは use App\DataProvider\Eloquent\Favorite; でEloquentを直接利用してました。これを抽象クラスであるインターフェースを用い、コンストラクタインジェクションで注入する形式に置き換えていきます。

僕自身の理解のため、既存のコードをコメントアウトし、なぜ変更したかを自分なりの言葉で書いて進めます。

<?php
declare(strict_types=1);

namespace App\Services;

// Eloquentの直接利用をやめ、インターフェースクラスを利用
//use App\DataProvider\Eloquent\Favorite;
use App\DataProvider\FavoriteRepositoryInterface;

class FavoriteService
{
    /**
     * @var FavoriteRepositoryInterface コンストラクタインジェクションで注入されるインターフェースを受け取る変数を用意
     */
    private $favorite;

    /**
     * Create a new service instance.(クラスのインスタンスが作成される際に、インターフェースの注入を受け取り保存するコンストラクタ)
     *
     * @return void
     */
    public function __construct(FavoriteRepositoryInterface $favorite)
    {
        $this->favorite = $favorite;
    }

    /**
     * Switch to Favorite Status
     *
     * @param  int $bookId
     * @param  int $userId
     * @param  string $createdAt
     *
     * @return int  turn on:1 / turn off: 0
     */
    public function switchFavorite(int $bookId, int $userId, string $createdAt): int
    {
        return $this->favorite->switch($bookId, $userId, $createdAt);
        // ここを丸ごとRepositoryクラスへ外出し、インターフェースを通して抽象化
        // サービスはインターフェースだけ考えればいい
        /*
        return \DB::transaction(
            function () use ($bookId, $userId, $createdAt) {
                $count = Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    Favorite::create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt
                    ]);
                    return 1;
                }
                Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
        */
    }
}

解説

これで、同じインターフェースを持つクラスであれば、なんでも動作する状態になりました。

何が嬉しいかというと、例えばテストで データベースにアクセスしない、モッククラスを作った として簡単に差し替えが可能です。

また、他のデータストアを使うことになっても、サービスクラスには関係なく、具象クラスを作って差し替えるだけで良い、という状態にもなりました。

コントローラもこのサービスクラスを使うので、データストアが変わっても影響を受けない状態となります。

リスト5.5.2.16:インターフェースと具象クラスのバインド

現段階では、インターフェースも具象クラス(Repository)も作っただけで、関連づいていません。

そこで、関連づけ(バインディング)を実行します。これでControllerからRepositoryまで一通り繋がり、動作することになります。

なお、書籍には 新たにサービスプロバイダを作成しても構いません とありますが、いったんは書籍の例で動作させてみます。

<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        // インターフェースクラスとリポジトリ(具象)クラスを関連づけ
        $this->app->bind(
            \App\DataProvider\FavoriteRepositoryInterface::class,
            \App\DataProvider\FavoriteRepository::class
        );
    }
}

curlコマンドで実行確認

実行してみると、実際に動きますね(当たり前)

写経してるのでtypoしたりreturn忘れて返り値の型が違うよと怒られたりしましたが、スルッと動くと気持ちいいですね!

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=1&user_id=2' --write-out '%{http_code}\n'
200

というわけでDBを確認してみます。

# 動かす前、レコードなし
mysql> select * from favorites;
Empty set (0.00 sec)

# 動作後、いいねレコード作成
mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       1 |       2 | 2018-12-12 14:28:09 | 2018-12-12 14:28:09 |
+---------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

# 再実行、いいね取り消しのためレコード削除
mysql> select * from favorites;
Empty set (0.00 sec)

別のbook_idで複数レコード作るのを確認

book_idが違うリクエストでAPIを実行します。

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=2&user_id=2' --write-out '%{http_code}\n'
200
$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=3&user_id=2' --write-out '%{http_code}\n'
200

2レコード作られていることがわかります。

mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       2 |       2 | 2018-12-12 14:29:20 | 2018-12-12 14:29:20 |
|       3 |       2 | 2018-12-12 14:29:36 | 2018-12-12 14:29:36 |
+---------+---------+---------------------+---------------------+
2 rows in set (0.01 sec)

章の終わり

いいねの操作などは、Queueを使用して非同期でもいいでしょうとあります。別の方法も採用できる実装例で進んできたことをここで知ることになるとは、うまい構成だなあとか思ったりしてました。

また、前述しましたが、データストア先を変更する場合は、同じインターフェース(FavoriteRepositoryInterface)を持ったデータ操作クラス(具象クラス)を作成して、バインドしなおせば、ビジネスロジック(サービス)を変更せずとも差し替えが可能になります。

リポジトリパターンは、今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません。

しかしシステムの要件や規模の拡張が見込まれるサービスでは、検討に値するデザインパターンです、という言葉で章が締められています。

やってみて

で、ここからは意訳で僕の言葉ですが、

「今まで登場してきた各クラスを疎結合にできる反面、当然ながらクラス数が増えるため、デモや期間限定の機能では不要かもしれません」

という話は、システム全体からみたら逆にレアケースだと思います。要件が追加されないサービスなんか見たこともないし、拡張がされなかったシステムも見たことがないです。

リポジトリパターンが全てを解決するわけじゃ決してないと思いますが、漫然と今のままの実装を続けるのではなく、サービスの特徴を鑑みて適切なデザインパターンを検討し採用する ってことだよな、と思いながら読みました。

また、実際に僕が実装してみた印象でも、どんどん責務が分割されていき、綺麗に繋がっていくのを体感できました。 責務が分割され単一になるということは、より1つのことに集中してコードが書けるということだよな、と実感しました。

クラス数が増える=繁雑になる、では決してない と思います。 むしろ 適切に責務が分割され、1つずつ確実に実装を進められ、悩むことが少なくなる ことを実感しました。

さいごに

オブジェクト指向での実装を進めると、1つのクラスの責務、1つのメソッドの責務は単一責任の原則(だったかな)に則るのが普通だとも思います。

実際に業務で書いているコードに関しても、どんどん分割し、責務を分け、メソッド名を付ける際に2つ以上の責務を持たないか考え、実装を進めたりしています。

非常にスッキリしますし、互いのメソッドやクラスが疎結合になっていくので、のちのメンテも楽になるなあ、って話を上司としたこともあります。

データベースの章を通読し、コードを全部実行し、自分なりの言葉でアウトプットしてみたわけですが、ぺちオブでの勉強会のインプットも相まって、とてもすんなり内容が入ってきました。

今後もデザインパターンを意識しつつ、納期、リソース、要件、などなど、様々な内容から、最適な技術を採用していこうと思わせていただきました。

この章だけでも購入するに十分な理由になると思います。

Laravelやってみたいな、オブジェクト指向で実装するってなんだっけ?って思ってる人がもしいらっしゃいましたら、いろんな意味で読んでみることをオススメします。

別に回し者でもないですが、時間をかけ、自分の中の情報を関連づけて進めていくことで、割とバラバラだった知識が繋がりましたし、自分の中に浸透していなかったインプットが着実に刻まれていく感覚を味わうことができました。

かいつまんで読み進めているので全部読み切るのにまだ結構かかりそうですが、少しずつ進めて、確実にモノにしていこうと思います。

はー楽しかった!!!!

学習記録:12月11日(火):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン)

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

学習記録:12月9日(日):【輪読会まとめ 後編パート1】Laravel Webアプリケーション開発 Chapter5 データベース の後編の続きなのですが、まずもって「リポジトリパターン」をOOPの理解を含めて自分の言葉にするのと、リファクタリングを行う前のソースコードを生成していくので2時間ほどかかりました。

よって後編パートをさらに分割し、今日はリファクタリング前のコードを書くところまで実行します。

【抽象化って】読み進め、その前に【なんやねん】

抽象化って言葉を掘り下げたい。いったいなんやねんな。まじわからんし、ふわっとしすぎなんじゃー!って思ったそこのアナタ。そう(強引)、僕もそうでした。

なので僕なりに解釈した「抽象化」を表してみます。

  • 書籍販売のECがあるとする
  • プログラム、層、サービス、などそれぞれに責務がある
  • 例えばサービス(ドメイン?)の責務はユーザーにサービスを提供すること
    • そこに「どのデータストアを使うのか」という責務は含まれない
  • 書籍販売ならただ以下ができればいいはず(細かいことは割愛)
    • 「書籍一覧を取得」できて
    • 「購入ボタンが押せ」て
    • 「カートに商品が入っ」て
    • 「購入できる」
  • つまり、書籍販売のECの責務からしたら、書籍販売ができれば良い
    • 本来、MySQLなのかPostgreSQLなのかは意識させてはいけない
  • より本質的な責務に集中し、追加しやすく変更に強い作りにする必要がある
    • そのため、本質的なところ以外は「外出し」して、自分の責務に集中させる
  • 本質的以外のところは意識の外に出す
  • これが抽象化!

もちろん全然違うよ!こうだよ!って意見があると思うし、僕自身、理解の途上にいるので、異論反論などバンバン受け付けています!マサカリ大歓迎!

リポジトリパターン

  • データ周りの要求変更に負けない設計パターンを取り入れる
  • ビジネスロジックからデータ操作を別レイヤへ
    • リポジトリ層へ移し分離・隠蔽
    • メンテナンス性やテスト容易性を高める

5-5-1 リポジトリパターンの概要

  • アプリケーションのデータストアは様々
  • RDB、NoSQL、キャッシュ、ファイル、SaaSAPI、etc...
  • テストコードでは本番とは違うデータベース使うこともある
    • そもそもDB使わない場合もある
  • そんなデータストアの参照先が変わっても
    • プログラムの変更範囲は限定的にしたい!
    • それ、リポジトリパターンで解決できるかも!

ただ図をみるのもなんなので、書いてみました。僕の場合、図は自分で書かないと理解できないのです…

image.png (204.7 kB)

リポジトリパターン、僕なりの解説

①密結合

  • 状態
    • ビジネスロジックMVCならModel、DDDならサービスクラス)の中に
    • データアクセスクラス(SQL書いてるやつ)があって
    • RDBMSに読み書きしている
  • 問題点
    • データストアが変わった場合、ビジネスロジックの層に改修が入る
    • 影響範囲が大きい
    • 適切に切り離していないのでテストしにくい

ビジネスロジックからデータストアを切り離す

  • 状態
  • 問題点
    • データストアが変わればインジェクションされる型などは修正必要
    • 依然、ビジネスロジックに修正は必要な状態

③インターフェースを用いて疎結合な状態に

  • 状態
    • インターフェースクラスを用意、データストアそのものを抽象化
    • インターフェースクラスをインジェクションされるビジネスロジック
      • データストアがある、というインターフェースだけ意識すれば良い
      • ここの型などはインターフェースで抽象化し具象クラスを吸収
    • 具象クラスが変わっても、インターフェースクラスで吸収している
      • データストアが変わったり複数になってもビジネスロジックに影響はない
      • ここの理解はまだ甘い
  • 問題点
    • 最初に設計し、インターフェースを実装しておく必要がある
    • やはり後からの変更は大変
    • 設計大事

文中の解説

  • ビジネスロジックからデータアクセス処理を切り離した
    • 特定のデータストアに依存しなくなった
    • 「何らかのデータ格納庫(リポジトリ)に対して操作する」レイヤを用意
      • インターフェース
    • ビジネスロジックはデータストアが何か意識することなく
      • 保存、検索の操作が可能になる
  • 本節では実際にコードのリファクタリングを行っていく

5-5-2 リポジトリパターンの実装

  • 最初はサービスクラスとデータベースアクセスが密結合
  • リファクタリングで他のデータストアへの変更、モック差し替え

1.アプリケーション仕様

  • いいねをつける機能をWebAPI
  • URLは /api/action/favorite
  • テーブルはLaravel標準のusers, 「5-1」で作ったbooks, 新規作成のfavorite

表5.5.2.1:favoritesテーブル

カラム 備考
book_id integer FK
user_id integer FK
created_at timestamp
updated_at timestamp

2.テーブルの作成

リスト5.5.2.2:favoritesテーブル作成するマイグレーションファイル

$ php artisan make:migration create_favorites_table
Created Migration: 2018_12_11_140814_create_favorites_table
$ ls -la database/migrations/*favorites*.php
-rw-r--r-- 1 laradock laradock 598 Dec 11 14:08 database/migrations/2018_12_11_140814_create_favorites_table.php

リスト5.5.2.3:favoritesテーブルを作成するコードを記述

<?php

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

class CreateFavoritesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('favorites', function (Blueprint $table) {
            $table->integer('book_id');
            $table->integer('user_id');
            $table->timestamps();
            $table->unique(['book_id', 'user_id'], 'UNIQUE_IDX_FAVORITES');
        });
    }

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

リスト5.5.2.4:マイグレーション実行 無事テーブル作成完了。ちなみにLaravelは複合主キーを扱えない(扱いにくい?)ため、 $table->unique(['book_id', 'user_id'], 'UNIQUE_IDX_FAVORITES'); とuniqueで代用しているそうです。

$ php artisan migrate
Migrating: 2018_12_11_140814_create_favorites_table
Migrated:  2018_12_11_140814_create_favorites_table

リスト5.5.2.5:テーブル定義

mysql> desc favorites;
+------------+-----------+------+-----+---------+-------+
| Field      | Type      | Null | Key | Default | Extra |
+------------+-----------+------+-----+---------+-------+
| book_id    | int(11)   | NO   | PRI | NULL    |       |
| user_id    | int(11)   | NO   | PRI | NULL    |       |
| created_at | timestamp | YES  |     | NULL    |       |
| updated_at | timestamp | YES  |     | NULL    |       |
+------------+-----------+------+-----+---------+-------+
4 rows in set (0.01 sec)

mysql> show create table favorites\G
*************************** 1. row ***************************
       Table: favorites
Create Table: CREATE TABLE `favorites` (
  `book_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  UNIQUE KEY `UNIQUE_IDX_FAVORITES` (`book_id`,`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)

3.コードの作成

以下3つのファイルを作る。

  1. データベースアクセスを受け持つEloquent(Favorite)
  2. ビジネスロジックを受け持つサービスクラス(FavoriteService)
  3. リクエストを受けるコントローラクラス(FavoriteAction)

Eloquentクラスの実装

以下のコマンドでファイル作成する。

リスト5.5.2.6:Favoriteモデルの生成

$ php artisan make:model DataProvider/Eloquent/Favorite
Model created successfully.

リスト5.5.2.7:Favoriteモデルの実装

<?php

namespace App\DataProvider\Eloquent;

use Illuminate\Database\Eloquent\Model;

class Favorite extends Model
{
    protected $fillable = [
        'book_id',
        'user_id',
        'created_at'
    ];
}

サービスクラスの実装

  • ここはartisanではなく手動でファイルを設置
  • 主な仕様
    • switchFavoriteメソッド内でEloquentを呼び出し
    • 「いいね」データ登録を行う
    • ただし、書籍コードとユーザーIDの組み合わせが既に存在する場合
      • レコードを削除して「いいね」を取り消す

リスト5.5.2.8:ビジネスロジックを受け持つFavoriteServiceクラスの実装 (「これはいかんな」ってのが自分にもわかるぞ、と思いながら書いてた。)

<?php
declare(strict_types=1);

namaspace App\Services;

use App\DataProvider\Eloquent\Favorite;

class FavoriteService
{
    public function switchFavorite(int $bookId, int $userId, string $createdAt) : int
    {
        return \DB::transaction(
            function () use ($bookId, $userId, $createdAt) {
                $count = Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->count();
                if ($count == 0) {
                    Favorite::create([
                        'book_id' => $bookId,
                        'user_id' => $userId,
                        'created_at' => $createdAt,
                    ]);
                    return 1;
                }
                Favorite::where('book_id', $bookId)
                    ->where('user_id', $userId)
                    ->delete();
                return 0;
            }
        );
    }
}

Action(Controller)の実装

  • 主な仕様
    • ユーザーリクエストを受ける
    • FavoriteServiceクラスに処理を渡す
    • コンストラクタインジェクションでFavoriteServiceクラスのインスタンスを内部保持
    • 戻り値はHTTP_OK
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Services\FavoriteService;
use Illuminate\Http\Request;
use Carbon\Carbon;
use Symfony\Component\HttpFoundation\Response;
class FavoriteAction extends Controller
{
    /**
     * @var \App\Services\FavoriteService
     */
    private $favorite;
    /**
     * Create a new Controller instance.
     *
     * @return void
     */
    public function __construct(FavoriteService $favorite)
    {
        $this->favorite = $favorite;
    }
    /**
     * Switch to Favorite Status
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return response
     */
    public function switchFavorite(Request $request)
    {
        $this->favorite->switchFavorite(
            (int)$request->get('book_id'),
            (int)$request->get('user_id', 1),
            Carbon::now()->toDateTimeString()
        );
        return response('', Response::HTTP_OK);
    }
}

routing設定

エンドポイント(外部から叩くURI)を登録する。

<?php

Route::post('/action/favorite', 'FavoriteAction@switchFavorite');

動作確認

エラー出てますな…。あとで直します。

修正しました。Eloquentでsyntax出てるやん…。お恥ずかしい。

ちゃんと200が返ってきました。

$ curl 'http://localhost/api/action/favorite' --request POST --data 'book_id=1&user_id=2' --write-out '%{http_code}\n'
200

データ確認

ちゃんとデータ登録されていますね。いやーよかったよかった。

mysql> select * from favorites;
+---------+---------+---------------------+---------------------+
| book_id | user_id | created_at          | updated_at          |
+---------+---------+---------------------+---------------------+
|       1 |       2 | 2018-12-11 15:44:23 | 2018-12-11 15:44:23 |
+---------+---------+---------------------+---------------------+
1 row in set (0.00 sec)

やってみて

分量が…

明らかに分量が増えすぎていて、今回でも終わりませんでしたな(遠い目

しかし、それだけ理解するためのスタートは切れているってことだし、これをアウトプットの形に結実させれば、リポジトリパターンと抽象化と疎結合に関しては、一度道を通ったことになるかなって思ってます。

抽象化

抽象化を本当に1分で言えと言われるとこうなるのかな、とか思いつつ、まだまだ自分の言葉にできてないよなって思ってます。そもそもこの章のコード動いてないし。もうちょいがんばろ。

抽象化を解説してみた

自分が本当に解説できるのか、というのと、エンジニアリングにまるで関係のない人に通じるのか、どう思うのか、を聞いてみました。思った通りというか、11月前半の僕と大して変わらない認識でちょっと安心。

これを奥さんに理解できるように解説できたらまずはよし、って感じかなあ。

  • プログラムってのがあってだな
  • 奥さん:(ぬいぐるみを頷かせる)
  • こいつはユーザーにサービスを提供するのが責務
  • 奥さん:(ぬいぐるみを頷かせる)
  • その責務にどのデータベースを使うのか、は関係ない
  • 奥さん:(ぬいぐるみを左右に傾かせて「はてー?」)
  • データベースはMySQLとかPostgreSQLとかOracleとかいっぱいある
  • 奥さん:(ぬいぐるみを頷かせる)
  • それがサービスの成長にしたがって他のデータベースも併用しなくちゃいけなくなった
  • 奥さん:(ぬいぐるみを頷かせる)
  • もしこの時点でプログラムが「MySQLしか使えませーん」となってると全部修正しなくちゃいけない
  • 奥さん:(ぬいぐるみをビクッと動かして驚いたアクション)
  • めっちゃ時間もお金もかかる
  • 奥さん:(ぬいぐるみを大きく動かして「大変だー」のアクション)
  • じゃあどうするか。じゃーん!「抽象化!」
  • 奥さん:(抽象化!と手を突き上げるアクション)

というわけで引き続き…

というわけで、リファクタリングは次回に続きます!楽しい!

学習記録:12月10日(月):はじめよう!要件定義 Chapter-03&Chapter-04

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

かつ、学習記録:12月1日(土):はじめよう!要件定義 Chapter-01&02 の続きでもあります。

たぶん1日目にも言った気がするんですが、現在の業務にも密接に関わってくる内容だし、著者の羽生さんに教えを受けた(主催の勉強会に参加した)身としては、通読して業務に活かさなければならない。

というわけで続きを読み進めてみます。

前回までのあらすじ

第1部 要件定義ってなんだろう?、の以下を読み進めていました。

Chapter-01 要件定義=要件を定義すること

あらすじというか、書籍から得たものはだいたい以下の2つに集約される気がします。

Chapter-02 要件定義の基本的な流れ

クライアントの思いをクライアント自身が外に出すのが要求。 それを受け取ったら、実現可能性を含めて検討し、提案する。 提案には当然プロの視点が含まれているし期待されており、実現できたとしてもより良い方法があれば提案する必要がある。

しかし代替案を出すのは相手を否定することから始まるので、プロセス、理由、伝わりやすさを考えて正しく伝える必要があるよね、と。ここから受けた僕の印象としては、やっぱりお互いに尊敬を忘れないのが大事だなとも思いました。

そしてこのプロセスを繰り返し回すことで要件定義にたどり着くけど、それを面倒臭がっちゃいけない。人間はわかりあうことが難しい。諦めたらダメよ、と。

クライアントとの対話をしていく。そこから全てが始まるよって、要件定義とは お互い良い仕事をしていくための知恵 だよ、というところで、結ばれていました。

では引き続きChapter-03へ。

Chapter-03 定義すべき要件の内訳

ゴールから逆算する

ゴールから逆算するってどういうこと?の解説から入ります。

「目玉焼きを提供する」の例

これがクライアントから出てくる要求である、と理解してます。

  • ゴールはこの「目玉焼きを提供する」という成果を出すのがゴール
    • 目玉焼きが欲しいから
    • 目玉焼きがある、という状態を実現するため
    • 目玉焼きを作る、という依頼をコックさんに出す

ソフトウェア開発の場合

要件がないとダメよね、ということが解説されています。実現可能性や代替案を考える前の情報交換かな、と理解しました。

  • コックさんの代わりにプログラマ
  • 完成したソフトウェアを存在する、という状態を実現する
  • では完成したソフトウェアとは?
    • 作ってと言われて「はいわかりました」とは言うには情報が足りない
    • だって完成したソフトウェアがこの時点でどういうものかわからない
    • 要件が足りないよね

要件として必要な内容とは

ここで初めて、要件定義をする、実現可能性や代替案を考えるための情報が揃ってくることになるのかな、と思いました。

  • プログラマがソフトウェアを完成させるために必要な情報
    • UI
    • 機能
    • データ

というわけで上記3つの解説に入っていきます。

UI

ユーザーに接するものだよね、そしてそれは現代における概念に寄せるよね、というようなことが書いてあると理解しました。ごくごく当たり前のことですが、古くは帳票、パンチカード、などなど、様々な歴史がありました。

僕のようにそこそこ年齢がいっていると、それら全てを経験してたりします。が、それに囚われず、時代に合わせた表現や概念は重要だなって思います。

  • User Interface
    • ユーザーに接するもの
  • 古くは、画面や帳票
    • しかし今は古い概念
    • スマホ当たり前の時代だから
    • 業務システムでは今でも重視される要項のひとつ
    • だがしかし、これ以降はUIといえば画面系、で進める

機能

つまりはソフトウェアにやらせる仕事の総称。コンピュータへの入力から仕事をし、結果を出力する流れである、という基本的なことが書かれており、機能も処理も同じ意味ですよ、とあります。

言葉の定義は大事ですよね。

データ

消費税、なんとか金額、など。ここはさらりと触れて終わってますが、UI(画面)、機能、データ、どれも重要だし、画面とデータは密接だし、色々考えるところあるなあと思いつつ読んでました。

UI、機能、データを定める

この3つの要素が明確に定まって初めて、プログラマはソフトウェアを作れますよ、と。

つまりはこの3つの要素がソフトウェア開発における要件に必要な情報となりますね、と。

余談ですけど、定まってないのにとりあえず作るとかあるなとか、その場合うっかり実装まで行っちゃうことあるよなとか、サクッとモック作って見せるのが最強だよなとか、色々思いながら読んでいました。

で、ここでもゴールから逆算するのが重要なので、大雑把な流れを見ていくことになります。

Chapter-04 3つの要素の定め方

何はともあれUI

そりゃそうだよな、と思うことも、こうまで徹底して解説されると最高の納得感があります。

そもそも何を持って完成したと納得できるのか

と言う問いかけが文中にありますが、ほんとこれを常に意識し続ける必要あるよなって思います。

開発してるとついつい実装に没頭して忘れちゃうことあるし。

作り手がいくら「できました!」と言っても、完成の定義からずれてたり、定義そのものが曖昧なら「完成」を迎えるのは難しいですから。

では完成とはどう確認するのかと言うと

UIを通じてソフトウェアを操作すること

と断言されています。 僕自身にも異論は全くないです。

UIって?

自動車を例にすると、エンジンも回るしタイヤも向きが変わるしブレーキも効きますよと言っても、それぞれむき出しで実装されたら「いやいや普通の人が運転できないでしょこれ」ってなるよね、と。

ソフトウェアも同じで、完成したことを確認できる為にも、操作者が使うことのできるUI でなくてはならない、と書かれています。

心の底から同意します。 利用するのにテクニックや熟考が必要な時点で、UIは破綻してるよな、って常々思います。

操作に対して機能が動作すること

綺麗な画面だろ、でもこれ動かないんだぜ…なんて言語道断。完成したとは言えないよね。 UIを通じて操作すると、期待通り動かないと納得できない。

操作に対してきちんと機能が動作すること

が必要である、と書かれています。当たり前ですけど、自己満足でもいけないし、拡大解釈すると、押せそうな感じになってて押せないとか、押せなさそうなのに押さないと進まないとか、そういうのもダメだよなって思いながら読んでます。

機能に必要なデータが揃っていること

すごく当たり前なことが例に書かれてますが、ネットショップで買い物して注文データが記録されなかったら大変だよね、と。

機能が必要とするデータがきちんと揃っていること

これが必要である、と。

これを必要なこと三人衆の最後に持ってくるのは意味があるのかな。どれもこれも大事だけど、僕自身は 最終的に永続化されるデータ に落として初めて回りだすと思ってもいるので…うん、どれもこれも並列で大事だな。

やっぱりどれかを大事にするんではなく、三位一体で回していくことが重要、要求、要件の検討と提案、を繰り返す、その中の三本柱とも言えるんだな、と一人納得しながら読んでました。

UI、機能、データを決める

このように、ユーザーが何をしたいのか、ゴールから逆算すると言うことは

  • 納得する順序を把握して
  • それを満たすために必要なことを決めていく

ということだと書かれています。これまたごくごく当たり前ですけど、これを間違って酷いことになった現場やプロジェクト、サービスを結構な数見てきました。

で、完成したソフトウェアという成果をだすための要件を決めるには

  1. UIを決める
  2. 機能を決める
  3. データを決める

大雑把だけど、この流れで進めていくんだよ、と。

じゃあ具体的にこの3つをどう決めていけばいいのか。

UIを決めるとはどういうことか。機能とは具体的にどんなことを定めるのか。

というわけで、第二部からは要件定義の詳細を順番に見ていくことに。今から楽しみです。

要件定義の心がけ

要件定義のために要件定義をするなよ、とあります。

なんのこと?と思わなくもないですが、なんとなく予想もできます。

依頼主が求めること

完成したソフトウェアであり、そのソフトウェアがもたらしてくれる便益である、と。

それを達成し、本当の意味で 完成としての納得が得られる よねと。

便益(ベネフィット)はどう得るの?

実際にソフトウェアが完成して利用し始めないと、つまり運用してみないと判断できないですよね。 ゆえに何はともあれ 完成して利用できる状態にする のがゴール設定である、とあります。

品質は一定以上ないとダメですけど、そのチームの状態(人員、リソース、スキル)、予算、期間を鑑みて、ゴールするにはどうするのかを考えて要件定義をし、チームを作り、開発を進め、リリースしていく。

そうして初めて、クライアントの要求に応えるための第一歩を踏み出せるのだよな、と改めて思ったりしてました。

ソフトウェアを利用できるには何が必要?

ユーザーが使えることが大前提。デプロイですよねと。どうでもいいんですが、デプロイって「配備」って意味なんですね。知りませんでした。

とはいえ、デプロイしたからって言ったって、品質がボロボロじゃあどうにもならん。すなわちテストせねばならんよな、と。

プログラマは行き当たりばったりで作ってもだめだし、要件に基づいて作業せねばならん。 その結果として、一定の品質のソフトウェアができてこと利用できるってことだよなって改めて考えつつ読みました。

ソフトウェア開発プロセス

企画->業務設計->要件定義->設計->実装->テスト->導入->リリース

という一連のプロセスがある。 このプロセスの中には、書籍のスコープから外れる「プロジェクトマネジメント」の理解も必要。 とはいえいっぺんに理解は難しい。

しかし利用できないソフトウェアは無価値。

ソフトウェアを実現するために要件定義という工程が必要。要検定義をせずに後工程に先送り(設計でカバーとか)すると、必ずそこで要件定義は発生するよねと。

定まってないものは作れないし、たとえ先行で作ったとしてもそれは完成には至らないし、出来上がらない、つまり事実上作っていないものはテストできない。テストできてなければデプロイできない。

後工程につなぐための要件定義

であることを忘れるなよ、とあります。

後工程に対する理解を深める

後工程に繋ぐことをおざなりにしたら、それは要件定義が終わってないということ。それで前に進むのは 要件定義のでっち上げ になる。

でっち上げた要件で作ったものは納得を得られないし、ちゃんとできたとも言えない。 そのようにならないためにも要件定義をやり、これなら自分でも作れるぞ、という 後工程への理解 を深める必要があるんだなってことが書いてあります。

後工程の理解を深めない=不要な人

要件を定義さえすればいい、後工程は実装者が考えるから自分はそれ以降知らなくていい、なんてことで進めた要件定義は実務に耐えられない。

結果、後工程で要件定義をやり直さないとプログラミングできないなら、その要件定義を行った人は無価値である。つまり不要な人員であることを示すことだよね、と。

本当に「ですよね!」と思うことがこれでもかと書いてあります。

でかいプロジェクトあるあるで「要件定義するだけの人」「基本設計書を書くだけの人」「詳細設計書を書くだけの人」「プログラミングするだけの人」「テストするだけの人」「リリース判定するだけの人」「ISOの審査基準を判定するだけの人」なんてのを何度か体験したことがあります。

それに対して公に何かいう気は全くありませんし、ここまで書けば何を言いたいかわかっていただけるとも思ってもいます(笑)

それくらい、工程同士の繋がり、プロセスの繋がり、チームでの様々な共有、などなど、1つの生き物として捉えないと死ねるよな、って思ったりしています。

要件定義は楽じゃない、急がば回れ

簡単ならこの本を読まなくてもいい。でも実際は難易度が高い。その高い難易度、そこからくる後工程の理解不足のギャップをどう埋めるのか。

それこそ後工程との連携であり、クライアント、チーム内で合意形成をしていきながら要件定義をしていく。

これなら進められるぞ、ということを後工程の人と検討しながら進めていく。

実際非常に手間のかかる要件定義。でも後回しにすればするほど破綻する。個人的には1つでも後回しにしたら緩やかに破綻に向かい、一定の境界を超えたらいきなり破綻、くらいでちょうどいいのでは、と思ったりします。

そのためにも、僕の上司のように まずサクッとUI、画面、データをヒアリング してから、 素早くモックに落とし込み、モックベースで要件を詰めていく のって最強なんだろうなあと思います。

急がば回れ。そしてできれば「急がば回りつつも可能な限り速く回る」ためのテクニックは持っていたいなって思うのでした。

読んでみて

要件定義の話でここまでワクワクすることは今まで一度もなかったと思います。スラスラ読める本に出会ったこともなかったし、自分のやってきたことを客観的に振り返ることもなかったと思います。

しかしこの本は、僕にそれらをもたらしてくれています。

読みやすいというのもあるんですけど、1つ1つの言葉の解説、進め方が非常に丁寧で、絶対に脱落させないという強い意志を感じます。

著者である羽生さんの厳しさに裏付けられた優しさみたいなものも感じます。

実はあとがきの一番最後に、本当にシビれるしあったかくなる言葉が直球で書かれていたりします。

僕も書籍でこういうことが言えるような仕事人生を歩もう。とまで思わせてもらいました。

掛け値無しにいい本です。エンジニア、コードだけ書いていたいと思うこともあるかと思いますし、僕もそういう瞬間あります。

が、この本を読むことで、ちょっとだけでも考えや印象は変わるんじゃないかなって思います。

引き続き、読み進めていきます。

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

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

昨日に引き続きです。分量増えちゃった。しかも後編はパート1と2に分かれるほどの分量になってしまいました(汗)

というわけでクエリビルダから。僕にとってはEloquentよりこちらの方が馴染みがありますし、他のフレームワークを使っての開発もたくさんありますので、Laravelにロックインされないためにも、クエリビルダを使いたいなあと思ったりします。

が、昨日も書いた通りですので両方使ってみようと思ってます。

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

ちなみに、クエリビルダの章については、全てのSQLとコードを実際に実行したので、その結果も掲載している部分があります。冗長な部分がありますがご容赦くださいませ。あと、この章のSQL、コードは全部動くはずです。(実行し直したので大丈夫…のはず)

前置き

Laravelには tinker っていう便利なやつがあります。DBを操作するコードの実行には全てtinkerを使っています。一応、tinkerを使うところはコマンドから記載しているので大丈夫かとは思いますが…。

ちなみに、tinkerを実行する時は、Dockerコンテナの中に入り、artisanがある場所で実行しています。

$ docker-compose up -d nginx mysql workspace
(中略)
$ docker-compose exec --user=laradock workspace bash
$ pwd
/var/www
laradock@0f043864bf49:/var/www$ ls -la artisan 
-rw-r--r-- 1 laradock laradock 1686 Oct 16 16:35 artisan
laradock@0f043864bf49:/var/www$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> 

5-4 クエリビルダ

  • クエリビルダのメソッドを繋げてSQLを組み立てる
  • メソッドチェーン
  • Eloquentも内部にクエリビルダのインスタンスを持つ

リスト5.4.0.1:データ検索のSQL

  • 価格が1000円以上、かつ出版日が 2011-01-01 以降
  • これをクエリビルダで書き換えていく
SELECT
    bookdetails.isbn, 
    books.name, 
    authors.name,
    bookdetails.price
FROM 
    books
LEFT JOIN 
    bookdetails 
    ON books.bookdetail_id = bookdetails.id
LEFT JOIN 
    authors 
    ON books.author_id = authors.id
WHERE 
    bookdetails.price >= 1000 
AND 
    bookdetails.published_date >= '2011-01-01'
ORDER BY 
    bookdetails.published_date DESC;

リスト5.4.0.2:データ検索のSQL文をクエリビルダで組み立てる

  • ①ベースのbooksテーブルのクエリビルダインスタンスを取得
  • ②取得カラムを指定
  • ③テーブル結合
  • ④〜⑤条件指定
  • ⑥ソート
    • ここまでの各メソッドの戻り値は Illuminate\Database\Query\Builder
    • つまりクエリビルダを戻り値に受け取り続けることでメソッドチェーンが可能
  • SQL実行して結果のオブジェクトを取得
    • ここが呼ばれるまでDB接続もSQL実行もされない
    • 実行結果は stdClass オブジェクトのコレクションで取り扱いも容易
      • 各カラムを操作するのも簡単
<?php
$results = DB::table('books')                                                  //①
->select(['bookdetails.isbn','books.name','authors.name','bookdetails.price']) //②
->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')        //③
->leftjoin('authors', 'books.author_id', '=', 'authors.id')                    //③
->where('bookdetails.price', '>=', 1000)                                       //④
->where('bookdetails.published_date', '>=', '2011-01-01')                      //⑤
->orderby('bookdetails.published_date', 'desc')                                //⑥
->get();                                                                       //⑦
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select(['bookdetails.isbn', 'books.name', 'authors.name', 'bookdetails.price'])->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')->leftjoin('authors', 'books.author_id', '=', 'authors.id')->where('bookdetails.price', '>=', 1000)->where('bookdetails.published_date', '>=', '2011-01-01')->orderby('bookdetails.published_date', 'desc')->get();
=> Illuminate\Support\Collection {#2917
     all: [
       {#2918
         +"isbn": "9784204176365",
         +"name": "著者名9",
         +"price": 3755,
       },
       {#2920
         +"isbn": "9793433402596",
         +"name": "著者名2",
         +"price": 3962,
       },
     ],
   }
>>> 

5-4-1 クエリビルダの書式

  • ①ベースとなるクエリビルダオブジェクト
  • ②〜⑥メソッドチェーンによる処理対象や内容の特定
  • ⑦クエリ実行
  • この書式は検索、更新、削除でも共通

tinkerによるクエリ実行

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select(['bookdetails.isbn', 'books.name', 'authors.name', 'bookdetails.price', 'bookdetails.published_date'])->leftjoin('bookdetails', 'books.bookdetail_id', '=', 'bookdetails.id')->leftjoin('authors', 'book                                                                                                                                                                          desc')->get();
=> Illuminate\Support\Collection {#2884
     all: [
       {#2922
         +"isbn": "9784204176365",
         +"name": "著者名9",
         +"price": 3755,
         +"published_date": "2018-08-07",
       },
       {#2926
         +"isbn": "9793433402596",
         +"name": "著者名2",
         +"price": 3962,
         +"published_date": "2015-05-16",
       },
       {#2924
         +"isbn": "9784238339682",
         +"name": "著者名2",
         +"price": 9908,
         +"published_date": "2009-02-24",
       },
       {#2928
         +"isbn": "9790930291296",
         +"name": "著者名7",
         +"price": 7689,
         +"published_date": "2006-05-11",
       },
       {#2879
         +"isbn": "9788521682677",
         +"name": "著者名7",
         +"price": 9093,
         +"published_date": "2000-11-14",
       },
       {#2874
         +"isbn": "9787518986811",
         +"name": "著者名3",
         +"price": 1634,
         +"published_date": "2000-05-10",
       },
     ],
   }

5-4-2- クエリビルダの取得

リスト5.4.2.1:DBファサードを利用したクエリビルダの取得

<?php

// 書籍テーブルのクエリビルダ取得
$query = DB::table('books');

リスト5.4.2.2:Connectionクラスからクエリビルダを取得

<?php

// ①サービスコンテナからDatabaseManagerクラスのインスタンス取得
$db = \Illuminate\Foundation\Application::getInstance()->make('db');

// ②上記インスタンスからConnectionクラスのインスタンスを取得
$connection = $db->connection();

// ③Connectionクラスのインスタンスからクエリビルダを取得
$query = $connection->table('books');

リスト5.4.2.3:データ操作専用クラスを作ってクエリビルダを利用

  • コンストラクタインジェクションを利用
  • クエリビルダ提供元のクラスを外から与える
  • 拡張性やテスト容易性を保つことが可能
<?php
declare(strict_types=1);

namespace App\DataAccess;

use Illuminate\Database\DatabaseManager;

class BookDataAccessObject
{
    /** @var DatabaseManager */
    protected $db;

    /** @var string */
    protected $table = 'books';

    public function __construct(DatabaseManager $db)
    {
        $this->db = $db;
    }

    public function find($id)
    {
        $query = $this->db->connection()
            ->table($this->table);
        (以下略)
    }
}

5-4-3 処理対象や内容の特定

  • 各種メソッドの紹介と実装例

Select系メソッド

表5.4.3.1:Select系メソッド

メソッド 機能
select(カラム名の配列) 取得対象のカラム名を指定する
selectRaw(SQL文) select文の中身をSQLで直接指定する

リスト5.4.3.2:Select系メソッドの利用

<?php

$result = DB::table('books')->select('id', 'name as title')->get();

$result = DB::table('books')->selectRaw('id, name as title')->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select('id', 'name as title')->get();
=> Illuminate\Support\Collection {#2881
     all: [
       {#2872
         +"id": 11,
         +"title": "Quae sunt libero adipisci.",
       },
       {#2859
         +"id": 12,
         +"title": "Est nihil ut quo.",
       },
       {#2868
         +"id": 13,
         +"title": "Quas maiores sit aut.",
       },
       {#2867
         +"id": 14,
         +"title": "Quibusdam ut eius omnis.",
       },
       {#2874
         +"id": 15,
         +"title": "Animi et sed culpa.",
       },
       {#2869
         +"id": 16,
         +"title": "Dignissimos sunt est autem ea.",
       },
       {#2882
         +"id": 17,
         +"title": "Minus cum ut est.",
       },
       {#2860
         +"id": 18,
         +"title": "At voluptates numquam magni.",
       },
       {#2853
         +"id": 19,
         +"title": "Ratione dolore exercitationem.",
       },
       {#2852
         +"id": 20,
         +"title": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> DB::table('books')->selectRaw('id, name as title')->get();
=> Illuminate\Support\Collection {#2879
     all: [
       {#2840
         +"id": 11,
         +"title": "Quae sunt libero adipisci.",
       },
       {#2856
         +"id": 12,
         +"title": "Est nihil ut quo.",
       },
       {#2858
         +"id": 13,
         +"title": "Quas maiores sit aut.",
       },
       {#2871
         +"id": 14,
         +"title": "Quibusdam ut eius omnis.",
       },
       {#2865
         +"id": 15,
         +"title": "Animi et sed culpa.",
       },
       {#2850
         +"id": 16,
         +"title": "Dignissimos sunt est autem ea.",
       },
       {#2866
         +"id": 17,
         +"title": "Minus cum ut est.",
       },
       {#2870
         +"id": 18,
         +"title": "At voluptates numquam magni.",
       },
       {#2861
         +"id": 19,
         +"title": "Ratione dolore exercitationem.",
       },
       {#2855
         +"id": 20,
         +"title": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> 

Where系メソッド

  • 連続して呼ぶとAND条件
  • ORにしたければ orWhere orWhereBetween など先頭に or をつける

表5.4.3.3:Where系メソッド

メソッド 機能
where(`カラム名', ''比較演算子, '条件値') whereを使用した一般的な条件指定。比較演算子を省略すると等価判定(=)となる
whereBetween('カラム名', '範囲') betweenを使用した範囲指定
whereNotBetween('カラム名', '範囲') not betweenを使用した範囲指定
whereIn('カラム名', '条件値') inを使用
whereNotIn('カラム名', '条件値') not inを使用
whereNull(カラム名) is nullを使用
whereNotNull(カラム名) is not nullを使用

リスト5.4.3.4:Where系メソッドの利用

<?php

$results = DB::table('books')
    ->where('id', '>=', '30')
    ->orWhere(`created_at`, '>=', '2018-01-01')
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->where('id', '>=', '30')->orWhere('created_at', '>=', '2018-01-01')->get();
=> Illuminate\Support\Collection {#2857
     all: [
       {#2858
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2856
         +"id": 12,
         +"name": "Est nihil ut quo.",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2849
         +"id": 13,
         +"name": "Quas maiores sit aut.",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2879
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2840
         +"id": 15,
         +"name": "Animi et sed culpa.",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2851
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2861
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2880
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2855
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2864
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

Limit, Offsetメソッド

表5.4.3.5:LimitとOffsetメソッド

メソッド 機能
limit(数値) または take(数値) limit句に置き換わる
offset(数値) または skip(数値) offset句に置き換わる

リスト5.4.3.6:Limit, Offsetメソッドの利用

<?php

$results = DB::table('books')
    ->limit(10)
    ->offset(6)
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->limit(10)->offset(6)->get();
=> Illuminate\Support\Collection {#2865
     all: [
       {#2872
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2871
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2866
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2870
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

集計系メソッド

SQLを生で書くことが多い僕にはお馴染みのメソッドたちです。

表5.4.3.7:集計系メソッド

メソッド 機能
orderBy(カラム名, 方向) order by句に置き換わる
groupBy(カラム名) group by句に置き換わる
having(`カラム名’, '比較演算子', '条件値') havingを利用した絞り込み
havingRaw(SQL文) having句の中身をSQLで直接指定

リスト5.4.3.8:集計系の中から、orderByメソッドによる複数カラムのソート

<?php

$results = DB::table('books')
    ->orderBy('id')
    ->orderBy('updated_at', 'desc')
    ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->orderBy('id')->orderBy('updated_at', 'desc')->get();
=> Illuminate\Support\Collection {#2879
     all: [
       {#2861
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2880
         +"id": 12,
         +"name": "Est nihil ut quo.",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2849
         +"id": 13,
         +"name": "Quas maiores sit aut.",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2859
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2858
         +"id": 15,
         +"name": "Animi et sed culpa.",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2857
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2856
         +"id": 17,
         +"name": "Minus cum ut est.",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2851
         +"id": 18,
         +"name": "At voluptates numquam magni.",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2840
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
       {#2847
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-18 07:08:48",
         +"updated_at": "2018-11-18 07:08:48",
       },
     ],
   }
>>> 

JOINメソッド

フレームワークにより記法が異なりますが、パッと見でわかる感じですね。

表5.4.3.9:JOINメソッド

メソッド 機能
join('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の内部結合、inner joinに置き換わる
leftJoin('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の外部結合、left joinに置き換わる
rightJoin('対象テーブル', '結合対象カラム', '演算子', '結合対象カラム') テーブル間の外部結合、right joinに置き換わる

リスト5.4.3.10:連続したJOINメソッドによる結合

tinkerでお手軽に実行します。ワンライナーにしてるので実際にコードに書く場合と分けてみます。

<?php

$results = DB::table('books')
                ->leftJoin('authors', 'books.author_id', '=', 'authors.id')
                ->leftJoin('publishers', 'books.publisher_id', '=', 'publishers.id')
                ->get();
$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->leftJoin('authors', 'books.author_id', '=', 'authors.id')->leftJoin('publishers', 'books.publisher_id', '=', 'publishers.id')->get();
=> Illuminate\Support\Collection {#2872
     all: [
       {#2869
         +"id": 12,
         +"name": "株式会社 青田出版",
         +"bookdetail_id": 17,
         +"author_id": 23,
         +"publisher_id": 12,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ3",
         +"deleted_at": null,
         +"address": "4586817  群馬県青山市南区中津川町松本1-5-2",
       },
       {#2874
         +"id": 12,
         +"name": "株式会社 青田出版",
         +"bookdetail_id": 30,
         +"author_id": 11,
         +"publisher_id": 12,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ1",
         +"deleted_at": null,
         +"address": "4586817  群馬県青山市南区中津川町松本1-5-2",
       },
       {#2867
         +"id": 9,
         +"name": "有限会社 佐々木出版",
         +"bookdetail_id": 25,
         +"author_id": 32,
         +"publisher_id": 9,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ2",
         +"deleted_at": null,
         +"address": "8481973  群馬県山田市北区宇野町喜嶋6-2-10",
       },
       {#2868
         +"id": 15,
         +"name": "株式会社 山田出版",
         +"bookdetail_id": 10,
         +"author_id": 8,
         +"publisher_id": 15,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ8",
         +"deleted_at": null,
         +"address": "7517039  山梨県工藤市北区小林町三宅2-7-10",
       },
       {#2870
         +"id": 10,
         +"name": "株式会社 田辺出版",
         +"bookdetail_id": 49,
         +"author_id": 17,
         +"publisher_id": 10,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ7",
         +"deleted_at": null,
         +"address": "3881619  高知県津田市中央区喜嶋町宮沢9-1-7 ハイツ藤本110号",
       },
       {#2866
         +"id": 5,
         +"name": "株式会社 青山出版",
         +"bookdetail_id": 38,
         +"author_id": 39,
         +"publisher_id": 5,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ9",
         +"deleted_at": null,
         +"address": "2219948  千葉県西之園市東区井上町佐々木4-5-8 ハイツ井高110号",
       },
       {#2871
         +"id": 8,
         +"name": "株式会社 吉本出版",
         +"bookdetail_id": 32,
         +"author_id": 1,
         +"publisher_id": 8,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ1",
         +"deleted_at": null,
         +"address": "9972025  福井県廣川市東区吉本町笹田5-4-2 ハイツ青山104号",
       },
       {#2865
         +"id": 27,
         +"name": "有限会社 井高出版",
         +"bookdetail_id": 28,
         +"author_id": 37,
         +"publisher_id": 27,
         +"created_at": "2018-11-14 16:36:18",
         +"updated_at": "2018-11-14 16:36:18",
         +"kana": "チョシャメイ7",
         +"deleted_at": null,
         +"address": "7757641  秋田県宇野市東区西之園町小泉9-4-4 ハイツ野村107号",
       },
       {#2864
         +"id": 18,
         +"name": "有限会社 西之園出版",
         +"bookdetail_id": 43,
         +"author_id": 22,
         +"publisher_id": 18,
         +"created_at": "2018-11-14 16:36:06",
         +"updated_at": "2018-11-14 16:36:06",
         +"kana": "チョシャメイ2",
         +"deleted_at": null,
         +"address": "8728464  岐阜県鈴木市西区近藤町江古田1-3-8",
       },
       {#2863
         +"id": 10,
         +"name": "株式会社 田辺出版",
         +"bookdetail_id": 50,
         +"author_id": 24,
         +"publisher_id": 10,
         +"created_at": "2018-11-14 15:56:28",
         +"updated_at": "2018-11-14 15:56:28",
         +"kana": "チョシャメイ4",
         +"deleted_at": null,
         +"address": "3881619  高知県津田市中央区喜嶋町宮沢9-1-7 ハイツ藤本110号",
       },
     ],
   }
>>> 

5-4-4 クエリの実行

  • クエリビルダの最後に指定するSQL実行について

5-4-4-1 データの取得

こちらはお馴染みな感じがしますね。

表5.4.4.1:クエリを実行し結果を取得するメソッド

メソッド 機能 結果
get() 全てのデータを取得 stdClassオブジェクトのコレクション
first() 最初の1行 オブジェクト単体

リスト5.4.4.2:tinkerによるgetメソッド実行例

Seederを使ってbooksテーブルにダミーデータを入れてから実行しています。tinker便利ですね。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('books')->select('id', 'name')->get();
=> Illuminate\Support\Collection {#2847
     all: [
       {#2849
         +"id": 11,
         +"name": "Quae sunt libero adipisci.",
       },
       {#2846
         +"id": 12,
         +"name": "Est nihil ut quo.",
       },
       {#2845
         +"id": 13,
         +"name": "Quas maiores sit aut.",
       },
       {#2844
         +"id": 14,
         +"name": "Quibusdam ut eius omnis.",
       },
       {#2843
         +"id": 15,
         +"name": "Animi et sed culpa.",
       },
       {#2842
         +"id": 16,
         +"name": "Dignissimos sunt est autem ea.",
       },
       {#2855
         +"id": 17,
         +"name": "Minus cum ut est.",
       },
       {#2857
         +"id": 18,
         +"name": "At voluptates numquam magni.",
       },
       {#2858
         +"id": 19,
         +"name": "Ratione dolore exercitationem.",
       },
       {#2859
         +"id": 20,
         +"name": "Repellat molestiae provident nisi.",
       },
     ],
   }
>>> DB::table('books')->select('id', 'name')->first();
=> {#2862
     +"id": 11,
     +"name": "Quae sunt libero adipisci.",
   }
>>> DB::table('books')->select('id', 'name')->find(11);
=> {#2858
     +"id": 11,
     +"name": "Quae sunt libero adipisci.",
   }

実際のクエリ

-- getメソッド
select `id`, `name` from `books`

-- firstメソッド
select `id`, `name` from `books` limit 1

-- findメソッド(なぜかlimitがつく)
select `id`, `name` from `books` where `id` = 11 limit 1

表5.4.4.3:レコード数取得や計算を行うメソッド

集約関数と呼ばれるやつですね。

メソッド 機能
count() 件数取得
max(カラム名) 最大数取得
min(カラム名) 最小値
avg(カラム名) 平均値

リスト5.4.4.4:tinkerによる各メソッド実行例

こちらもtinkerで実行します。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('bookdetails')->count();
=> 100
>>> DB::table('bookdetails')->max('price');
=> 9918
>>> DB::table('bookdetails')->min('price');
=> 66
>>> DB::table('bookdetails')->avg('price');
=> "5193.3600"

5-4-4-2 データの登録、更新、削除

こちらもお馴染みです。

表5.4.4.4:データ更新を行うメソッド

メソッド 機能
insert(['カラム'=>'値', ...]) データ登録
update(['カラム'=>'値', ...]) データ更新
delete() データ削除
truncate() 全行削除

リスト5.4.4.5:データ更新

こちらもtinkerで実行してみます。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::table('bookdetails')->where('id', 1)->update(['price' => 10000]);
=> 1
>>> DB::table('bookdetails')->max('price');
=> 10000

5-4-5 トランザクションとテーブルロック

トランザクション、テーブルロックといった操作も可能です。残念ながら僕は未だ ロックを意図的に実施する必要があるシステム を経験したことがないのですが、想定されるおおよそのSQLがメソッドとして用意されているのはパワフルだな、と思いました。

表5.4.5.1:トランザクション系メソッド

メソッド 機能
DB::beginTransaction() 手動でトランザクション開始
DB::rollback() 手動ロールバック
DB::commit() 手動コミット
DB::transaction(クロージャ) クロージャの中でトランザクションを実施

手動トランザクションの実例

せっかくなので、トランザクションの動きをなぞって検証してみます。

$ php artisan tinker
Psy Shell v0.9.9 (PHP 7.2.4-1+ubuntu16.04.1+deb.sury.org+1 — cli) by Justin Hileman
>>> DB::beginTransaction();
=> null

トランザクション直後のデータ

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+-------+---------------------+---------------------+
| id  | isbn          | published_date | price | created_at          | updated_at          |
+-----+---------------+----------------+-------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     |  3265 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

UPDATE実施(未コミット)

>>> DB::table('bookdetails')->where('id', 100)->update(['price' => 100000]);
=> 1

UPDATEしたが、commitしていない状態、値段は変わっていない

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+-------+---------------------+---------------------+
| id  | isbn          | published_date | price | created_at          | updated_at          |
+-----+---------------+----------------+-------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     |  3265 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+-------+---------------------+---------------------+
1 row in set (0.00 sec)

commit実施

>>> DB::commit();
=> null

UPDATEし、commitした結果、値段が変わっている

mysql> select * from bookdetails where id=100;
+-----+---------------+----------------+--------+---------------------+---------------------+
| id  | isbn          | published_date | price  | created_at          | updated_at          |
+-----+---------------+----------------+--------+---------------------+---------------------+
| 100 | 9780493063348 | 1998-02-06     | 100000 | 2018-11-18 07:00:03 | 2018-11-18 07:00:03 |
+-----+---------------+----------------+--------+---------------------+---------------------+
1 row in set (0.00 sec)

表5.4.5.2:悲観的ロックのメソッド

こちらはなんとなく利用シーンが思い浮かぶ気がするんですが、じゃあ具体的にどんな時?と言われるとパッと出てきません。まだまだだな、と思うと以前の僕ならこの章のように悲観的になったりもしたのですが、今の僕は「楽しみだな」と思たりはします。

ここではメソッドの紹介に止まり、実際に動かすところまでは書いていませんでした。 この本は2週目を考えているので、その際にシチュエーションと実際の実装をやりたいですね。

メソッド 機能
sharedLock() selectされた行に共有ロックをかけ、トランザクションコミットまで更新を禁止する
lockForUpdate() selectされた行に排他ロックをかけ、トランザクションコミットまで読み書き両方を禁止する

5-4-6 ベーシックなデータ操作

  • EloquentもクエリビルダもSQLに変換されているよね
  • コードから生成されるSQLが常に一定とは限らない
    • 個人的にこれは困るなあ…
  • クエリが長いとメソッドチェーンも長くなるので手軽さや可読性が失われる
  • SQL直接書きたい!
  • ありますよ。

表5.4.6.1:ベーシックなSQL実行メソッド

あらゆるSQLが書けるようなメソッドが用意されてます。プリペアドステートメントもPDOで親しんだのと同じなので、速度を求められる場合や複雑な分析系のSQLを書く際には重宝しそうです。(SQLが複雑な時点で設計が…という話は置いといて)

メソッド名 説明
DB::select('selectクエリ', [クエリに結合する引数]) select文によるデータ抽出
DB::insert('insertクエリ', [クエリに結合する引数]) insert文によるデータ登録
DB::update('updateクエリ', [クエリに結合する引数]) update文によるデータ更新。更新された行数が返る
DB::delete('deleteクエリ', [クエリに結合する引数]) delete文によるデータ削除。削除された行数が返る
DB::statement('SQL', [クエリに結合する引数]) 上記以外のSQLを実行する場合

コード5.4.6.1:DB:selectを使用したデータ抽出

以下のSQLを実行します。

mysql> SELECT 
    ->     bookdetails.isbn, books.name 
    -> FROM 
    ->     books 
    -> LEFT JOIN 
    ->     bookdetails 
    ->         ON books.bookdetail_id = bookdetails.id 
    -> WHERE 
    ->     bookdetails.price >= '1000' 
    -> AND bookdetails.published_date >= '2011-01-01' 
    -> ORDER BY 
    ->     bookdetails.published_date DESC;
+---------------+--------------------------------+
| isbn          | name                           |
+---------------+--------------------------------+
| 9784204176365 | Dignissimos sunt est autem ea. |
| 9793433402596 | Quas maiores sit aut.          |
+---------------+--------------------------------+
2 rows in set (0.00 sec)

コードはこちら。 ちなみに //利用方法 の部分、配列の中は stdClassオブジェクト なのでアロー演算子じゃないとエラーになります。ので修正してます。

<?php
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= ? AND bookdetails.published_date >= ?'
    .   ' ORDER BY bookdetails.published_date DESC';

$results = DB::select($sql, [1000, '2011-01-01']);

// 僕はこっち派
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date'
    .   ' ORDER BY bookdetails.published_date DESC';

$results = DB::select($sql, ['price' => 1000, 'published_date' => '2011-01-01']);

// 利用方法、$bookはstdClassオブジェクトなのでアロー演算子だよ
foreach ($results as $book) {
    echo $book->name;
    echo $book->isbn;
}

tinkerで実行します。 tinkerは改行を入れるとエラーになりますが、パッと確認できる最小単位なので便利なので一貫して使ってます。

>>> $sql =  'SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC';
=> "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC"
>>> $results = DB::select($sql, ['price' => 1000, 'published_date' => '2011-01-01']);
=> [
     {#2864
       +"isbn": "9784204176365",
       +"name": "Dignissimos sunt est autem ea.",
     },
     {#2863
       +"isbn": "9793433402596",
       +"name": "Quas maiores sit aut.",
     },
   ]
>>> foreach ($results as $book) { echo $book->name; echo $book->isbn; }
Dignissimos sunt est autem ea.9784204176365Quas maiores sit aut.9793433402596
>>> 

foreachでの stdClass オブジェクトへのアクセスも確認できます。

コード5.4.6.2:PDOオブジェクトを直接使用する

ほぼ同じなので写経して動かしたものを貼っておきます。

<?php
$sql =  ' SELECT bookdetails.isbn, books.name '
    .   ' FROM books'
    .   ' LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id'
    .   ' WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date'
    .   ' ORDER BY bookdetails.published_date DESC';

$pdo = DB::connect()->getPdo();
$statement = $pdo->prepare($sql);
$statement->execute(['price' => 1000, 'published_date' => '2011-01-01']);
$results = $statement->fetchAll(PDO::FETCH_ASSOC);

// 利用方法、PDOだと配列
foreach ($results as $book) {
    echo $book['name'];
    echo $book['isbn'];
}
>>> $sql =  'SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC';
=> "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC"
>>> $pdo = DB::connection()->getPdo();
=> PDO {#2850
     inTransaction: false,
     attributes: {
       CASE: NATURAL,
       ERRMODE: EXCEPTION,
       AUTOCOMMIT: 1,
       PERSISTENT: false,
       DRIVER_NAME: "mysql",
       SERVER_INFO: "Uptime: 10737  Threads: 2  Questions: 77  Slow queries: 0  Opens: 125  Flush tables: 1  Open tables: 118  Queries per second avg: 0.007",
       ORACLE_NULLS: NATURAL,
       CLIENT_VERSION: "mysqlnd 5.0.12-dev - 20150407 - $Id: 38fea24f2847fa7519001be390c98ae0acafe387 $",
       SERVER_VERSION: "5.7.24",
       STATEMENT_CLASS: [
         "PDOStatement",
       ],
       EMULATE_PREPARES: 0,
       CONNECTION_STATUS: "mysql via TCP/IP",
       DEFAULT_FETCH_MODE: BOTH,
     },
   }
>>> $statement = $pdo->prepare($sql);
=> PDOStatement {#2844
     +queryString: "SELECT bookdetails.isbn, books.name FROM books LEFT JOIN bookdetails ON books.bookdetail_id = bookdetails.id WHERE bookdetails.price >= :price AND bookdetails.published_date >= :published_date ORDER BY bookdetails.published_date DESC",
   }
>>> $statement->execute(['price' => 1000, 'published_date' => '2011-01-01']);
=> true
>>> $results = $statement->fetchAll(PDO::FETCH_ASSOC);
=> [
     [
       "isbn" => "9784204176365",
       "name" => "Dignissimos sunt est autem ea.",
     ],
     [
       "isbn" => "9793433402596",
       "name" => "Quas maiores sit aut.",
     ],
   ]
>>> foreach ($results as $book) { echo $book['isbn']; echo $book['name']; }
9784204176365Dignissimos sunt est autem ea.9793433402596Quas maiores sit aut.
>>>

対話型でそれぞれ処理が実行され、無事に動いていることがわかります。

やってみて

実際にtinkerを使ってコードを実行していくと、変数や配列、オブジェクトに何が設定され、何が返ってくるのかがはっきりとわかりました。

コードを実行していて、これほど動きを実感できることってそうそうないよなって思います。

ずーっと昔、VisualCで業務システムのコードを書いていた時に、デバッガを使ってメモリ確保の中身まで追いかけた日々を思い出します。

そしてEloquent(ORM)とクエリビルダ、どっちがいいか悪いかはないと思うに至りました。Laravelに特化してサービスを構築して、効率的にリリースを継続するなら、フレームワークの力を最大限に引き出すべきだろうと思いました。

が、そうではないレベルにサービスが成長したのなら、それはケースバイケース。その時に採り得る最良の方法で課題を解決していくために、技術を、道具を、選択していくべきだよな、と改めて思わせてくれました。

データベースの章だけでもこんな風に思わせてくれるんですから、通読すれば得られることはすごく多いと思います。Laravel本、と言いますけど、Laravelを通して設計やOOPについて基礎からしっかり学べる書籍になっていると思います。

もし興味があって、購入を迷われているかたがおられましたら、ぜひ購入してみてください。得られることはものすごくたくさんあると思います!

そして後半パート2へ続くのじゃ…(汗)

そしてなんと…ここまでで約3.4万文字。後編を2つに分けることに相成りました!

というわけで、リポジトリパターンは後編パート2に続く!!!

【前編】登壇で振り返る今年のアウトプット

これは Everyone Outputer Advent Calendar 2018 の9日目のエントリーです。振り返ったら長くなりすぎたので、これは前編となります。

今年は人生で一番 アウトプットを意識してきた1年 でした。

自主的な学習、登壇に加え、継続的かつ計画的な学習を続けてきました。

その中から、登壇をチョイスして振り返ってみたいと思います。

1月

1月は忙しかったのかな。何も登壇していませんでしたね。何してたんだろうこの頃w

…と思ったら、2週間の長期休暇で、冬休みの娘と折り紙に興じていた ようですね。

休暇は長くとる派なので、この月は休養してたんだなー。

① 2月9日:【吉祥寺.pm】とある負傷兵の回復日誌

「新しい挑戦、新しい視点」 というテーマでしたので、他の人がしたことのない新しい視点 という意味で、以下の内容で15分、お話させていただきました。

体は人それぞれ

同じ負荷で同じことができるわけもなく、耐えられるのも壊れるのも人それぞれのボーダーラインがあります。

しかし人はそれをよく知らない。ついつい周りに流されてしまう。スライドには書いてないこともありますが、そんなことをお話したかと思います。

実は自分のことをよく知らない

人それぞれな上に、自分のことをよく知らないんですよね、意外と。

なので、自己分析しましょう。わからなければプロである医者を頼りましょう。医者が頼りにならなければセカンドオピニオン受けましょう。

そして放置せず、根性でなんとかせず、改善に向けて適切な休養と処置を受けましょう、というお話をしたかと思います。

己を知ることから全てが始まる。そこから何をするのか決める。エンジニアリングにも通じるかな、と思いながら話した記憶が蘇ります。

じゃあ知ってみよう

自分がどういう状態、状況なのかを知る。客観的に知る必要があるので、書き出すのが一番です。

僕は自分の体調がおかしかったとき、医者にいく前に症状や状況、経緯を紙に書いて持っていった記憶があります。

しかし1度目の医者ではラチがあかず、何度か病院と医者を変更し続け、やっと今の主治医に出会えました。

正しく知るために行動し、対処していく。エンジニアリングではそれができても、いざ自分の体となるとなかなかやらないことも多いかと思うので、ちゃんとやりましょうというお話をしたような気がします。

知った結果を活用しよう

状況、状態、処置の方法を知ったなら、適切な診断と処方を受け、生活習慣を改善し、休養し、必要なら薬を飲み、安定させていく。

完治しない場合もありますが、確実に改善に向かうと思います。

僕も完治はしていませんし、今でも薬にお世話になっていますが、完全に自律神経を壊した頃と比べれば8割くらいマシになりました。

登壇してみて

自分の辛かった体験や経験を、少しでも多くの人に知ってもらって、もし辛い思いをしている人がいたら、少しでもヒントになればいいな。

もし諦めている人がいるなら、前に進む方法があるんだよ、と言うことを伝えたかったです。

結果として、想像以上に大きな反響があり驚きました。

やってよかったなって思うと共に、何度か別の場所でも話してみよう。僕の経験をきっかけに病院にいくとか、自己分析してみるとか、それだけでもきっと希望の1つになるんじゃないか、って思ったことを覚えています。

反響

② 2月28日:【PHP勉強会@東京】とある負傷兵の回復日誌

同じ登壇は3回やってみたほうがいい、と以前そーだいさんに言われてました。理由は明白で 違う分野と場所で3回話したら、だいたい自分のものになっている というような理由だったと思います。

僕も心から同意したので、2017年11月30日のLTLoversで5分、同月の吉祥寺.pmで15分、そしてこの会で20分、3回話してみました。

結果としては、毎回反響が増えていって、こういった悩みを解決するのも技術力なんだな、って実感した記憶があります。

詳細は前回とほぼ同じなので割愛しますが、またどこかでやろうかなって思ったりしています。

③ 3月28日:【PHP勉強会@東京】PHPerが知るべきTCP/IPのきほん

5分でやるには無理がありましたw

でもなんとか「こういうもんだよ!」ってのを、手書きの図を使ってやってみた登壇。細かいところは話きれずに別の人に解説をいただくことになるんですが「こういうことしてんだな」ってのを知るきっかけにはなったかなあ、とは思っています。

やってみて

いやー5分で話すテーマじゃなかったな!と。 無理やり5分に収めたので、削除したスライドがあって、そこを思い切り会場で別の人に訂正され教えていただくというありがたい事態にw

個人的にはすごくすごくありがたいことだったんですが、会場としては そんなにマサカリ投げなくても という反応だったみたいですw

でもこれをきっかけに、DNSを学んでいくことになったので、やってみてよかったなって思います。

いずれ15分テーマくらいでやってみようと思ってます。

④ 4月8日:【インフラ勉強会】とある負傷兵の回復日誌

インフラ勉強会を知った直後くらいに、定期的に LT大会があることを知ったんです。そこで個人的に「やってみたいなー」って呟いたんですよね、確か。 そしたら たけし さんに拾っていただいたんです。あれよと言うまにエントリーが決まり、LTの内容でもないし、テーマに即しているわけでもないのにトークさせていただくことに。

その時のNaccoさんのアナウンスがこちらでした。 環境紹介LTの最後に、LTじゃないけどしゃべりたいということで時間をいただいてお話させていただくというありがたい体験をさせていただきました。

ここで初めて 音声と画面共有とチャットを用いてオンライン登壇する というのを体験させていただいた貴重な第一回でもありました。

この回をきっかけに、このあと何回かのオンライン登壇をさせていただくことにもなりました。

⑤ 4月25日:【PHP勉強会@東京】PHPerが知るべきDNSのきほん

4月のTCP/IPで学習不足だな、じゃあ何からやるかな、よしDNSをやろう、と思うきっかけになってました。

また、4月5日から前職のリードディレクターと毎日の学習を始めたんですが、その題材が 3分間DNS基礎講座 でした。

なのでその内容の前半を20分にまとめてトークしてきました。

この本のいいところ

前月に学習したTCP/IPの基礎がちゃんと書いてあり、そこからDNSの話に繋がっていました。かつ1つの題材が本当に3分で読めるよう分割されていて、かつわかりやすい。

論点を3分でわかることに絞る ことを延々と連結させているのが素晴らしかったです。おかげでDNSの基礎を学びつつ、もっと知りたい、ここはふわっとしていてわかりにくい、ここは別角度から理解したい、と次々とやってみたいことが湧き出てきて、これがのちの buildersconの登壇 につながりました。

本当にいい本です。何から始めればいいのやら…でも時間かかるのはなあ…と思っている人がいたら、読んでみることを強くオススメします。

やってみて

毎日の学習、約1ヶ月の成果としても登壇したんですが、ここら辺から、日々のインプットアウトプット->登壇のためのインプットアウトプット、の回転の中に身を置き始めたな、って思います。

最終的には他の本も読み、RFCをいくつか読み、スライドを作っていくことになるんですが、この時はまだそこまでやっていませんでしたね。

何はともあれ、今年の集大成とも言えるスライドのきっかけになった登壇でした。

⑥ 5月25日:【吉祥寺.pm】エンジニアリングで解決するために非エンジニアと一緒にやったこと

退職エントリーにも書きましたが、前職のチーフディレクターと一緒にやったことをトークしました。

テーマがエンジニアリングで解決したこと、しなかったことだったので、それにのっとった形でした。

やってみて

改めて自分が良きパートナーに恵まれていたこと、もがき苦しんでいた場所から脱することができたこと、僕自身の本当の成長がここから始まったこと、非エンジニアでもエンジニアリングの内容を理解してもらうことは可能なこと、など、いろんなことをお話できたかと思います。

とにもかくにも、僕の2018年はこれに始まったに尽きるんですけど、それがどうして行われ、どんな効果があったのか、を伝えることはできたかな、と思います。

反響

teckl さん本当にありがとうございます!

川柳はその場の流れで入れましたねw

⑦ 5月30日:【php勉強会@東京】エンジニアリングで解決するために非エンジニアと一緒にやったこと

吉祥寺.pmで話したことの再演です。

再演なので詳細は割愛します。

⑧ 6月16日:【PHPカンファレンス福岡2018】MySQLで画像 を扱うメリット・デメリットと障害・解決事例

2017年、PHPカンファレンス(東京)で話す予定だった内容を、8ヶ月越しに福岡で達成できました。話の内容としては、前職のエンジニアブログ に割と詳しく書いたので、そちら参照していただけたら幸いです。

トピックとしては、声出しリハだけでも20回はやったってことくらいですかね。おかげで時間ほぼぴったりに終わることができました。

⑨ 6月23日:【インフラ勉強会】エンジニアリングで解決するために非エンジニアと一緒にやったこと

発足半年記念のオフラインイベント、いわばオフ会を東京・大阪で実施するということで、LTに応募して登壇してきました。

内容としては再演であり、5分の短縮版でもあります。

⑩ 6月27日:【PHP勉強会@東京】DNS浸透問題〜なぜ浸透と言ってはいけないのか〜

DNSを学習しはじめて突き当たった浸透問題。なんで言ってはいけないのか、というか、 なぜ浸透と言ってしまうのか を僕なりに考察した内容を、軽く5分でお話しました。

物議をかもすテーマだったんですが、本気でDNSを学ぶ上で避けて通れないテーマでしたのでお話させていただいた記憶があります。

5分の内容なのでそれほど濃密ではないので、15分に拡大した次の登壇資料をみていただいたほうがいいかなあと思います。

⑪ 7月27日:【吉祥寺.pm】DNS浸透問題「なぜ浸透と言ってはいけないのか」 〜正しい知識と手順を添えて〜

テーマは「コード & レスポンス」

自分のアウトプットと、その結果得られたこと、もちろんコードだけでなく、デザイン、インフラ、ハードウェア…人事総務!経営!などなど、ジャンルは問いません

ということだったので、自分の日々のインプット、アウトプットから得られたこととして、前回LTした内容をさらに深く調べ、考察し、検証し、解決するために僕がやっていることを添えて、15分に拡大してお話してきました。

やってみて

浸透は使ってはいけないんじゃなく、そもそも根本的に間違っていると言うことはわかっていたんですが、じゃあなぜ使ってしまうのか、使わないようにするにはどうすべきなのか、使ってしまう事態をそもそも避けるには何が正しい手順なのか。

正しい手順と知識を持って行動することこそ根本を知り本質的な対応をすることができる唯一の手段。 とはいえ正論だけで人は生きてはいけない。

そんな堂々巡りに答えを出した結果をお話しました。

みんななるべく同じ言葉(定義)を使い、楽しくエンジニアリングしたいものだし、楽しさこそが技術を世界を発展させていくんだと僕は信じているので、そんな思いも込めました。

結果として、思った以上どころか想像以上の反響をいただけたので、この次に控えていたbuildersconの1時間セッションへの大きな励みをいただきました。

登壇してよかったな、って心から思いました。

前半を振り返って

11回の登壇をしてたんだな、と回数を数えて自分でも驚いています。 日々のインプットとアウトプット、プログラミングや技術のテーマだけでなく、テクニックも技術だよな、それは人生全てに当てはまるよな、っていう思いが強いです。

そう思ったからこそ、健康の話や非エンジニアとやったことなど、エンジニアリングと直接関係のないことにフォーカスを当てたりもしてました。

働くことを生活と融合させることが僕の至上命題であり生きる意味の1つなのですが、そこを強く意識もしていたなって思います。

そして4月から学習を開始したTCP/IPDNSですが、buildersconに向かって結実していく流れだったなーと今になって思い返していたりもします。

浸透問題の話だけでも丁寧にやると30分くらいかかるし、僕が言ってることはあくまでも僕の推論と検証 であり、本当に正しいかどうかはまだ自分の中で完全に答えが出ていません。

DNSに関しては来年のDNS温泉に出席するまでにブラッシュアップし直し、RFCを可能な限り読んだ上でスライドを作り直し、1時間トークのために削った内容をしっかり加え、査読していただいた中京大学の鈴木教授へのせめてもの恩返しにしたいと思ってもいます。

やはり、講釈する側は常に情報をアップデートしなければなりませんし、曖昧なことは言うべきではないですから。

そして後半に続くのですが、アドベントカレンダーの枠は空いていないようなので、これは個人の学習記録に続けさせていただこうかなと思っています。

来年もアウトプットしていくぞ!

学習記録: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で作ってみようと思っています。

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