【Laravel】 Modelを使用してCRUDや外部結合をしよう

Laravelにおいて、データベースにアクセスする際はModelの活用が必須です。

モデルを使ってデータを取り出したり、変更してみましょう。

Laravelのドキュメントにはモデルではなく、Eloquent ORMに分類されています。

モデルの作成

1つのモデルは1つのテーブルと紐づきます。

まずはartisanでモデルを作成します。

php artisan make:model Note

そうすると、app/Models/内にモデルが作成されます。

モデル名はテーブル名を単数形かつPascalCaseにしたものにするべきです。

最初から入っているuse HasFactory;は、テストデータを入れるためのものです。不要であれば削除して大丈夫です。

テーブルとの関連付け

モデルを作っただけでは関連付けされません。紐付けを行う必要があります。基本的には、テーブル名だけ設定すれば動作します。

class Notes extends Model
{
    /**
     * モデルに関連付けるテーブル名
     * 
     * @var string
     */
    protected $table = 'notes';
}

主キーは、デフォルトで符号なしBigIntegeridが使用されます。id以外やAUTO INCREMENTしないなどの場合、次のように設定します。(incrementingpublicであることに注意)

class Notes extends Model
{
    protected $table = 'notes';

    /**
     * 主キーのカラム名と型の設定
     */
    protected $primaryKey = 'note_id';
    protected $keyType = 'string';

    /**
     * 自動増分するかどうか
     */
    public $incrementing = false;
}

コントローラからモデルを使う

モデルを作ったので、早速コントローラから使用してみましょう。まずはモデルをuseします。


namespace App\Http\Controllers;

use App\Models\Notes;

// ...

Create

useしたら、まずはデータを書き込む処理を作ってみます。

データを書き込む際は、まずはモデルのクラスをnewし、フィールド名をクラスのメンバのように扱います。セットする際は代入するだけでセット可能です。

適当にセットしたら、最後に->save()すると書き込み完了です。

class HelloController extends Controller
{
    public function index ()
    {
        // ...
    }

    public function putNote()
    {
        $note = new Note;
        $note->content = "hogefugahige";
        $note->version = 2;
        $note->save();
    }
}
書き込み結果の例

created_atupdated_atは、デフォルトでは自動的に書き込まれます。

Read

データを読み込む際は、様々な読み込み方が可能です。

全件取得

ModelClass::all()で取得できます。データは配列のように扱うことができます。

ビューに出力する例では、all()で取得したらそのまま@foreachできます。

コントローラ
class HelloController extends Controller
{
    public function index()
    {
        $notes = Note::all();
        return view('index', compact('notes'));
    }
}
ビュー
@foreach($notes as $note)
    <p>{{ $note->content }}</p>
    <p>{{ $note->created_at }}</p>
@endforeach
出力されるHTML
<p>hogefugahige</p>
<p>2022-03-08 14:16:06</p>
<p>foobarbaz</p>
<p>2022-03-08 14:18:32</p>

コントローラでは、配列のようにforeachなどを使用できます。

$notes = Note::all();
$n = count($notes); // 取得した件数

foreach($notes as $note) {
    $aaa = $note->created_at;
    $bbb = $note->content;
}

このように取得したデータは、モデルインスタンスと呼ばれています。

select

特定のカラムだけ取り出す場合、selectを使用します。selectしなかった場合はすべてのカラムを取得します。

$notes = Note::where('created_at', '2022-03-08 14:16:06')
    ->select('content', 'id')
    ->get();

プライマリーキーで取得

whereでも取得できますが、プライマリーキーである場合はfind(id)を利用した書き方が可能です。

// idが1のデータを取得
$note = Note::find(1);

where

where()を使用することで、SQLのwhereと同等の事が可能です。

第1引数にカラム、第2引数に演算子(> >= = <= < <> like等)、第3引数に値を入れます。

// ->get() を忘れないようにする
$notes = Note::where('created_at', '>', '2022-03-08 14:17:00')->get();

$notes = Note::where('content', 'like', '%hoge%')->get();

// % をエスケープする場合は \\% とする
$notes = Note::where('content', 'like', '\\%hoge\\%%')->get(); // %hoge で始まるものを検索

Note::where('created_at', '2022-03-08 14:18:32')のように第2引数を省略 (つまり実引数が2つ) した場合、演算子が=であるものとして処理されます。

