Laravelアプリケーション学習⑬（Eloquentとクエリビルダ）

本記事ではLaravelのアプリケーションを理解し、より良い設計・アーキテクチャを構築できるように学習したことを簡潔にまとめています。

#目次
1.Laravelのアーキテクチャ
2.アプリケーションのアーキテクチャ
3.HTTPリクエストとレスポンス
4.<font color="LimeGreen">データベース</font>
5.認証と許可
6.イベントとキューによる処理の分離
7.コンソールアプリケーション
8.テスト
9.エラーハンドリングとログの活用
10.テスト駆動開発の実践

#4．データベース

#4.1 Eloquent
EloquentはActive RecordライクなORM(Object Relational Mapping)で、Laravelを代表する機能の１つ。データベースとモデルを関連付けさせるもの。

##　クラスの作成
下記コマンドでEloquentのクラスファイルを作成できる。

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


##　Eloquentのプロパティ

Eloquentでは、対応するデータベースのテーブル名や主キーのカラム名などに対してあらかじめルールが定められている。
下記にクラス名「Author」を指定した場合と様々なルールを示す。


```php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
   use HasFactory;

   // ① t_authorテーブルを関連付けする
   protected $table = 't_author';

   // ② テーブルの主キーをidではなく、author_idとする
   protected $primary = 'author_id';

   // ③ タイムスタンプを記録しない（デフォルトはture）
   protected $timestamps = false;

   // ④ nameとkanaカラムの指定可能
   protected $fillable = [
      'name', 
      'kana'
   ];
   
   // ⑤ idを指定不可にする
   protected $guarded = [
      'id'
   ];   
}
```
その他、以下のプロパティが存在する。

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

## データ検索・更新の基本

###１．全権抽出 - all

allメソッドはテーブルの全レコードを取得するメソッド。
戻り値は`Collectionクラス(Illuminate\Database\Eloquent\Collection)のインスタンス`が返される。Collectionの要素は、Illuminate\Database\Eloquent\Modelクラスのインスタンスであり、foreach分で1レコードずつ取り出せる。

```php
$authors = \App\Models\Author::all();

foreach ($authors as $author) {
   echo $author->name; // nameカラムの値の出力
}
```
また、Collectionクラスには、アイテム数のカウント条件に合致したアイテムのみを返却する機能などがある。下記に、`count()`を用いてレコード数の取得の例を示す。

```php
$authors = \App\Models\Author::all();

// レコード数を取得する
$count = $authors->count();
```

下記に`filter()`を用いて条件で絞り込む例を示す。

```php
$authors = \App\Models\Author::all();

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

// 絞り込んだ結果をforeach文で取得する
foreach ($filtered_authors as $author) {
   echo $author->name;
}
```

### 結果をJSONで取得
APIなどで抽出結果をJSON形式で返すケースでは、`toJson`メソッドを利用する。

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

return $author->toJson();
```
toJsonメソッドの実行結果の例を下記に示す。

```
{"id":1, "name":"太郎１", "kana":"タロウ", "created_at":"2018-07-18", "updated_at":"2018-09-32"}
```

### カラムの値に対して固定の編集を加える
カラムの値を取得する際に、例えば、金額のカラムの値に対して3桁ごとにカンマを挿入したり、片仮名のカラムの値を全角・半角に変換するなど、毎回固定の編集を加えたいケースがある。
同様に、値の登録時でも、フォームの入力値に対して編集を加えてから登録したいケースがある。
このような処理はそれぞれ***「アクセサ」***と、***「ミューテータ」***の機能を利用する。

アクセサはEloquentのクラスにget（カラム名）Attributeの名前でメソッドを定義。
ミューテータはset（カラム名）Attributeの名前でメソッドを定義する。

```php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Author extends Model
{
   public function getKanaAttribute(string $value): string
   {
      //KANAカラムの値を半角カナに変換
      return mb_convert_kana($value, "k");
   }

   public function setKanaAttribute(string $value):void
   {
      // KANAカラムの値を全角に変換
      $this->attributes['kana'] = mb_convert_kana($value, "KV");
   }
}
```

定義されたカラムの利用に関しては、下記コード例に示す通り、通常のカラムと違いはない。

```php
// データ取得時
$authors = \App\Modles\Author::all();
foreach ($auhtors as $author) {
   echo $author->kana; // 半角カナの値が返される
}

