Eloquent ORMについてなにも知らなかった(1) - 基本的な使い方編 -

先週に書いた記事で、Eloquent ORM(以下Eloquent)をライブラリとして導入する方法を書きました。

blog.zuckey17.org

この記事の中ではじめ、次のようなコードを書いてbooksテーブルについてBookクラスを作り全件を取得して表示しました。

<?php
// ダメなコードです

class Book extends Model {
    protected $table = 'books';

    protected $fillable = [
        'id',
        'isbn',
        'name',
        'author',
        'created',
        'updated',
    ];
}

===

$books = Book::all()->toArray();
foreach($books as $book) {
    echo $book['name'];
}

このコードは動いてはいましたが、2点の指摘をいただきました。

  1. toArrayするのはやめましょう
  2. table、fillableフィールドの指定はいりません。

非常にはずかしいことに、普段Laravelを書いて、Eloquentを使っているというのに知らないことがありすぎたので、本エントリではこれらの指摘とその他に調べた基本的だと思われることをまとめておきます。

以下で記載しているコードの省略なしは、

php-sandbox/basic-eloquent-usage at master · zuckeyM-17/php-sandbox · GitHub

にありますので、確認に使ってみてください。

指摘について

1. toArrayするのはやめましょう

eloquentのcollectionを利用するように変更 · zuckeyM-17/php-sandbox@029ad50 · GitHub

全件取得 ->表示するのは、以下のようにして書けます。

<?php

$books = Book::all();
foreach($books as $book) {
    echo $book->name;
}

Eloquentは、allgetのメソッドを利用するとCollectionクラスという便利なリストのインスタンスを戻してくれます。
例のようにforeachで回すと、単一のModelが取得できますし、Modelであれば、->でフィールドにアクセスできます。
toArray()を使うとそれらの恩恵に預かることができないので、ORMを使う旨味が激減します。

また、取得には、他にfindfirstlastなどのメソッドがあります。
それぞれプライマリキーで検索やlimitを書けているのですが、これらは単一のModelを取得することに利用します。

※ findの引数が配列になっている場合は、Collectionが戻ります。

2. table、fillableフィールドの指定はいりません。

不要なfillableとtableの記述を削除 · zuckeyM-17/php-sandbox@1e42688 · GitHub

tableフィールド

EloquentはActiveRecordパターンの実装*1 なので、1つのModelインスタンスは1テーブルの1レコードと完全に一致します。
そのため、テーブルの名前が複数形のスネークケースという慣例に則っていれば、EloquentはbooksBookモデルに紐付けるといったように、テーブルの指定を推測してくれます。

この他にも、connectionというフィールドも良く明示的に書いていましたが、DBが複数あるなどの特殊な場合を覗いてはDefaultの設定を見るため、指定する必要はありません。

fillableフィールド

fillableフィールドは悪意のあるユーザーがアプリが意図しない変更をDBに与えないように変更可能かどうかをカラム名ホワイトリストで縛るものです。

以下のようにfillableフィールドに指定した名前のカラムには、fill値を指定してレコードを作成することが可能です。
もし、指定されていたとしても、その値は無視されます。

<?php

class Book extends Model {
    protected $fillable = [
        'isbn',
        'name',
        'author',
    ];
}

===

$newBook = new Book();
$newBook->fill([
    'id' => 111, // 無視される
    'isbn' => '978-4-04-429201-0',
    'name' => '涼宮ハルヒの憂鬱',
    'author' => '谷川 流',
]);
$newBook->save();

同様の役割にguardedというフィールドが存在します。
こちらは、fillableとは逆でブラックリストです。変更されたくない値を明示的に指定することができます。

<?php

protected $guarded = [
    'created',  // createdをfillすることができない
];

その他の基本事項

ここから先は、指摘を受けてからその他に調べて、使ってはいたもののしっかりと理解していなかったり、知らなかったものなどについて紹介します。

ChunkとCursor

公式ドキュメントの以下の項目に記載のあったchunkメソッドとcursorメソッドですが、どちらも大量のデータを取得する際にメモリ使用量を抑える際に使うメソッドのようです。
https://laravel.com/docs/5.5/eloquent#chunking-results

ざっくりとした理解だと、

  • chunk => 複数回に分けてqueryを発行することにより、メモリ使用量を少なくする
  • cursor => カーソルによって1クエリで1行ずつデータを舐めることによりメモリ使用量を少なくする

こちらについては、僕自信の理解が曖昧なため、もう少し調べる必要があると思います。
詳しく理解されている方がいらしたら、コメントで参考サイトなど教えていただきたいです。

Chunk

<?php

Book::chunk(2, function ($books) { // 2冊ずつSQLを回す
    foreach ($books as $book) {
        echo $book->name , "\n";
    }
});

Cursor

<?php

foreach (Book::cursor() as $book) { // カーソルで1行ずつ処理をする
    echo $book->name , "\n";
}

上記どちらも、結果として全件の本のタイトルが表示されます。

Query Scope

クエリスコープはallgetメソッドを発行する際に、結果を絞り込むのに便利な機能です。

Global Scope

Global Scopeはその定義をそれぞれのModelのbootメソッド内にてaddGlobalScopeにて宣言すれば、すべてのクエリにて絞込みが利用できます。

<?php

use Illuminate\Database\Eloquent\Scope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;

class BookScope implements Scope
{
    public function apply(Builder $builder, Model $model)
    {
        $builder->where('id', '>', 2);
    }
}

===

class Book extends Model {
    ~~~
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope(new BookScope);
    }
    ~~~
}

===
$books = Book::all(); // idが2より大きい`Book`のCollectionを取得

また、addGlobalScopeで設定していても、

<?php

$books = Book::withoutGlobalScope(BookScope::class)->get();

とすると、絞込みを回避することもできます。

Anonymous Global Scope

こちらは、Scopeクラスを作らなくても、以下のようにbootメソッド内で絞込み条件を付け足すこともできます。

<?php

class Book extends Model {
    ~~~
    protected static function boot()
    {
        parent::boot();

        static::addGlobalScope('id', function (Builder $builder) {
            $builder->where('id', '>', 2);
        });
    }
    ~~~
}

Local Scope

Local Scopeは各Modelに定義します。 scopeというプレフィックスをつけてpublicなメソッドとして宣言し、クエリを引数に取って、決まった絞込みを利用できます。

<?php

class Book extends Model {
    ~~~
    public function scopeRecent($query) // scopeはプレフィックス、recent()というメソッドがqueryBuilderで使えるようになる
    {
        return $query->where('id', '>', 3);
    }
    ~~~
}

===
$books = Book::recent()->get(); // idが3より大きい`Book`のCollectionを取得

Dynamic Scope

Dynamic ScopeについてもLocal Scope同様各Modelに定義します。 Local Scopeの定義に引数を増やして、絞り込む値を利用時に入れることができます。

まとめ

  • 前回のブログについて指摘していただいた実装を見直し、調査しました
    • ブログを書くことで改善できてよかったです!
  • その他にもEloquentの知らなかった基本的な機能がたくさんあったので、まとめて、自分で書いてみました
    • まだ基本の「き」なのでこれからも少しずつ調べていこうと思います

次回はCollectionの様々な使い方、Eventあたりをやりたいです。