一定件数取得

一定件数だけ取得したい場合、first()last()limit(件数)を用います。firstlastで取得した場合、コレクション(配列のようなもの)ではなく普通に単体で取得されます。

// 最初の1件を取得
$notes = Note::all()->sortBy('created_at')->first();

$notes = Note::where('created_at', '>', '2022-03-08')->limit(1)->get();

allではlimitを使うことができません。allではEloquent\Collectionであり、Builderではないためです。

wherelatestなどはクエリを組み立てるためにBuilderが使用されるため、limitを用いることができます。

ソート

ソートする場合はsortBy(カラム名)sortByDesc(カラム名)を使用します。

または、latestoldestを用います。

$notes = Note::all()->sortBy('created_at');

Update

更新したい場合、項目を取得した後、データのCreateのような感覚で値を代入し、save()します。

$note = Note::find(2);
$note->content = "updated hogehoge";
$note->version = 3;
$note->save();

複数更新

当てはまるもの全て更新したい場合、updateを使用します。

updateの引数には連想配列を入れ、キーにはカラム名を、値には更新後の値を入れます。save()の実行は不要です。

Note::where('version', 2)
    ->update(['content' => 'aaabbbccc', 'version' => 100]);

Delete

削除する場合、取得したレコードで->delete()を実行します。

$note = Note::find(3);
$note->delete();

複数件削除

whereで取得したもの全部を削除するなどの場合、同じように最後に->delete()を付けます。

Note::where('version', 100)->delete();

ソフトデリート

もしソフトデリートをしたい場合、モデル側にuse SoftDeletes;を追加します。デフォルトではdeleted_atカラムが使用されます。

use Illuminate\Database\Eloquent\Model;

// useの追加を忘れないようにする
use Illuminate\Database\Eloquent\SoftDeletes;

class Note extends Model
{
    // ソフトデリートを行う
    use SoftDeletes;
    
    // ...
}

データの削除方法は、ハードデリートと同様に->delete()するだけです。

ソフトデリートしたデータの復元

取得したデータに->restore()を使用します。しかし、ソフトデリートされたデータはそのままではデータの取得対象とならないため、withTrashed()をつけて取得します。

Note::withTrashed()->where('version', 100)->restore();

ソフトデリートしたデータをハードデリート

ソフトデリートを使用している場合にレコード自体を削除したい場合、ソフトデリートしたデータに対してforceDelete()を実行します。

Note::withTrashed()->where('version', 100)->forceDelete();

データの複製

取得したモデルインスタンスにreplicate()します。複製したデータで適当に書き換える等したら、最後にsave()します。

$note = Note::find(2);

// replicateでデータ複製 (idはデフォルトでは自動で更新される)
$newNote = $note->replicate();

// 適当にデータ書き換え
$newNote->version = 100;
$newNote->content = 'hogefuga foober';

// 保存
$newNote->save();

fillでデータ設定

1つ1つのカラムで代入を書くのが面倒という場合、fillを用いて一括で代入することができます。fillには連想配列で入れ、キーには更新したいカラム名を、値はその値を入れます。

fillを用いる場合、対象となるカラムをホワイトリスト形式でモデルに書いておく必要があります。

class Note extends Model
{
    protected $table = 'notes';

    /**
     * fillを用いて代入するリストをホワイトリスト形式で書く
     */
    protected $fillable = ['content', 'version'];
}
$note = new Note;

$note->fill([
    'version' => 300,
    'content' => 'abcde xyz',
])->save();

モデルに処理を書く

例えば、特定の条件を満たすデータを取得するという場合、毎回コントローラやサービス層に書くのでしょうか。データの更新命令や取得の色々をコントローラが行うのは、コントローラにデータベースのデータに関する責任をもたせていることになります。

データの取扱に関する様々なルールは、Modelに書くべきです。ただ、取得したデータをアプリに合わせて色々加工したりするのは、サービス層がするべきです。Modelには、システムに依存しない汎用的な処理を書きます。例えば、特定の引数だけ取ってそのままデータを更新する、外部結合してソートして取得する、などです。

モデルのインスタンス (要はNote::find(1)などで取得したもの) は、そのままそのモデルのクラスのインスタンスです。そのため、モデルのクラス内で$this->id$this->created_atのようにカラムを指定してデータを取得できます。