// データ登録時
$author = new \App\Modles\Author();
$author->name = $request->input('name');
$author->kana = $request->input('kana');  // 登録時に全角カナに変換される
$author->save();
```

### データがない場合のみ登録する
`findOrCreat`や`firstOrNew`メソッドを使うと、条件に該当するデータがない場合に、新規登録を行う。

```php
$author = \App\Models\Author::findOrCreate(['name' => '太郎']);

$author = \App\Models\Author::firstOrNew(['name' => '太郎']);
$author->save();
```

###論理削除
Eloquentではdeleted_atカラムを利用して削除処理が行われた日付を保存し、「このカラムがnullでなければ削除済みデータである」として扱うことが可能。

（実装方法）
***１．対象のテーブルにdeleted_atカラムを追加
２．EloquentのクラスにSoftDeletedトレイとを追加***

実際に、既存のauthorsテーブルで論理削除ができるように実装していく。
まずはdeleted_atを追加するためにマイグレーションファイルを作成する

```
php artisan make:migration softdelete_authors_table --table=authors
```

作成したマイグレーションにdeleted_atを追加する。

```php
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class SoftdeleteAuthorsTable extends Migration
{
   public function up()
   {
      Schema::table('authors', function(Blueprint $table) {
         $table->softDeletes();  // 追加
      });
   }

   public function down()
   {
      Schema::table('authors', function (Blueprint $table) {
         $table->dropColumn('deleted_at'); // 追加
      });
   }
}
```

マイグレーション実行後、ModelにSoftDeletesトレイトを定義する。

```php
<?php
namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes; // 追加
 
class Author extends Model
{
   use SoftDeletes;   // 追加
}
```

以上で、Authorモデルのdeleteメソッドやdestroyメソッドを実行したときは、deleted_atカラムに削除時間が登録されて削除扱いとなり、データを取得するメソッドでは、論理削除となっているデータは含まれなくなる。

なお、論理削除されたデータを含めて取得したい場合は、下記コードに示す通り、withTrashed()メソッドを使用する。

```php
// 削除済みのレコードも含めて取得する
$authors = \App\Models\Author::withTrashed()->get();

// 削除済みのレコードのみ取得する
$deleted_authors = \App\Models\Author::onlyTrashed()->get();

//復元
$restore = \App\Models\Author::resotre();

// 完全削除
$forceDelete = \App\Modles\Author::forceDelete();
```

##実行されるSQLの確認
EloquentはSQLをあまり意識せずに手軽に利用できるのが最大の利点であるが、レコード数が多いテーブルの検索や、複雑な抽出条件による検索を実行するケースでは、発行されるSQLによっては、パフォーマンスやサーバー不可に影響を及ぼす可能性がある。
→内部で発行されているSQLを意識することが重要である。
下記にEloquentで発行されるSQLを取得するメソッドである、`toSql`メソッドの利用例を示す。

```php
$sql = \App\Models\Author::where('name', '=', '著者A')->toSql();
```
上記の結果として下記のSQL文を取得できる。

```
select * from 'authors' where 'name' = ?
```
上記のtoSqlメソッドは、プリペアドステートメントの形で実行前のSQLを取得できる（toSqlはSQLの作成を行う機能なので、実行は行われない）。実際に実行されたSQLを確認したい場合は、DBファサードの`getQueryLog`メソッドを利用すると、そのリクエスト内で実行されたすべてのSQLを取得できる。
下記にgetQueryLogメソッドの使用方法を下記に示す。

```php
use Illuminate\Support\Facades\DB;

--[略]--

// SQL保存を有効化する
DB::enableQueryLog();

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

// クエリを取得する
$queries = DB::getQueryLog();

// SQL保存を無効化
DB::disableQueryLog();
```
上記コード例で取得した$queries配列の内容は下記の通りで、実行される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
   ]
]
```
上記の結果から分かる通り、findメソッドは内部でIN句に変換されており、検索対象のテーブルのレコード数によってはパフォーマンスに影響を与える可能性を認識できる。

## Eager(積極的な)loading
where句で条件を指定すると、データの数だけクエリを発行してしまう***（N＋1問題）***。

***→`withメソッド`を使うとN+1問題を解決できる（クエリが１行になる）***

```php
① $books = Book::with('author')->get();
② $author = Author::where('id', 1)->get();
```
上記の分を実行して得られるSQLを下記にそれぞれ示す。

