Laravel – リレーション/クエリー

 

リレーション

  • 複数テーブルで構成されるデータを、モデルベースで扱う方法
  • モデルでhasOne()hasMany()belongsTo()メソッドによってリレーションを定義
  • 親モデルに属する子モデルを、親モデルの属性のように扱えるようになる

クエリービルダー
クエリービルダーのメソッド

  • SQLによりフレームワークではなくRDBMS側でテーブルの連結や絞り込みなどの操作を行う
  • クエリービルダーメソッドを組み合わせてSQLを生成する

ANDとORの優先順位

  • WHERE条件でANDORを組み合わせる場合の書き方
  • where()orWhere()の優先順位とクロージャ―による括弧の実装

クエリービルダーとリレーション

  • クエリービルダーの効率性と、リレーションによる直感的な操作を組み合わせる手順

子モデルの属性による絞り込み

  • 子モデルの属性の条件による絞り込みを、クエリービルダーで行う方法
  • whereHas()メソッドを使う

ローカルスコープクエリー

  • 特定モデルで利用するクエリービルダーを、モデルのメソッドとして定義する

 

Laravel – 子テーブルの条件で絞り込み

概要

複数テーブルが関連付けられているデータを子テーブルの条件で絞り込みたい場合。クエリービルダーJOIN句によってテーブルを結合すると結果は配列となるが、配列ではなくオブジェクトのままリレーションで操作したいとき。

whereHas()メソッドを使うと、親のモデルでhasMany()で定義された子モデルの操作が可能になる。

whereHas()の使い方は以下のとおり。

  • 第1引数で子モデル名を指定
  • 第2引数にクエリーを引数としたコールバックを定義
    • コールバックで引数のクエリーに対してクエリービルダーメソッドを適用
  • 最後のget()を忘れがちなので注意

準備

クエリービルダーの確認で使ったテーブルで確認する。

1つ先のリレーション

親が直接持つ子モデルの場合、以下の様に記述する。ここでは子モデルのcustomerの名前が'customer2'のデータを抽出している。

抽出結果は親モデルのコレクションとしてビューに渡している。

モデルのコレクションを受け取ったビューでは、親モデルとしてこれらを扱い、必要に応じてリレーションによって子モデルの属性を取り出す。

emailを持たないcustomerに対応するため、optional()メソッドを使っている

結果は以下のとおりで、customer.id=2に相当するcustomer2のデータが抽出されている。

  1. 2020-06-05 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: rubber sheet
  2. 2020-06-06 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: plastic plate

 

whereHas()で生成されるSQLを確認しておく。where句の対象としてサブクエリーが構成されている(見やすいように結果を改行している)。

バインドされる値も確認。

2つ先のリレーション

子モデルの子モデルの属性で絞り込む場合は、whereHas()のコールバック内でwhereHas()を入れ子で呼び出す。呼び出し部分だけを取り出すと以下のとおり。

コールバックの引数は、コールバック内ローカルスコープなので同じ名前でも構わない。

結果は以下のとおりで、メールアドレスが'customer2'で始まるcustomer2のみ取り出されている。

  1. 2020-06-05 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: rubber sheet
  2. 2020-06-06 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: plastic plate

 

生成されるSQLを確認。サブクエリーが入れ子で構成されている。

バインドされる値も確認。

 

Laravel – クエリービルダーとリレーション

概要

Laravelでデータベースの大量のデータを扱う場合、クエリービルダーでSQLを発行してDBMS側でデータを絞り込んだ方がフレームワークの負担が少なくなる。

一方でリレーションに基づいた記述は、可読性やメンテナンスの面で有利な面を持つ。

そこで、クエリービルダーとリレーションの組み合わせについて試してみた。

  • モデルクラスのall()メソッドとwhere()のチェーンによる絞り込みは、DBMSの機能を使っておらず非効率ではないか
  • モデルクラスで記述するクエリービルダーは、DBMS側でデータを絞り込んだ結果をフレームワークで扱うので効率的なようだ
  • DBファサードで記述するクエリービルダーは、得られるデータがモデルのインスタンスではなく連想配列になるため、モデルで定義したリレーションが使えない

準備

クエリービルダーで使ったものと同じデータを準備する。

all()とwhere()の組み合わせ

以下の様なコントローラーのアクションでデータを絞り込んでビューに渡す。

ビュー側では$ordersを受け取って、リレーションに基づいて表示する。