class Note extends Model
{
    // ...

    /**
     * 最も新しいnoteを取得
     */
   static function getLatestNote()
   {
       return Note::latest()->first();
   }

   /**
    * Noteの本文を更新
    */
   function updateNoteContent($content)
   {
       $this->content = $content;
       $this->save();
   }
}
class HelloController extends Controller
{
    public function index()
    {
        // staticなものは Class::Method で呼び出せる
        $note = Note::getLatestNote();

        return view('index', compact('note'));
    }

    public function updateNote()
    {
        $note = Note::find(6);

        // staticでないものは、モデルインスタンスから呼び出せる
        $note->updateNoteContent('foobar abcde');
    }
}

結合

外部結合等をしてデータを取得したい場合はよくあります。その場合、hasManybelongsToを主に用います。

hasManyは親から子のレコードに対して、belongsToは子から親のレコードを取得します。

例えば次のようなテーブル構造だったとします。

relation用サンプルER図

この場合、noteから所属するgroupを取得したい場合、$note->belongsTo(Group::class, 'foreign_key', 'owner_key')->get()のようにします。

第2引数を省略した場合、外部キーのカラム名はメソッド名から推測されます。例えば、メソッド名がgroupなら、外部キーのカラム名がgroup_idであるものとして処理されます。

第3引数を省略した場合、カラム名はidであると処理されます。

そのため、メソッド名はgetGroupではなく、groupのようにするべきです。

use App\Models\Group;

class Note extends Model
{
    protected $table = 'notes';

    static function getNote($noteId)
    {
        return Note::find($noteId);
    }

    /**
     * そのNoteが属するGroupを取得
     */
    function group()
    {
        // noteの`group_id`を使って、groupsテーブルの対応するレコードを取得
        return $this->belongsTo(Group::class);

        // belongsTo(Group::class, 'group_id'、'id') のように外部キーのカラム名を明示できる
    }
}
class Group extends Model
{
    protected $table = 'groups';
}
$note = Note::getNote(1);

// $note->belongsTo(Group::class, 'group_id')->first() でも取得できる
$group = $note->group()->first();

逆に、groupが持っているnoteを取得したい場合、hasManyhasOneを用います。複数件取得するならhasManyです。第2引数には対象テーブルの外部キーのカラム名を、第3引数には自身のテーブルの外部キーで参照されるカラム名を入れます。

第2引数を省略した場合、自身のテーブル名単数形+_idであると処理されます。下の例ではgroupsテーブルなので、外部キーのカラム名はgroup_idであると推論されます。

第3引数を省略した場合、カラム名はidになります。

use App\Models\Note;

class Group extends Model
{
    protected $table = 'groups';

    /**
     * このGroupに属する全てのNoteを取得
     */
    function notes() {
        return $this->hasMany(Note::class);

        // hasMany(Note::class, 'group_id', 'id') のようにキーを明示しても良い
    }
}
$group = Group::find(2);
$notes = $group->notes()->get();

タイムスタンプの有効無効

デフォルトでは、created_atupdated_atを用いた自動的なタイムスタンプの記録が有効化されています。

もし使用しない場合は、

class Notes extends Model
{
    public $timestamps = false;
}

のようにします。

別のコネクションを用いる場合

app/config/database.phpには、データベースのコネクション情報が入っています。特に指定しない限り'default' => env('DB_CONNECTION', 'mysql'),のところに書かれている設定が使用されますが、モデル単位で別のコネクションを使用したい場合、モデルに$connectionを追加します。

class Note extends Model
{
    // app/config/database.php にあるコネクション名を指定
    protected $connection = 'sqlite';
}

まとめ

LaravelのModelはデータベースのテーブルと直結しています。直接SQLを書くことはまず無く、モデルを利用して抽象化された書き方で書き込みます。

特に外部結合を簡単に書けるのは大きなメリットです。他にも多対多の関係も処理できたりしますが、まずはhasManybelongsToの親子の向きを覚えて使えるようになりましょう。

モデルは単にORMを使用してSQLを書かずに済ませるものではありません。MVCやその他アーキテクチャにおけるモデルの役割を知って使うと、良い設計を行うことができます。

モデル操作ではトランザクションが必要不可欠なので、そちらの方法も確認しておくことを推奨します。

laravel mode thumb

役に立ったらシェアしよう!