学習記録:12月12日(水):【輪読会まとめ 後編パート2・完結編】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン)
これは 俺のインプットアウトプット記録 Advent Calendar 2018 の12日目のエントリーです。
そして昨日のエントリー 学習記録:12月11日(火):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン) の続きで、データベースの章の読了完結編です。
昨日はリポジトリパターンの実装で、
- データ周りの要求変更に負けない設計パターンを取り入れる
- ビジネスロジックからデータ操作を別レイヤへ
という内容を読み進め、まずは サービスクラスとデータベースアクセスが密結合 な実装をするところまでで終わっていました。
よって今日は リファクタリングで他のデータストアへの変更、モック差し替え のところまで進め、本章を読了したいと思います。
4. リファクタリング
ビジネスロジックを受け持つFavoriteServiceクラスを改めて確認してみるところから。
- 「いいね」のデータ登録部分は EloquentであるFavoriteクラスに依存
- つまりMySQLに接続できるEloquentが前提になっている
ビジネスロジックから特定のデータベース操作を取り除く
以下の順序で本質部分以外を取り除き、抽象化を進めていきます。
- Repositoryを抽象化するインターフェースを作成
- データベース操作を担当するRepositoryクラス作成
- Serviceクラスはインターフェースを参照
- インターフェースと具象クラスを紐付ける
「いいね」データの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で複数レコード作るのを確認
$ 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、キャッシュ、ファイル、SaaSのAPI、etc...
- テストコードでは本番とは違うデータベース使うこともある
- そもそもDB使わない場合もある
- そんなデータストアの参照先が変わっても
- プログラムの変更範囲は限定的にしたい!
- それ、リポジトリパターンで解決できるかも!
ただ図をみるのもなんなので、書いてみました。僕の場合、図は自分で書かないと理解できないのです…
リポジトリパターン、僕なりの解説
①密結合
- 状態
- 問題点
- データストアが変わった場合、ビジネスロジックの層に改修が入る
- 影響範囲が大きい
- 適切に切り離していないのでテストしにくい
②ビジネスロジックからデータストアを切り離す
③インターフェースを用いて疎結合な状態に
- 状態
- 問題点
- 最初に設計し、インターフェースを実装しておく必要がある
- やはり後からの変更は大変
- 設計大事
文中の解説
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つのファイルを作る。
- データベースアクセスを受け持つEloquent(Favorite)
- ビジネスロジックを受け持つサービスクラス(FavoriteService)
- リクエストを受けるコントローラクラス(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)の実装
- 主な仕様
<?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)
やってみて
分量が…
明らかに分量が増えすぎていて、今回でも終わりませんでしたな(遠い目
しかし、それだけ理解するためのスタートは切れているってことだし、これをアウトプットの形に結実させれば、リポジトリパターンと抽象化と疎結合に関しては、一度道を通ったことになるかなって思ってます。
後編パート2がさらに2つになってしまいそう。 / はてなブログに投稿しました
— まみー (@mamy1326) 2018年12月11日
学習記録:12月9日(日):【輪読会まとめ 後編パート2】Laravel Webアプリケーション開発 Chapter5 データベース(リポジトリパターン) - 叫ぶうさぎの悪ふざけ https://t.co/Fx8QWUyUfA #はてなブログ
抽象化
抽象化を本当に1分で言えと言われるとこうなるのかな、とか思いつつ、まだまだ自分の言葉にできてないよなって思ってます。そもそもこの章のコード動いてないし。もうちょいがんばろ。
抽象化って人間にわかるように説明すると何なのよ!となると「本来そのプログラムや層が本質的にやるべきことに集中するため」に行う「本質的じゃない内容の外出し」と理解してるんですが、その解説も含めたい。そもそもコードがまだ動いてないので、引き続き更新する。図を自分で書くと理解深まるなー
— まみー (@mamy1326) 2018年12月11日
抽象化を解説してみた
自分が本当に解説できるのか、というのと、エンジニアリングにまるで関係のない人に通じるのか、どう思うのか、を聞いてみました。思った通りというか、11月前半の僕と大して変わらない認識でちょっと安心。
これを奥さんに理解できるように解説できたらまずはよし、って感じかなあ。
- プログラムってのがあってだな
- 奥さん:(ぬいぐるみを頷かせる)
- こいつはユーザーにサービスを提供するのが責務
- 奥さん:(ぬいぐるみを頷かせる)
- その責務にどのデータベースを使うのか、は関係ない
- 奥さん:(ぬいぐるみを左右に傾かせて「はてー?」)
- データベースはMySQLとかPostgreSQLとかOracleとかいっぱいある
- 奥さん:(ぬいぐるみを頷かせる)
- それがサービスの成長にしたがって他のデータベースも併用しなくちゃいけなくなった
- 奥さん:(ぬいぐるみを頷かせる)
- もしこの時点でプログラムが「MySQLしか使えませーん」となってると全部修正しなくちゃいけない
- 奥さん:(ぬいぐるみをビクッと動かして驚いたアクション)
- めっちゃ時間もお金もかかる
- 奥さん:(ぬいぐるみを大きく動かして「大変だー」のアクション)
- じゃあどうするか。じゃーん!「抽象化!」
- 奥さん:(抽象化!と手を突き上げるアクション)
11月当初の僕なら、これすら言葉にできなかったんですが、ぺちオブ #phper_oop のおかげでだんだんと言葉にできるようになってきた。試しに奥さんに「抽象化」って言葉から何を連想する?と聞いてみたら、傍にあったぬいぐるみをちょこまか動かして「私!ぬい!抽象化!」って言ってたのです。
— まみー (@mamy1326) 2018年12月11日
その表情と動きだけで勉強する気持ちを根こそぎ持ってかれるくらいかわいらしいんですけど、それはグッと維持しつつ「やっぱりそうだよねえ」と言いながら、僕なりの「OOPでの抽象化」を言葉にして解説してみた。しかしちょこまかとぬいぐるみを動かして頷くだけで理解はしていただけなかった模様!
— まみー (@mamy1326) 2018年12月11日
というわけで引き続き…
僕の場合は、この抽象化を奥さんに解説できるレベルまでなんども話しては掘り下げ、言葉の意味を解説して僕自身も深く理解し、初めて他人に解説できるようになるんじゃないかなって考えてます。
— まみー (@mamy1326) 2018年12月11日
というわけでこの辺の話や思いを加筆修正しつつ、コードを動かすところまで持って行こう。
というわけで、リファクタリングは次回に続きます!楽しい!
学習記録:12月10日(月):はじめよう!要件定義 Chapter-03&Chapter-04
これは 俺のインプットアウトプット記録 Advent Calendar 2018 の10日目のエントリーです。
かつ、学習記録:12月1日(土):はじめよう!要件定義 Chapter-01&02 の続きでもあります。
たぶん1日目にも言った気がするんですが、現在の業務にも密接に関わってくる内容だし、著者の羽生さんに教えを受けた(主催の勉強会に参加した)身としては、通読して業務に活かさなければならない。
というわけで続きを読み進めてみます。
前回までのあらすじ
第1部 要件定義ってなんだろう?、の以下を読み進めていました。
Chapter-01 要件定義=要件を定義すること
あらすじというか、書籍から得たものはだいたい以下の2つに集約される気がします。
依頼した人が
— まみー (@mamy1326) 2018年11月29日
出来上がったものに対して
「これならOKです」
と言うために
「何がどうなればいいのか」
ということを明確に定めたもの
なるほど、となる。
人と人とは「簡単には」わかり合えないっていう前提で、っていつも言います
— magnoliak (@magnolia_k_) 2018年11月29日
お互いに驚くくらい思い込みによる暗黙の前提で喋っている
あと、関心があることしか喋ってない
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、機能、データを決める
このように、ユーザーが何をしたいのか、ゴールから逆算すると言うことは
- 納得する順序を把握して
- それを満たすために必要なことを決めていく
ということだと書かれています。これまたごくごく当たり前ですけど、これを間違って酷いことになった現場やプロジェクト、サービスを結構な数見てきました。
で、完成したソフトウェアという成果をだすための要件を決めるには
- UIを決める
- 機能を決める
- データを決める
大雑把だけど、この流れで進めていくんだよ、と。
じゃあ具体的にこの3つをどう決めていけばいいのか。
UIを決めるとはどういうことか。機能とは具体的にどんなことを定めるのか。
というわけで、第二部からは要件定義の詳細を順番に見ていくことに。今から楽しみです。
要件定義の心がけ
要件定義のために要件定義をするなよ、とあります。
なんのこと?と思わなくもないですが、なんとなく予想もできます。
依頼主が求めること
完成したソフトウェアであり、そのソフトウェアがもたらしてくれる便益である、と。
それを達成し、本当の意味で 完成としての納得が得られる よねと。
便益(ベネフィット)はどう得るの?
実際にソフトウェアが完成して利用し始めないと、つまり運用してみないと判断できないですよね。 ゆえに何はともあれ 完成して利用できる状態にする のがゴール設定である、とあります。
品質は一定以上ないとダメですけど、そのチームの状態(人員、リソース、スキル)、予算、期間を鑑みて、ゴールするにはどうするのかを考えて要件定義をし、チームを作り、開発を進め、リリースしていく。
そうして初めて、クライアントの要求に応えるための第一歩を踏み出せるのだよな、と改めて思ったりしてました。
ソフトウェアを利用できるには何が必要?
ユーザーが使えることが大前提。デプロイですよねと。どうでもいいんですが、デプロイって「配備」って意味なんですね。知りませんでした。
とはいえ、デプロイしたからって言ったって、品質がボロボロじゃあどうにもならん。すなわちテストせねばならんよな、と。
プログラマは行き当たりばったりで作ってもだめだし、要件に基づいて作業せねばならん。 その結果として、一定の品質のソフトウェアができてこと利用できるってことだよなって改めて考えつつ読みました。
ソフトウェア開発プロセス
企画->業務設計->要件定義->設計->実装->テスト->導入->リリース
という一連のプロセスがある。 このプロセスの中には、書籍のスコープから外れる「プロジェクトマネジメント」の理解も必要。 とはいえいっぺんに理解は難しい。
しかし利用できないソフトウェアは無価値。
ソフトウェアを実現するために要件定義という工程が必要。要検定義をせずに後工程に先送り(設計でカバーとか)すると、必ずそこで要件定義は発生するよねと。
定まってないものは作れないし、たとえ先行で作ったとしてもそれは完成には至らないし、出来上がらない、つまり事実上作っていないものはテストできない。テストできてなければデプロイできない。
後工程につなぐための要件定義
であることを忘れるなよ、とあります。
後工程に対する理解を深める
後工程に繋ぐことをおざなりにしたら、それは要件定義が終わってないということ。それで前に進むのは 要件定義のでっち上げ になる。
でっち上げた要件で作ったものは納得を得られないし、ちゃんとできたとも言えない。 そのようにならないためにも要件定義をやり、これなら自分でも作れるぞ、という 後工程への理解 を深める必要があるんだなってことが書いてあります。
後工程の理解を深めない=不要な人
要件を定義さえすればいい、後工程は実装者が考えるから自分はそれ以降知らなくていい、なんてことで進めた要件定義は実務に耐えられない。
結果、後工程で要件定義をやり直さないとプログラミングできないなら、その要件定義を行った人は無価値である。つまり不要な人員であることを示すことだよね、と。
本当に「ですよね!」と思うことがこれでもかと書いてあります。
でかいプロジェクトあるあるで「要件定義するだけの人」「基本設計書を書くだけの人」「詳細設計書を書くだけの人」「プログラミングするだけの人」「テストするだけの人」「リリース判定するだけの人」「ISOの審査基準を判定するだけの人」なんてのを何度か体験したことがあります。
それに対して公に何かいう気は全くありませんし、ここまで書けば何を言いたいかわかっていただけるとも思ってもいます(笑)
それくらい、工程同士の繋がり、プロセスの繋がり、チームでの様々な共有、などなど、1つの生き物として捉えないと死ねるよな、って思ったりしています。
要件定義は楽じゃない、急がば回れ
簡単ならこの本を読まなくてもいい。でも実際は難易度が高い。その高い難易度、そこからくる後工程の理解不足のギャップをどう埋めるのか。
それこそ後工程との連携であり、クライアント、チーム内で合意形成をしていきながら要件定義をしていく。
これなら進められるぞ、ということを後工程の人と検討しながら進めていく。
実際非常に手間のかかる要件定義。でも後回しにすればするほど破綻する。個人的には1つでも後回しにしたら緩やかに破綻に向かい、一定の境界を超えたらいきなり破綻、くらいでちょうどいいのでは、と思ったりします。
そのためにも、僕の上司のように まずサクッとUI、画面、データをヒアリング してから、 素早くモックに落とし込み、モックベースで要件を詰めていく のって最強なんだろうなあと思います。
急がば回れ。そしてできれば「急がば回りつつも可能な限り速く回る」ためのテクニックは持っていたいなって思うのでした。
読んでみて
UIってただ機能が動けばいいってもんじゃないし、それは操作者であるユーザーが扱えるUIじゃなきゃ意味がない。要検定義のための要件定義するなよ、ソフトウェアを作るのが目的じゃなく、利用して便益をえることだぞ、手間がかかるけど急がば回れだぞ、と言うメッセージが響きました。
— まみー (@mamy1326) 2018年12月10日
要件定義の話でここまでワクワクすることは今まで一度もなかったと思います。スラスラ読める本に出会ったこともなかったし、自分のやってきたことを客観的に振り返ることもなかったと思います。
しかしこの本は、僕にそれらをもたらしてくれています。
読みやすいというのもあるんですけど、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 クエリビルダ
リスト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クラスからクエリビルダを取得
- ①DatabaseManagerクラスのインスタンスをサービスコンテナから取得
- ②connection()メソッドでConnectionのインスタンス取得
- ③tableメソッドでクエリビルダのインスタンスを取得
<?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分、お話させていただきました。
- 登壇会場:吉祥寺.pm13
- 発表時間:15分
- タイトル:とある負傷兵の回復日誌
体は人それぞれ
同じ負荷で同じことができるわけもなく、耐えられるのも壊れるのも人それぞれのボーダーラインがあります。
しかし人はそれをよく知らない。ついつい周りに流されてしまう。スライドには書いてないこともありますが、そんなことをお話したかと思います。
実は自分のことをよく知らない
人それぞれな上に、自分のことをよく知らないんですよね、意外と。
なので、自己分析しましょう。わからなければプロである医者を頼りましょう。医者が頼りにならなければセカンドオピニオン受けましょう。
そして放置せず、根性でなんとかせず、改善に向けて適切な休養と処置を受けましょう、というお話をしたかと思います。
己を知ることから全てが始まる。そこから何をするのか決める。エンジニアリングにも通じるかな、と思いながら話した記憶が蘇ります。
じゃあ知ってみよう
自分がどういう状態、状況なのかを知る。客観的に知る必要があるので、書き出すのが一番です。
僕は自分の体調がおかしかったとき、医者にいく前に症状や状況、経緯を紙に書いて持っていった記憶があります。
しかし1度目の医者ではラチがあかず、何度か病院と医者を変更し続け、やっと今の主治医に出会えました。
正しく知るために行動し、対処していく。エンジニアリングではそれができても、いざ自分の体となるとなかなかやらないことも多いかと思うので、ちゃんとやりましょうというお話をしたような気がします。
知った結果を活用しよう
状況、状態、処置の方法を知ったなら、適切な診断と処方を受け、生活習慣を改善し、休養し、必要なら薬を飲み、安定させていく。
完治しない場合もありますが、確実に改善に向かうと思います。
僕も完治はしていませんし、今でも薬にお世話になっていますが、完全に自律神経を壊した頃と比べれば8割くらいマシになりました。
登壇してみて
自分の辛かった体験や経験を、少しでも多くの人に知ってもらって、もし辛い思いをしている人がいたら、少しでもヒントになればいいな。
もし諦めている人がいるなら、前に進む方法があるんだよ、と言うことを伝えたかったです。
結果として、想像以上に大きな反響があり驚きました。
やってよかったなって思うと共に、何度か別の場所でも話してみよう。僕の経験をきっかけに病院にいくとか、自己分析してみるとか、それだけでもきっと希望の1つになるんじゃないか、って思ったことを覚えています。
反響
うわあああ紹介して頂いてありがとうございます!こんなに反響あると思ってなかった…かかりつけのお医者さんにレビューしてもらおうかな / ITエンジニア向け勉強会「吉祥寺.pm13」に行ってきたら特濃レベルに濃かった話(前編)。 - うさみ日記 (id:naoto_usami / @otoan_u) https://t.co/JL91FAxKev
— まみー (@mamy1326) 2018年2月20日
ちょっと時間経ちましたけど、改めて神経内科に行くって発想って、どこにも紹介されていないし、もの凄い有益だよなってうさみさんのブログを読んで思ったんですよ、これはもっと広まるべき内容ですよ!!
— magnoliak (@magnolia_k_) 2018年2月21日
② 2月28日:【PHP勉強会@東京】とある負傷兵の回復日誌
- 登壇会場:第123回 PHP勉強会@東京
- 登壇資料:とある負傷兵の回復日誌
- 時間:20分
同じ登壇は3回やってみたほうがいい、と以前そーだいさんに言われてました。理由は明白で 違う分野と場所で3回話したら、だいたい自分のものになっている というような理由だったと思います。
僕も心から同意したので、2017年11月30日のLTLoversで5分、同月の吉祥寺.pmで15分、そしてこの会で20分、3回話してみました。
結果としては、毎回反響が増えていって、こういった悩みを解決するのも技術力なんだな、って実感した記憶があります。
詳細は前回とほぼ同じなので割愛しますが、またどこかでやろうかなって思ったりしています。
③ 3月28日:【PHP勉強会@東京】PHPerが知るべきTCP/IPのきほん
- 登壇会場:第124回 PHP勉強会@東京
- 登壇資料:PHPerが知るべきTCP/IPのきほん
- 時間:5分LT
5分でやるには無理がありましたw
でもなんとか「こういうもんだよ!」ってのを、手書きの図を使ってやってみた登壇。細かいところは話きれずに別の人に解説をいただくことになるんですが「こういうことしてんだな」ってのを知るきっかけにはなったかなあ、とは思っています。
絵文字でわかるスリーウェイハンドシェイク
— 湊川あい🌱わかばちゃんと学ぶシリーズ発売中 (@llminatoll) 2018年3月28日
💻<「今ちょっといい?」
「ええで」<🕋
💻<「ほな接続するわ」#phpstudy by @mamy1326
やってみて
いやー5分で話すテーマじゃなかったな!と。 無理やり5分に収めたので、削除したスライドがあって、そこを思い切り会場で別の人に訂正され教えていただくというありがたい事態にw
個人的にはすごくすごくありがたいことだったんですが、会場としては そんなにマサカリ投げなくても という反応だったみたいですw
でもこれをきっかけに、DNSを学んでいくことになったので、やってみてよかったなって思います。
いずれ15分テーマくらいでやってみようと思ってます。
④ 4月8日:【インフラ勉強会】とある負傷兵の回復日誌
- 登壇会場:インフラ勉強会
- 登壇資料:とある負傷兵の回復日誌
- 時間:20分
インフラ勉強会を知った直後くらいに、定期的に LT大会があることを知ったんです。そこで個人的に「やってみたいなー」って呟いたんですよね、確か。 そしたら たけし さんに拾っていただいたんです。あれよと言うまにエントリーが決まり、LTの内容でもないし、テーマに即しているわけでもないのにトークさせていただくことに。
その時のNaccoさんのアナウンスがこちらでした。 環境紹介LTの最後に、LTじゃないけどしゃべりたいということで時間をいただいてお話させていただくというありがたい体験をさせていただきました。
本日4/8の #インフラ勉強会 は
— Nacco@ボロちゃん (@climbing_nacco) 2018年4月8日
20:00~
インフラ勉強会LT大会「わたしの環境紹介LT」 後半戦
スピーカーは@barson_4 さん@sakuraba_nemu さん@nagashi_ma_w さん@andesm さん@dozonot さん@mamy1326 さん
22:00~
@ts03511 さんの
「新旧CentOS比較」
お楽しみに😆https://t.co/lNvnOsr5ZB
ここで初めて 音声と画面共有とチャットを用いてオンライン登壇する というのを体験させていただいた貴重な第一回でもありました。
この回をきっかけに、このあと何回かのオンライン登壇をさせていただくことにもなりました。
⑤ 4月25日:【PHP勉強会@東京】PHPerが知るべきDNSのきほん
4月のTCP/IPで学習不足だな、じゃあ何からやるかな、よしDNSをやろう、と思うきっかけになってました。
また、4月5日から前職のリードディレクターと毎日の学習を始めたんですが、その題材が 3分間DNS基礎講座 でした。
なのでその内容の前半を20分にまとめてトークしてきました。
- 登壇会場:第125回 PHP勉強会@東京
- 登壇資料:PHPerが知るべきDNSのきほん
- 時間:20分
この本のいいところ
前月に学習したTCP/IPの基礎がちゃんと書いてあり、そこからDNSの話に繋がっていました。かつ1つの題材が本当に3分で読めるよう分割されていて、かつわかりやすい。
論点を3分でわかることに絞る ことを延々と連結させているのが素晴らしかったです。おかげでDNSの基礎を学びつつ、もっと知りたい、ここはふわっとしていてわかりにくい、ここは別角度から理解したい、と次々とやってみたいことが湧き出てきて、これがのちの buildersconの登壇 につながりました。
本当にいい本です。何から始めればいいのやら…でも時間かかるのはなあ…と思っている人がいたら、読んでみることを強くオススメします。
やってみて
毎日の学習、約1ヶ月の成果としても登壇したんですが、ここら辺から、日々のインプットアウトプット->登壇のためのインプットアウトプット、の回転の中に身を置き始めたな、って思います。
最終的には他の本も読み、RFCをいくつか読み、スライドを作っていくことになるんですが、この時はまだそこまでやっていませんでしたね。
何はともあれ、今年の集大成とも言えるスライドのきっかけになった登壇でした。
⑥ 5月25日:【吉祥寺.pm】エンジニアリングで解決するために非エンジニアと一緒にやったこと
- 登壇会場:吉祥寺.pm14
- 登壇資料:エンジニアリングで解決するために非エンジニアと一緒にやったこと
- 時間:15分
退職エントリーにも書きましたが、前職のチーフディレクターと一緒にやったことをトークしました。
テーマがエンジニアリングで解決したこと、しなかったことだったので、それにのっとった形でした。
やってみて
改めて自分が良きパートナーに恵まれていたこと、もがき苦しんでいた場所から脱することができたこと、僕自身の本当の成長がここから始まったこと、非エンジニアでもエンジニアリングの内容を理解してもらうことは可能なこと、など、いろんなことをお話できたかと思います。
とにもかくにも、僕の2018年はこれに始まったに尽きるんですけど、それがどうして行われ、どんな効果があったのか、を伝えることはできたかな、と思います。
反響
teckl さん本当にありがとうございます!
- ステークホルダーと視座を共有
— teckl (@teckl) 2018年5月25日
- 特にディレクター
- 互いの技術を毎日LTしあう
- 相互理解
良いなぁ… #kichijojipm
DNSの基礎がわかり、業務に役立った
— teckl (@teckl) 2018年5月25日
ディレクターがTCP/IPとDNSを理解
そんなにかかる?から「なるほどかかる」へ
素晴らしい。。 #kichijojipm
川柳はその場の流れで入れましたねw
技術力
— teckl (@teckl) 2018年5月25日
あるならそれで
エンジニア
俳句縛りw
#kichijojipm
⑦ 5月30日:【php勉強会@東京】エンジニアリングで解決するために非エンジニアと一緒にやったこと
吉祥寺.pmで話したことの再演です。
- 登壇会場:第126回 PHP勉強会@東京
- 登壇資料:エンジニアリングで解決するために非エンジニアと一緒にやったこと
- 時間:15分
再演なので詳細は割愛します。
⑧ 6月16日:【PHPカンファレンス福岡2018】MySQLで画像 を扱うメリット・デメリットと障害・解決事例
- 登壇会場:PHPカンファレンス福岡2018
- 登壇資料:MySQLで画像 を扱うメリット・デメリットと障害・解決事例
- 時間:15分
2017年、PHPカンファレンス(東京)で話す予定だった内容を、8ヶ月越しに福岡で達成できました。話の内容としては、前職のエンジニアブログ に割と詳しく書いたので、そちら参照していただけたら幸いです。
トピックとしては、声出しリハだけでも20回はやったってことくらいですかね。おかげで時間ほぼぴったりに終わることができました。
⑨ 6月23日:【インフラ勉強会】エンジニアリングで解決するために非エンジニアと一緒にやったこと
- 登壇会場:【オフライン】インフラ勉強会 半年記念イベント
- 登壇資料:エンジニアリングで解決するために非エンジニアと一緒にやったこと
- 時間:5分LT
発足半年記念のオフラインイベント、いわばオフ会を東京・大阪で実施するということで、LTに応募して登壇してきました。
内容としては再演であり、5分の短縮版でもあります。
⑩ 6月27日:【PHP勉強会@東京】DNS浸透問題〜なぜ浸透と言ってはいけないのか〜
- 登壇会場:第127回 PHP勉強会@東京
- 登壇資料:DNS浸透問題〜なぜ浸透と言ってはいけないのか〜
- 時間:5分LT
DNSを学習しはじめて突き当たった浸透問題。なんで言ってはいけないのか、というか、 なぜ浸透と言ってしまうのか を僕なりに考察した内容を、軽く5分でお話しました。
物議をかもすテーマだったんですが、本気でDNSを学ぶ上で避けて通れないテーマでしたのでお話させていただいた記憶があります。
5分の内容なのでそれほど濃密ではないので、15分に拡大した次の登壇資料をみていただいたほうがいいかなあと思います。
⑪ 7月27日:【吉祥寺.pm】DNS浸透問題「なぜ浸透と言ってはいけないのか」 〜正しい知識と手順を添えて〜
- 登壇会場:吉祥寺.pm15
- 登壇資料:DNS浸透問題〜なぜ浸透と言ってはいけないのか〜
- 時間:15分
テーマは「コード & レスポンス」
自分のアウトプットと、その結果得られたこと、もちろんコードだけでなく、デザイン、インフラ、ハードウェア…人事総務!経営!などなど、ジャンルは問いません
ということだったので、自分の日々のインプット、アウトプットから得られたこととして、前回LTした内容をさらに深く調べ、考察し、検証し、解決するために僕がやっていることを添えて、15分に拡大してお話してきました。
やってみて
浸透は使ってはいけないんじゃなく、そもそも根本的に間違っていると言うことはわかっていたんですが、じゃあなぜ使ってしまうのか、使わないようにするにはどうすべきなのか、使ってしまう事態をそもそも避けるには何が正しい手順なのか。
正しい手順と知識を持って行動することこそ根本を知り本質的な対応をすることができる唯一の手段。 とはいえ正論だけで人は生きてはいけない。
そんな堂々巡りに答えを出した結果をお話しました。
みんななるべく同じ言葉(定義)を使い、楽しくエンジニアリングしたいものだし、楽しさこそが技術を世界を発展させていくんだと僕は信じているので、そんな思いも込めました。
結果として、思った以上どころか想像以上の反響をいただけたので、この次に控えていたbuildersconの1時間セッションへの大きな励みをいただきました。
登壇してよかったな、って心から思いました。
前半を振り返って
11回の登壇をしてたんだな、と回数を数えて自分でも驚いています。 日々のインプットとアウトプット、プログラミングや技術のテーマだけでなく、テクニックも技術だよな、それは人生全てに当てはまるよな、っていう思いが強いです。
そう思ったからこそ、健康の話や非エンジニアとやったことなど、エンジニアリングと直接関係のないことにフォーカスを当てたりもしてました。
働くことを生活と融合させることが僕の至上命題であり生きる意味の1つなのですが、そこを強く意識もしていたなって思います。
そして4月から学習を開始したTCP/IPとDNSですが、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 導入部
- 一般用語(移転、移行)と違い
- 実行はartisanコマンド
- DBに自分で接続する必要はない
- PHPコードで定義のためgitなどでバージョン管理しやすい
- 以下の流れで解説
5-1-1 マイグレーション処理の流れ
図5.1.1.1:マイグレーションの流れ
ただ読んで見るのもなんなので、書き起こしてみました。 この書き起こしがあったので、先日書いた 学習記録:12月7日(金):Webフレームワークに依存しない、PHP製のシンプルなSQL マイグレーションツール「Mig」を使ってみた でも理解がスムーズだったなあと思います。
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 メソッド
- upメソッド:データベース定義の追加変更を行う処理
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 マイグレーションの実行状態を管理するテーブル
- 実行、未実行、ロールバックの状態管理するテーブル
- 以下に登録されていないマイグレーションファイルを実行
migrate:rollback
コマンドはbatch
がもっとも大きい番号のマイグレーションファイルが対象
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\seeds
の DatabaseSeeder.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の利用
- より現実に近いテストデータをよしなに作ってくれる
- Laravelではなく外部のライブラリ
- 詳しくはこのへん
表5.2.4.1:Fakerで作成できる主なダミーデータ
項目名 | 出力データ |
---|---|
name | 氏名 |
メールアドレス | |
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という便利機能
- 有効にするには以下
リスト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 メソッド
リスト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
- where句に相当
- XXXにはカラム名
リスト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 データ操作の応用
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されたりのタイミング
- ミューテータ
リスト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の確認
リスト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で作ってみようと思っています。
というわけで後半に続きます。
【技術書典6】誰かに背中を押された話【執筆に向けて】
これは 技術同人誌 その2 Advent Calendar 2018 Advent Calendar 2018 の12月8日のエントリーです。
技術書典、参加するだけでも最高に楽しくて刺激的で、持ち帰るものも膨大なら、成長も膨大。
個人的に 技術書典は成長の種を得るところ って思っています。
で、参加するうちに 書いてみたい と思うようになったんですよね。ポエムでもいいし、僕が活動理念にしている 初学者に寄り添い、背中を押す ことができる場所だなって、いろんな人の書籍を読むたびに思ってたんです。
そんな折、僕が主催に属している エンジニアの登壇を応援する会 の Slack で共同執筆の話が持ち上がりました。 これはやるしかない!と思い、編集長の ariaki さんと話しながら、ざっくりまとめた内容を綴ってみたいと思います。
というわけで、3つのテーマのうちの最初 誰かに背中を押された話 をします。
はじめに
人は誰もが背中を押されて生きている と言えるかもしれません。 しかし押されるタイミングが合わなければ、羽ばたけない人もいる。 僕もそんな1人でした。
エンジニアとして、何をするにもとっかかりが掴めない。 ずっと暗がりでもがいている。 諦めた方がいいんだろうか…
そんな漠然とした悩みを打ち明けた人がいました。
そこで本章は、僕が人生で初めて ガッツリ背中を押されるまで を解説していきます。
背中を押されるまで
不惑をとうに越え、人生そのものに焦りを迎えていたある日、同僚の何気ない言葉 から僕の背中は押され続けてきました。
きっかけ
43歳になっていました。何もかもが焦ります。 これだけは誰にも負けないと自信を持てるものが何もない。
そんな折、同僚のチーフディレクターとランチに行った時のことです。
- D:「最近なんか悩みある?」
- M:「自分の技術、どこに特化させるか未だに答えが出ません…」
- D:「興味のある分野とかないの?」
- M:「この先20年役立つ、自分がやりたいの両方で、インフラですね」
- D:「なんか気になる書籍とかないの?」
- M:「あり過ぎて積みまくってます」
- D:「じゃあそれをお互いに毎日読んでLTし合おう。ちょうど俺も読みたいのあるし」
こうして、僕の毎日のインプット生活がスタートしたのでした。
やりかた
時間、互いの労力、計画性、全てに一定以上のものが求められました。では具体的にどうやったのか解説します。 個人的には ここまで1対1で他人が時間を割いてくれるものなのか と驚愕と歓喜が同居した複雑な気分と、モチベーションが常に補充される不思議な体験でもありました。
前準備
- 課題図書を決める
- それぞれが1日30分(目安)読む
- Markdownで得たこと、調べたことを記録する
LT
- 互いに10分間、記録したことを相手にプレゼンする
- 理解が及ばないところは互いに質疑応答する
- 10分経過で強制終了
- 続きは次回
ゼロ秒思考の実施
- 大きめ(できればA4)の紙を用意
- 書きやすいサインペンを用意
- 最初の1分でお題を決める
- 残りの時間、ゼロ秒で思考したことをとにかく書き出す
- 終わったらトピックを互いに話す(3分程度)
実施日
- 月曜日〜木曜日の18:30-19:00
- 金曜日はMTGが多いのでお休み
- 読書などのインプットは毎日続ける
- 書籍以外でも、技術メモ、他人のブログについてなどテーマは無差別
毎月の振り返り
- KPTを実施
- 翌月にすべきことを取捨選択
- より効果的な時間を作り上げていく
- 業務の改善ポイントも振り返る(ゼロ秒思考の振り返り)
結果
当エントリー執筆段階(12月8日)で、9ヶ月目に入っています。それまでどんな理由があっても1日も欠かしていません。 今はその会社を退職していますので、お互いのLTは実施していませんが、以下の結果が得られました。
- 継続的なインプット・アウトプットが習慣化
- DNSの話は builderscon tokyo 2018 のトーク Webアプリケーションエンジニアが知るべきDNSの基本 につながりました
- 非エンジニアに説明して理解を得られる状態に落とし込む
- エンジニア以外でも理解できるよう言葉の解説や正確性を重視した
- 学習時間の確保のための生活時間の見直し
- 寝る前のどの時間にやるのか決めた結果、1日のサイクルが変わった
- 1つの技術の深掘りによる理解
- 解説することによるさらなる自分へのインプット
- わかる!楽しい!
- 学習が楽しくなり、現在でも仕事が楽しい瞬間が増えた
- 解説したい!話したい!
- 特に自分のような悩みを抱えているかもしれない人の背中を押したい
出会いは必ず来る
こういう人には一生出会えないと思っていましたし、想像もしていませんでした。 全て自分で理解して進む必要があるんだ…そう思い込んでいたんですよね。 自分がいい歳だっていうのも拍車をかけたと思います。
しかし、そんな思いを杞憂にして吹き飛ばしてくれた同僚がいました。 そして背中を押してもらいました。
彼とは今でもたまに話し、これからも悩めば相談するでしょう。
- M:「俺、この恩をどうやって返せばいいかわからんっす」
- D:「それはお互い様でしょう。おかげで自分もDNSを理解したしw」
- M:「俺も組織論やリーダーシップについて目から鱗なところたくさんあったな」
- D:「重要なのは継続すること。それが達成できたならそれでいいんですよ」
こんなこと話したな。なんやねん最高かよ。って思ったし、この恩は自分のエンジニアの師匠の言葉 受けた恩は後続に送っていけ。それが未来だ という 恩送りの精神 で誰かに送り続けていこうと思います。
こうして、僕はインプット・アウトプットの輪廻の中に身を置き、今でも悩みながらぐるぐるぐるぐる回っていたりします。
押されたからには…押していく!
そう、自分も押さなくては、という気持ちになるわけです。 同じように困っている人が絶対にいるはずだ。
MySQLやDNSで登壇した際、自分が困ったこと、わからなかったこと、技術書やブログ、RFCで理解できなかったことを調べ、その行間に含まれている暗黙知を見える化してきました。
最初は 高度な話でもないのに必要とされるかな? という不安がありました。 しかしインプット・アウトプットを続け、登壇し、様々な人から感想をいただいて、不安が確信に変わりました。
人はわからないと不安になります。不安が続けば嫌になります。嫌になれば諦めて辞めちゃいます。
その不安を取り除こう。自分にできることでやっていこう。それをエンジニア人生の目標にしよう。
そう、思い定めたのでした。
もう少し続くんじゃよ
今現在、エンジニアとしても幸せだなって思える瞬間がすごく増えてきました。 MySQL、DNS、だけでなく、OOPやドメイン設計もわかるようになってきて、実際のコードを書くのがより楽しくなってます。 (OOPは「ぺちオブ」という勉強会があり、僕より遥かに濃密で深く強い意志で活動されている素晴らしい方々がいらっしゃいます。)
もちろんインフラを操作する時も前よりずっと安心感がありますし、少なくとも、しんどいな、手に汗握るな、というネガティブな瞬間が明らかに減りました。
稀有な人に背中を押されたな。彼には 今後も友達でよろしくね と言ってもらえて、こんなナイスガイがおるんやな、って思ってますが、じゃあ俺もナイスガイになってやろう。
そして誰かの背中を押していこう。 そう思っていた矢先、主催のariakiさんに誘っていただき、エンジニアの登壇を応援する会に入ることになったのでした。
というわけで、 誰かの背中を押していく という話に続け、執筆します。
やっていくぞ!