ブラウザーでの表示は以下のとおり。

  1. 2020-06-05 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: rubber sheet
  2. 2020-06-06 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: plastic plate

 

ただしこの場合の動作は、DBMSから全データをall()で取得した後にcustomer_idで絞り込んでいる。以下の様にtinkerで確認してみると、where句を含むSQLは発行されていないようだ。

実際、get()なしで直接モデルのインスタンスが得られている。tinkerで確認してみると、$ordersOrderクラスのインスタンスを要素とするコレクションとなっている。

モデルのメソッドによるクエリービルダー

コントローラーのアクションで、以下の様にOrderモデルからクエリービルダーを構成する。

ビューは上と同じ内容で、結果も同じように表示される。

tinkerで確認するとSQLが構成されて値もバインドされているので、DBMS側でデータが絞り込まれているようだ。

get()の結果得られるデータは上記のall()where()と同じで、Orderクラスのインスタンスを要素とするコレクション。

DBファサードによるクエリービルダー

クエリビルダ―をモデルクラスからではなくDBファサードから記述した場合。

同じビューを使った場合、以下のようなエラーがブラウザーに表示される。

tinkerで確認すると、SQLは同じように発行されバインディングも同じ。

ただし、get()で得られるデータが上と異なる。全体は配列だが、要素がOrderクラスのインスタンスではなく、Orderの属性をキーとする連想配列となっている。

このため、$ordersの要素からリレーションを定義したはずのcustomerを呼び出そうとしても、「定義されていない属性」としてエラーが発生する。

そこでビュー側でリレーションを用いず各要素を連想配列として取り出してみる。

こうすると以下の様に$ordersの抽出結果が表示される。

  1. id: 2
  2. customer_id: 2
  3. ordered_at: 2020-06-05 00:00:00
  4. item: rubber sheet
  1. id: 3
  2. customer_id: 2
  3. ordered_at: 2020-06-06 00:00:00
  4. item: plastic plate

 

ただし各要素のorderに関係づけられたcustomerはこのままでは得られないため、クエリーレベルで2次元配列として関連する全モデルの属性をカラムとして取り出す必要がありそうだ。

 

Laravel – クエリービルダー

準備

以下の3つのテーブルを使ってクエリービルダーの動作を確認する。

3つのテーブルを結合させるSQLの例。

SQLの実行結果。

基本操作

tinkerで確認する。

ビルダーの書き方

DBファサードのtableメソッド

DBファサードのtable()メソッドを使う場合。

モデルのビルダーメソッド

モデルのビルダーメソッドを直接実行する場合。

ビルダーのインスタンスを保存して使う

DBファサードのtable()メソッドの場合、tinkerだと以下のように詳細な内容が表示される。toSql()メソッドで構築されるSQLが確認できる。

モデルからビルダーメソッドを直接呼び出す場合、tinkerでのインスタンスは表示はシンプルになる。

SQLや実行結果などの取得・確認方法

ビルダーのインスタンス取得

クエリービルダーを実行すると、ビルダーのインスタンスが得られる。

get()~実行結果の取得

クエリービルダーの実行結果はget()メソッドで得る。

toSql()~SQLの確認

toSql()メソッドでビルダーで生成されるSQLを確認できる。

getBindings()~バインドされる値の確認

バインドされる値はgetBindings()メソッドで確認できる。

クエリービルダーメソッドのチェーン

複数のコマンドを含むSQLの場合は、コマンドに対応するクエリービルダーメソッドをチェーンで繋げる。

メソッドチェーンの順番は問わない。以下のようにメソッドの順番を変えても、生成されるSQLは同じ。

コントローラーでの記述

基本の書き方

以下のルーティングで基本の書き方を確認。

コントローラーでの書き方。DBファサードを使う例。

  • customersテーブルのnameカラムの値を取得している
  • 結果は配列customersで各要素は2つの要素idnameを持つ連想配列となる
  • $customersをビューに渡している

ビューでは受け取った$customersの各要素を取り出し、idとnameを表示。

なおdd()で表示させた$customersの内容は以下のとおり。

メソッドチェーン

SQLの行が増えてきた場合、メソッドチェーンでビルダーメソッドを繋げる。

まず以下のようなルーティングを定義。

コントローラーのアクション内でメソッドチェーンを記述。以下の例では、冒頭のLEFT JOINを使ったSQLを意図している。

なお本筋ではないが、select()メソッドの要素内でASによるエイリアスを定義できる。