```
①　select * from books where 'author'.'id' in (1, 2, 3, 4 ,・・・) →　一行

②　データの数だけSQlを発行する(whereメソッドやall()メソッドなど）
```

###　応用：Eager loadingするリレーションデータを限定したい場合
ある特定の条件にマッチしたリレーションデータのみ、Eager loadingで取得した場合は`with`の中でクロージャを使うことで実現できる。
下記に、titleにfirstを含むリレーションデータだけEager Loadingしたい場合のコードを示す。

```php
$users = User::with(['posts' => function($query)
   {
      $query->where('title', 'like', '%first%');
   }
)->get();
```

#　クエリビルダ
クエリビルダは、メソッドチェーンを使って、SQLを組み立てて発行する仕組みである。Eloquentも、内部的にはクエリビルダのインスタンスを持ち、多くの機能はクエリビルダのインスタンスを持ち、多くの機能はクエリビルに依存している。

## クエリビルダの取得
クエリビルダの取得方法には、主に下記の２通りがある。
１．DBファサード（\Illuminate\Support\Facades\DB)から取得する方法
２．その実態だるIlluminate\Database\Connectionから取得する方法がある。

下記に１のDBファサードを利用したクエリビルダの取得方法を示す。

```php
// DBファサードからBooksテーブルのクエリビルダを取得
$query = \Illuminate\Support\Facades\DB::table('books');
```

次に、２のConnetionオブジェクトから取得する方法を示す。
Connectionオブジェクトから取得するには、先にDatabaseManagerクラスのインスタンスをサービスコンテナから取得する。（①）次に、connectioin()メソッドでConnetionのインスタンスを取得し（②）、tableメソッドでクエリビルダのインスタンスを得ている（③）。

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

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

// ③　上記インスタンスからクエリビルダを取得する
$query = $connection->table('books');
```
なお、実際のデータ操作でクエリビルダを使う場合は、専用クラスを作成したほうが、拡張性やテスト容易性を保ちやすくなる。
専用クラスを作成する場合は、コンストラクタインジェクションを利用して、クエリビルダ提供元のクラスを外から与えることで実現される。

下記にbooksテーブルのデータ操作を担う専用クラスの例を示す。

```php
<?php
declare(strict_types=1);

namespace App\DataAccess;

use Illuminate\Database\DatabaseManager;

class BookDataAccessObject
{
   protected $db;

   protected $table = 'books';

   public function __construct(DatabaseManager $db)
   {
      $this->db = $db;
   }
   
   public function find($id)
   {
      $query = $this->db->connection()->table($this->table);
      -- [略] -- 
   }
```

##ベーシックなデータ操作
Eloquentもクエリビルダも、フレームワーク内部ではコードからSQLに変換されているが、最適なSQｌ文であるとと限らない。
Laravelには、SQL文をそのまま記述して実行する手段も用意されている。第一引数にはSQL文を指定、第二引数にはプリペアドステートメントを指定する。ｌ

下記にベーシックなSQl実行メソッドを示す。

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

下記にDB::selectを使用したデータ抽出を行う例を示す。

```php
$sql = 'SELECT bookdetails.isbn, books.name'
      . 'FROM books'
      . 'LEFT JOIN bookdetails ON books.id = bookdetails.book_id'
      . 'WHERE bookdetails.price >= ? AND bookdetails.published_date >= ?';

$result = \Illuminate\Support\Facades\DB::select($sql, ['1000', '2011-11-01']);

// 利用方法
foreach ($results as $book) {
   echo $book->isbn;
   echo $book->name;
}
```

さらにDBインスタンスの裏で動作しているPDOオブジェクトを直接利用することも可能である。

```php
$sql = 'SELECT bookdetails.isbn, books.name'
      . 'FROM books'
      . 'LEFT JOIN bookdetails ON books.id = bookdetails.book_id'
      . 'WHERE bookdetails.price >= ? AND bookdetails.published_date >= ?';

$pdo = \Illuminate\Support\Facades\DB::connection()->getPdo();
$statement = $pdo->prepare($sql);
$statement->execute(['1000', '2022-01-01']);
$results = $statement->fetchAll(\PDO::FETCH_ASSOC);

// 利用方法
foreach ($results as $book) {
   echo $book['isbn'];
   echo $book['name'];
}
```
ベーシックによるクエリ実行やPDOオブジェクトを直接利用する方が、処理速度は速くなるが、コーディングの可読性が下がってしまいます。
状況に応じて使い分けるようにしなければならない。