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

概要

ローカルクエリースコープを使うと、特定モデルに必要なクエリービルダーに名前を付けて、1つのメソッドとして呼び出すことができる。

準備

クエリービルダーで使ったOrderCustomerのモデルとテーブルを使う。

それぞれのモデルでリレーションを定義している。

手順

ローカルスコープのクエリービルダーは、次の手順で定義する。

  • 対象のモデルでメソッド定義
    • 'scope'に続いてキャメルケースでメソッド名を定義
    • 第1引数にクエリー($queryなど)
    • ビルダーに引数を渡したいときは第2引数以降に加える
    • メソッド内の戻り値は、第1引数のクエリーに対してビルダーを組んだ結果を渡す
  • 定義したメソッドをビルダーの様に呼び出す
    • 呼び出す際は'scope'を除いたメソッド名で呼び出す

例1:引数なし

たとえばordersの最新3データのみ取り出す定形操作があるとする。このとき、Orderモデルで以下の様にメソッドを定義する。

メソッド名は頭に'scope'を付けてscopeLatest3()として、クエリーを受け取る引数に$queryを設定。

tinkerでこれを利用してみる。呼び出す際は’scope’を付けずにlatest3()とし、$queryに対応する引数は渡す必要はない。

生成されるSQLは以下のとおり。

例2:引数を渡す場合

モデルでのメソッド定義の第2引数以下に、渡したい引数を列挙する。以下の例ではcustomer_idを渡し、これに合致するordersのデータを抽出している。

実行時に渡したい引数は、第2引数custome_idで定義している。

tinkerで引数にcustoer_id=1を渡して実行してみる。

生成されるSQLとバインドされる値は以下のとおり。

例3:子モデルが絡む場合

ビルダーに引数を渡して、子モデルの条件で抽出したい場合。whereHas()を使うが、引数に渡すクロージャ―の変数はローカルスコープのため、useで引数の変数を受け取って渡す。

tinkerでcustomer_name='customer2'を渡して確認。

生成されるSQLとバインドされる値は以下のとおり。

 

Laravel – ANDとORの優先順位

概要

where()orWhere()組み合わせることで、WHERE句のANDORを表現できる。ここではこれらのメソッドの書き方と生成されるSQL、条件の組み合わせの優先順位を整理する。

以下のようなメソッドの書き方が可能になる。

  • 条件1 OR 条件2 AND 条件3ANDが優先される書き方
  • (条件1 OR 条件2) AND 条件3で括弧内のORが優先される書き方

where()orWhere()については、クエリービルダーのメソッドを参照。

準備

クエリービルダーで使った以下の3つのテーブルで各メソッドを確認する。

これをordersに対してcustomersemailsを左結合で関連付けて並べると以下のとおり。

チェーンの展開とANDの優先

where()orWhere()をチェーンで連ねた場合、それぞれの条件が単純にANDあるいはORで連ねられる。その結果ANDが処理された後ORが処理される。

  • ...->where(条件)'... AND 条件'
  • ...->orWhere(条件)'... OR 条件'

以下の例では、where()->orWhere()->where()とメソッドチェーンを組んでいる。

上記のクエリービルダーで生成されるSQLは以下のとおり。

この結果AND演算が優先され、「item='wire'または、item='rubber sheet'かつid=1」という条件で2つのデータが抽出される。

  • 4: 2020-06-07 00:00:00
    • customer name: customer1
    • email address: customer1@mail.com
    • ordered item : rubber sheet
  • 5: 2020-06-08 00:00:00
    • customer name: customer3
    • email address:
    • ordered item : wire

 

クロージャによる()の表現

ANDに対してORの条件を優先させたい場合、SQLでは括弧で括る。クエリービルダーでは、括弧で括りたい処理をクロージャ―でまとめる。

where(function($query) { 括弧でまとめたい処理群; })

以下の例では、上の例と順番は同じだが、最初のwhere()orWhere()をクロージャ―でまとめている。

この結果、まず最初の2項のOR演算が処理され、その結果と残りの条件のAND演算で抽出処理される。生成されるSQLは以下のとおりでOR演算が括弧で括られている。

実行結果は以下のとおりで、「item='wire'またはitem='rubber sheet'」で3つのデータが抽出され、その結果とid=1ANDで結果が一つに絞り込まれる。

  • 4: 2020-06-07 00:00:00
    • customer name: customer1
    • email address: customer1@mail.com
    • ordered item : rubber sheet

 

 

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の場合の内容を定義できる。

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

 

PHP – null合体演算子

null合体演算子(null coalesce operator)は、opr1 ?? opr2の形をとり、以下の様に結果を返す。

  • 左辺opr1の評価結果がnullでない場合は評価結果をそのまま返す
  • 左辺opr1の評価結果がnullの場合は右辺opr2を返す

変数の場合。未定義だとnullで第2オペランドが、定義済みだとその内容が返る。

??単独の演算子ではなく、左辺の評価対象と右辺の戻り値を含めて戻り値を持つ式に相当し、三項演算子と似ている。

 

 

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)と直接オブジェクトで指定しても結果は同じ。

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