メソッドチェーンでSQLをビルドした場合、結果は指定したカラムを持つ複数レコードが2次元配列で返される。

以下はビューで受け取った$ordersを表示させる例。foreachで1レコードに相当する連想配列を取り出し、ネストしたforeachで各レコードの属性・内容の対を取り出して表示させている。

ブラウザーには以下の様に表示される。

  1. ordered_at: 2020-06-05 00:00:00
  2. customer_name: customer1
  3. address: customer1@mail.com
  4. item: screw
  1. ordered_at: 2020-06-05 00:00:00
  2. customer_name: customer2
  3. address: customer2@mail.com
  4. item: rubber sheet
  1. ordered_at: 2020-06-06 00:00:00
  2. customer_name: customer2
  3. address: customer2@mail.com
  4. item: plastic plate
  1. ordered_at: 2020-06-07 00:00:00
  2. customer_name: customer1
  3. address: customer1@mail.com
  4. item: rubber sheet
  1. ordered_at: 2020-06-08 00:00:00
  2. customer_name: customer3
  3. address:
  4. item: wire

モデルのリレーション設定に基づく場合

クエリービルダーのメソッドチェーンへの対比として、同じデータをモデル間のリレーション定義に基づいて操作する例を示す。

CustomerEmailOrderの各モデルにリレーションに関する記述を追加する。

コントローラーではOrderモデルの全データを取り込み、ビューに渡す。

ビューでは受け取った$ordersから要素を順に取り出して、関連付けられたデータを表示させる。関連付けられたデータがない場合のエラーを防ぐため、optional()ヘルパーを使っている

ブラウザーには以下の様に出力される。emailが存在しない場合は無表示となっている。

  1. 2020-06-05 00:00:00: customer1
    • e-mail: customer1@mail.com
    • item: screw
  2. 2020-06-05 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: rubber sheet
  3. 2020-06-06 00:00:00: customer2
    • e-mail: customer2@mail.com
    • item: plastic plate
  4. 2020-06-07 00:00:00: customer1
    • e-mail: customer1@mail.com
    • item: rubber sheet
  5. 2020-06-08 00:00:00: customer3
    • e-mail:
    • item: wire

なおdd()で表示させた$ordersの内容は以下のとおり。1つ目の要素のみ展開している。

クエリービルダーとリレーション

クエリービルダーでSQLを発行すると、フレームワークに負担をかけずにDBMS側でデータを絞り込めるが、その方法によっては意図した動作にならなかったり、リレーションを使えない場合がある。

結論としてはモデルクラスでクエリービルダーを記述するのがよさそうだが、その詳細についてはこちらにまとめた。

クエリービルダーでのエイリアス

クエリービルダーでSQLと同様にエイリアスを定義できる。クエリービルダーでのエイリアスを参照。

 

Laravel – Trying to get property ‘…’ of non-object

Trying to get property ‘address’ of non-object

Laravelで複数のモデル/テーブルを関連付けて表示させようとしたところ、「オブジェクトでもないものから属性を得ようとした」というエラーが表示された。

エラー発生の流れ

以下のような構造の顧客とメールアドレスのテーブルを(マイグレーションで)準備して、

以下の様にデータを準備して、

モデルでリレーションを定義して、

コントローラーでCustomerの全データをビューに渡して、

ビューで各customersに関連付けられたemailsaddressを読もうとすると、

ブラウザーにエラーが表示された。

原因

以下の様に、customer3に対してはemailsのレコードがなく、戻り値がnullとなるので、そこからaddressを得ることができないため。

対処法

@issetディレクティブ

@issetで参照結果がnullかどうかを判定。

emailsのレコードを持たないcusutomersのレコードは、@issetによってnullが無視されるため表示されないが、emailに関する2行目だけを@issetで囲めばnameだけは表示される。

連想配列の要素として指定

オブジェクトの属性ではなく、連想配列の要素として読み出す。

参照しようとする配列がnullの場合、要素指定して読みだした結果はnullとなる。customersのレコードは全て表示され、emailsのレコードがnullの場合はemailsの情報だけ表示されない。

optional()ヘルパー

optional()の引数の内容がnullの場合、その属性参照はエラーとならずに属性の参照結果がnullとなる。

ブラウザーの表示結果は上と同じ。

なお、optional()は1段先の参照結果までは保証できるが、3段目以降には作用しない。必要ならoptional()を入れ子にしなければならない。

null合体演算子との組み合わせ

PHPのnull合体演算子を使って、結果がnullの場合の内容を定義できる。

以下の様に、ブラウザー上で適切な表現が可能になる。

 

Laravel – マイグレーション – カラムの追加

概要

マイグレーションによって生成されたテーブルに、新たにカラムを追加する。

テーブルを生成するマイグレーションファイルの前までロールバックしてファイル修正、再マイグレートという方法もあるが、ここでは新たにマイグレーションファイルを作成して追加カラムのみ処理する方法を整理する。

追加前

以下のマイグレーションファイルを準備する。

マイグレーション実行後に生成されたテーブルの構造は以下のとおり。

追加マイグレーションファイル

以下のコマンドでカラムを追加するためのマイグレーションファイルを新たに作成。

以下の様にファイルを編集。

  • up()メソッドで追加するカラムを定義
  • down()メソッドで追加したカラムを削除する処理を記述

追加マイグレーションの確認

新たに作成したマイグレーションファイルに基づいてマイグレーションを実行。

以下の様に、2つのカラムが追加されている。

ロールバックの確認

カラムを追加したマイグレーションの1ステップのみロールバック。

down()メソッドにより、追加されたカラムが削除されている。

 

Laravel – バリデーション – uniqueの除外

概要

たとえば登録済みユーザーの情報(ユーザー名、メールアドレスなど)を編集・更新することを考える。

メールアドレスをログインIDとしている場合、登録時にはアドレスにuniqueのバリデーションルールを適用している

ユーザー情報を編集する場合もメールアドレスには同様の制約をかけるが、アドレスを変更せずに他の項目を変更しようとしたとき、「既にデータベース上にアドレスが存在している」のでuniqueに対するバリデーションエラーとなる。

ここでは、その回避方法を整理する。

フォームリクエストの準備

作成

編集用のビューは別に作成済みで、ルーティングも設定されているとして、以下の様にフォームリクエストを作成したとする。

編集

フォームリクエストの実装の際、単にunique制約とする場合は以下の様になる。

バリデーションの適用

アクションの引数において、メソッドインジェクションでフォームリクエストを指定。

ここでユーザー名を空白、メールアドレスは登録済みの元の値でバリデーションが行われると、エラーは以下の様になる。

uniqueルールの除外

uniqueのルールを課しながら、特定のデータについてこれを除外するために、以下の構文が使える。

Rule::unique('テーブル名')->ignore($this->モデル->id)

以下、バリエーション。

  • Request::unique('users')だけだと、'unique:users'と同じ
  • これにメソッドチェーンでignore($this->user->id)を付けると、ルートパラメーターの値をとってくる。
  • ignore($this->user)と直接オブジェクトで指定しても結果は同じ。

このようにすることで、自身のメールアドレスに関しては同じ内容が許容される。

 

Laravel – ルートパラメーター~インスタンスを渡す

概要

ルートパラメーターは、idなどの値を指定するほか、モデルのインスタンスを直接渡してコントローラーで受け取ることができる。

値を渡して値を受け取る

ルーティング

以下の様にルーティングを設定したとする。

ルーティングは以下のようになる。

ビューでの渡し方

ビューからのルーティングでidなどの値を渡すなら、以下のようにURLで展開させるかroute()ヘルパーの第2引数で指定する。

いずれの場合も、以下のようなURLでGETリクエストされる。

ログイン済みユーザーのidを渡す場合、以下のように書ける。

コントローラーでの受け取り方

コントローラー側でパラメーターを受け取る場合、アクションメソッドの引数にパラメーターが渡されるので、それをそのまま利用すればよい。

モデルのインスタンスのまま渡して受け取る

ルーティング

以下の様にルーティングを設定する。

リソースルーティングで指定するなら以下のとおり。

何れの場合もルーティングは以下のようになる。

ビューでの渡し方

ビューからからのルーティングでモデルのインスタンスを渡す場合も、URLで展開させるかroute()ヘルパーの第2引数で指定する。ここでは認証済みのユーザーインスタンスを渡している。

インスタンスを渡しても、ルートパラメーターはidの値になる。

コントローラーでの受け取り方

コントローラーのアクションでは、引数にモデルクラスのメソッドインジェクションを適用して、idに対応したインスタンスを受け取る。

コントローラーの冒頭でモデルをuse指定してもよい。

 

Laravel – アップロード画像の変更・削除

概要

公開領域のディスクに保存された画像ファイルを変更・削除する手順を整理する。

  1. アップロード画像処理~準備
  2. 画像ファイルの入力
  3. アップロード画像ファイルの保存・登録
  4. アップロード画像の表示
  5. アップロード画像の変更・削除

変更は現在のファイルを削除して新しいファイルを保存するので、画像ファイルの削除手順がキーになる。

一般的なアップロードファイルの削除はこちらを参照。

ファイルの削除手順

storageディレクトリーのディスクに保存された画像ファイルの削除は、以下で行う。

ここで'ディスク''ファイルパス'は、保存・登録store()メソッドの引数に指定したものと同じ。

データの変更

例題のアプリケーションを、画像ファイルのほか、商品名や価格も併せて変更できるように修正する。

ルーティング

  • 画像データの編集は、GETメソッドでeditアクションにルーティングされる。
  • 編集されたデータの更新登録は、PATCHメソッドでupdateアクションにルーティングされる。

ビューからのリンク~index.blade.php

indexページで一覧された各商品に、編集のリンクを配置する。要点としては、

  • a要素のhrefの遷移先でroute('items.edit', $item)をしている
  • 第1引数はeditのルート名、第2引数は編集対象のデータインスタンス
  • GETのURLは以下の様に展開される
    • アプリケーションルート/{$id}/edit

コントローラー~edit

GETメソッドでルーティングされたeditアクションでは、URLに含まれる$idに対応するインスタンス$itemがデータベースから引数として渡され、このインスタンスをそのままeditビューに渡している。

ビュー~edit.blade.php

editビューでは渡された$itemインスタンスの内容を各input要素に表示し、画像ファイルのパスから画像を表示している。

  • 画像を扱うため、formenctypeオプションを指定している
  • form要素で指定できるメソッドはGETPOSTだけだが、updateルートはPATCHメソッドを期待しているので、@methodディレクティブで'patch'を指定している
  • 新規入力時と同じく、フォームリクエストで編集時の入力バリデーションを行っている

コントローラー~update

PATCHメソッドでルーティングされたupdateアクションでは、編集入力内容によって、商品データの内容と画像ファイルを更新している。

  • 編集時に画像が選択されていれば、現在のファイルを削除して、選択されたファイルを保存
    • このときにファイルのパスも取得
  • 商品内容、画像ファイルへのパスでデータの内容を更新

データの削除

ルーティング

indexページで一覧表示された商品の削除ボタンを押すと、DELETEメソッドでdestroyにルーティングされる。

ビューへの削除ボタン配置~index.blade.php

indexページの各商品表示にフォームと削除ボタンを加える。

  • form要素ではmethod="post"を指定し、@methodディレクティブで'delete'メソッドを指定している
  • 削除する商品を指定してdestroyにルーティングするだけなので、フォームにはsubmitボタンだけが配置されている

コントローラー~destroy

destroyアクションでは、ファイルが登録されていればpublicディスクから削除し、商品データをデータベースから削除している。

 

Laravel – アップロード画像の読み込み・表示

概要

データベースに登録されたパスから画像ファイルを取得し、ビューで表示する手順を整理する。ここでは、indexページで画像のほかに商品名や商品価格も表示させる。

  1. アップロード画像処理~準備
  2. 画像ファイルの入力
  3. アップロード画像ファイルの保存・登録
  4. アップロード画像の表示
  5. アップロード画像の変更・削除

表示された画像ファイルの変更・削除についてはこちらを参照。

publicディスクへのリンク作成

アップロード画像を保存したpublicディスクはstrage/app/publicに割り当てられているが、このディレクトリーはWebサーバー上は公開されていない。

そこで、publicディスクをアプリケーション下のpublicディレクトリーで公開するため、以下のコマンドを実行しておく。

これにより、publicディレクトリーにシンボリックリンクstorageが作成されて、publicディスクに割り当てられたディレクトリーに公開領域からアクセスが可能になる。

コントローラー~index

indexアクションでitemsテーブルの全データを取得し、これをビューに渡して表示させる。

ビュー~index.blade.php

コントローラーから$itemsを受け取り、@foreachディレクティブによって全データの内容と画像データを表示する。

ここでは\Storageファサードのurl()メソッドを使って画像ファイルのパスを得ていて、これによってpublicディレクトリー下のstorage/app/publicディレクトリー下にある画像ファイルにアクセスしている。

公開領域のファイルのURLについてはこちらを参照。

なお、商品画像が登録されていない場合は、プレースホルダー画像を表示させている。