You are here
Home > Posts tagged "Eloquent"

ダーティなレコード

フォームの画面でのユーザーのインプットをもとに既存のDBレコードを更新する際、もし何もデータ更新なしに「保存」ボタンを押されたらDBレコードを更新する?

もちろん、Javascriptで「保存」ボタンを無効にすることも可能。少なくともブラウザレベルでは。

しかしサーバーレベルではどう判断?

現在のデータとインプットデータを項目ごとに1つずつ比較?

そんな面倒なことを裏で自動的にEloquentは行ってくれます。

例えば、

class Product extends Model {
  protected $table = 'product';
  protected $primary_key = 'product_id';
  protected $timestamps = true;
}

use App\Product;

$product = Product::find(1); //product_id = 1 のレコードをゲット
$product->price = 2000; // まったく以前と同じ値段に
$product->update();

update()をコールしているのだけれど、何もデータに変更ないので、ここ裏ではDBの更新はありません(updated_atのタイムスタンプをチェックしてみてください)。賢いですね!

しかし、ここに例えば、この更新をもとに他のDBのレコードを更新することがプログラムされていたら?

$product->update();
update_other_table();

必要もないのにそちらも更新されては困ります。

更新されたかどうかを判断するには、

if($product->isDirty())
{
  $product->update();
  update_other_table();
}

と簡単にチェックできます。ここisDirty()はDBにレコードが保存される前にチェックする必要あります。つまり、update()の前に。

さらに、1つの項目だけが変更されたかどうかをチェックするには、

if($product->isDirty('price'))
{
  update_other_table();
}

素晴らしいですね。

ちなみに、この「ダーティ」。ダーティハリー、ダーティダンシングのムービーの「ダーティ」と同じです。

http://dictionary.goo.ne.jp/leaf/jn2/132268/m0u/

汚いの意味。

でもここでの意味は、「データが更新された」の意味。

マスアサインメントで一括取り込み

ララベルのマニュアルで紹介されている一般的なDBレコードの作成方法は、

class Member extends Model
{
  protected $table = 'member';
  protected $primary_key = 'member_id';
  protected $timestamps = true;
}

use App\Models\Member;

$member = new Member();
$member->name = '山田太郎';
$member->email = 'tyamada@gmail.com';
$member->password = 'password'; // ここもちろん実際は暗号化して
$member->save();

Memberのインスタンスを作成し、ちまちまとそれぞれの項目を埋めて、最後にセーブ。

しかし、ウェブのアプリでは、コントローラーにおいて、ユーザーの入力がたくさんの項目でいっぺんに入ってくるので、

namespace App\Http\Controllers;

use App\Models\Member;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class SignupController extends Controller
{
  public function postSignup(Request $request)
  {
    $member = new Member;
    $member->fill($request->all())->save();
  }
}

と、先のようにひとつひとつの項目に値を割り当てずに、一気に保存することできます。いわゆる、一括割り当てのマスアサインメント(Mass Assignment)です。

しかし、このコード、ちょっと問題があります。

上のコードでは、フォームからポストされる項目名がすべてが、クラスMemberのDBテーブルにあると仮定して、ララベルは一括割り当てを試みます。

たとえば、以下のようなフォームでは、

    <form method="post" action="signup">
        ログイン:
        <input type="email" name="email" value="{{ old('email') }}" required autofocus>

        パスワード:
        <input type="password" name="password" required>

        パスワードの確認:
        <input type="password" name="password_confirmation" required>
        
        名前:
        <input type="text" name="name" value="{{ old('name') }}" required>

        <button type="submit">保存</button>

    </form>

以下の項目が入力項目名ですが、

email
password
password_confirmation
name

password_confirmationは、通常、パスワード確認のための入力項目でDBには存在しない項目です。その値をDBに入れようとするところでエラーとなります。

どうしましょう?

エロクエント(Eloquent)では、どの値をDBに入れてよいか、どれを入れていけないかのルールの設定が可能です。$fillableあるいは$guardedのどちらかの変数の定義がその目的で使用されます。前者は、ホワイトリスト(入力OKの項目のリスト)、後者はブラックリスト(入力禁止の項目のリスト)です。

class Product extends Model
{
  protected $table = 'member';
  protected $primary_key = 'member_id';
  protected $timestamps = true;

  protected $fillable = ['email', 'password', 'name'];
}

こうすれば、エラーがなくDBに値を無事に入れることができます。

$fillableの代わりに、以下でも良いですね。

protected $guarded = ['password_confirmation'];

ああも言えたりこうも言えたり

Laravelでは、同じことを成し遂げるのにいくつか違う方法で行うことできます。これを便利かと思うかややこしいかと思うかは、人それぞれですが、

例えば、前回のマスアサインメントの紹介をしたときの以下のコード、

class SignupController extends Controller
{
  public function postSignup(Request $request)
  {
    $member = new Member;
    $member->fill($request->all())->save();
  }
}

これ以下のようにも書けるのですよ。

インスタンスを作成するクラス・メソッドで、コンパクトに。

class SignupController extends Controller
{
  public function postSignup(Request $request)
  {
    $member = Member::create($request->all());
  }
}

あるいは、パラメータでインスタンスを作成してまって、値を入れてセーブ。

class SignupController extends Controller
{
  public function postSignup(Request $request, Member $member)
  {
    $member->create($request->all());
  }
}

これら皆同じくDBにレコードを作成します。

Eloquentのアクセッサーの説明例

Laravelのマニュアルは、ほどよい説明で気に入っています。長い説明でポイントがつかめなく困ることはそうありません。理解には何回か読むことも必要ですが。

しかし、ときには掲載されるサンプルコードがよく使用されるような例ではなく、逆に混乱してしまうことあります。

例えば、Eloquent Mutatorsの以下のサンプルコード。

namespace App;

use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    public function getFirstNameAttribute($value)
    {
        return ucfirst($value);
    }
}

getFirstNameAttributeを上のように定義すると、

$user = App\User::find(1);

$firstName = $user->first_name;

のように使えると。

ucfirstの関数は、文字列の最初の文字を大文字にする関数なので、もとのDBテーブルuserfirst_nameの項目の値、つまり$user->first_nameが変換されるのだな、とは推測できます。

しかし、元の項目名もfirst_nameであるし、変換した値にアクセスするのもfirst_nameで、同じ変数名でややこしく思いませんか?

テンプレートで使用するなら、こちらの方が混乱せずに管理性が高いです。
{{ ucfirst($user->first_name) }}

アクセッサーの便利さの説明には、違うサンプルコードを使用した方が良いと思います。

例えば、

    public function getNameAttribute()
    {
        return $this->first_name.' '.$this->last_name;
    }

この例では、nameという項目はDBテーブルに存在しないという仮定です。

$user = App\User::find(1);

echo $user->name;

$user->first_nameが「Kenji」で、$user->last_nameが「Hino」なら、「Kenji Hino」が表示されます。つまり、モデルに新規の属性を作成するのは簡単ということが、このサンプルからわかります。

注意してもらいたいのは、関数名にパラメータがないことです。最初の例では、$valueがありました。そのパラメータは、関数名で指定される(この場合、FisrtName => first_name)変数の値をとってくるという意味ですね。

さて、この新規の属性name、他のDBから入ってくる属性と違って生成するオブジェクトに自動的にふくまれるというわけではありません。

$ php artisan tinker
Psy Shell v0.6.1 (PHP 5.6.13 — cli) by Justin Hileman
>>> use App\User;
=> false
>>> $user = User::find(1);
=> App\User {#847
     id: 1,
     created_at: "2015-04-21 11:56:46",
     updated_at: "2016-01-29 14:17:54",
     first_name: "Kenji",
     last_name: "Hino",
   }
>>> $user->name;
=> "Kenji Hino"
>>> $user->toArray()
=> [
     "id" => 1,
     "created_at" => "2015-04-21 11:56:46",
     "updated_at" => "2016-01-29 14:17:54",
     "first_name" => "Kenji",
     "last_name" => "Hino",
   ]
>>> 

最初のオブジェクトの属性としては、nameは入ってきませんね。しかし、$user->nameでは値が返ってきます。しかし、toArraytoJsonでは入ってきません。

これは、パフォーマンスのためにLaravelの意図的な仕様です。毎回毎回必要なものでもありませんね。

しかし、必要なときにはどうしたらよいのでしょう?

class User extends Model
{
    protected $appends = ['name'];
...

$appendsに新規属性を指定すると、


>>> $user->toArray()
=> [
     "id" => 1,
     "created_at" => "2015-04-21 11:56:46",
     "updated_at" => "2016-01-29 14:17:54",
     "first_name" => "Kenji",
     "last_name" => "Hino",
     "name" => "Kenji Hino",
   ]
>>> 

name入ってきますね。

Eloquentでカウントするときの注意

Eloquentのcount()の関数を使用して、DBのレコード数を数える作業はよく起こります。

例えば、前回の画像の件では、商品productのレコード1に対して商品画像product_imageレコードがが複数あるという、1対多の関係。そこでは、商品を削除するときに商品画像がないかをチェックする必要あります。画像のレコードがあるなら、削除を拒否あるいはユーザーに削除してよいか尋ねるということになります。

この場合は、商品画像があるかないかは、count()するのが一番。しかし、Eloquentではいろいろなカウントのコードの仕方があります。

今回は、これを説明するために、コントローラに特別にメソッドを作成してみました。

namespace App\Http\Controllers\User;

use App\Http\Controllers\Controller;
use App\Product;
use App\ProductImage;

class ProductController extends Controller
{
	public function getCount(Product $product)
	{
		$count1 = ProductImage::where('product_id', $product->product_id)->get()->count();
		$count2 = ProductImage::where('product_id', $product->product_id)->count();
		$count3 = $product->product_images->count();
		$count4 = $product->product_images()->count();

		return sprintf("<pre>count1 = %d\ncount2 = %d\ncount3 = %d\ncount4 = %d\n</pre>", $count1, $count2, $count3, $count4);
	}
}

最初の、$count1は、whereでproduct_imageのレコードを絞ってgetして、それらのレコードをcount()します。しかし、getしたのはIlluminate\Database\Eloquent\Collectionのオブジェクトであり、取得したレコードのデータすべてが入っています。2,3のレコード数なら問題ないけれど、1000とかあれば、それだけのデータがメモリーに入るわけで、単にレコード数が必要なのにとんでもない浪費です。

それに比べて次の、$count2は、SQLクエリのCOUNT(*)を使用するので、取得するのは、まさに1つのカウント数だです。$count1とは大きな違いです。

さて、Productのモデルには、

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
...
    public function product_images()
    {
        return $this->hasMany('App\ProductImage');
    }
}

ProductImageとのリレーションが定義されています。

これを利用したのが、先の$count3です。コードすっきりしましたね。しかし、ここ注意してください。この取得のしかたは、先の$count1とまったく同じなのです。つまり、必要なレコードをすべて含んだCollectionを作成してから、それをカウント。

これを正しくクエリで実行してもらうのが、$count4です。ちょっとした違いですね。product_images->count()product_images()->count()か。

実際、これらの実行がどうなっているかは、実行したクエリを見ればわかります。以下は、Debugbarの結果です。

count

カウント数は皆同じですが、2と4では、select count(*)ですが、1と3は、select *となっていますね。

親子関係のテーブルでのクエリーの作成(Eloquent編)

Laravel以前は、ほぼコードにSQL文を埋め込んでいたので、Eloquentよりクエリビルダーの方が馴染みあります。特に複数のDBテーブルをjoinした検索などには。

しかし、各Modelにおいてリレーションを定義していると、それを使用しないのがもったいないように思えてきます。

クエリビルダーでできることをEloquentではどうやるのか、興味ありありになってきました。

前回と同じ検索、親子関係のテーブルをリレーションを使ってできるか考えてみましょう。

前回と同様に、商品productと商品画像product_imageの親子関係、つまり、1対多の関係があるとします。
その関係をモデルで定義するには、以下。一応名前は、product_imagesと複数形にしてありますが、関数名のコンフリクトがないなら単数形でもOKです(要はプログラム内でどちらかに統一すること)。

namespace App;

use Illuminate\Database\Eloquent\Model;

class Product extends Model
{
...
    public function product_images()
    {
        return $this->hasMany('App\ProductImage');
    }
}

Laravelのマニュアルによると、hasが親子のテーブルをjoinしてくれるようです。


$products = Product::has('product_images')->get();

この実行は以下のようなSQL文となります。


select * from `product` where exists (select * from `product_image` where `product_image`.`product_id` = `product`.`product_id`)

ちょっと通常のjoinとは違いますね。

しかし、商品画像のレコードを1つでも持つ商品は、これ使えそうですね。そうなら、検索値で絞るとすると、


$input = [
    'name' => '商品名',
    'mime' => 'image/gif'
];

$products = Product::has('product_images')
    ->where('name', $input['name'])
  ->where('mime', $input['mime'])
  ->get();

しかし、これを実行するとエラーとなります。なぜなら、whereは両方とも、Productに対しての条件となり、mimeの項目名が、productに存在しないというエラーとなります。

正しくやるには、前回のクエリビルダーで使用したwhereInのようなものが必要です。


$input = [
    'name' => '商品名',
    'mime' => 'image/gif'
];

$products = Product::where('name', $input['name'])
    ->whereHas('product_images', function($query) use($input) {
	$query->where('mime', $input['mime']);
})->get();

今回は、クエリビルダーと違って、whereInを使うではなく、whereHasとなります。また、whereHasゆえに、Product::hasは要らなくなります。ちょっと慣れが必要ですね。

これを実行すると、SQL文は以下のようになります。

select * from `product` where `name` = 'Produt' and exists (select * from `product_image` where `product_image`.`product_id` = `product`.`product_id` and `mime` = 'image/gif')

入力のブラックリストとホワイトリスト

このブログを開始してから、もうすでに1年以上。RawのSQLを書いてコードに埋め込む日常から、Eloquentを使用したORMのコードへと日常へと移行しています。Eloquentに関しても、ブログを書き始めた頃からは理解が深まり、洗練されたLaravelのコードを書けるようになってきたこの頃です。

1年前に書いた「マスアサインメントで一括取り込み」のトピックで、EloquentのModelのクラスの属性fillableguardedの話、1年の経験で学んだことを含めてここでもう一度説明します。

まず、話のお膳立てを。

DBテーブルmemberにおいて以下の項目があるとします。

+----------------+------------------+------+-----+---------------------+----------------+
| Field          | Type             | Null | Key | Default             | Extra          |
+----------------+------------------+------+-----+---------------------+----------------+
| member_id      | int(10) unsigned | NO   | PRI | NULL                | auto_increment |
| active_flag    | char(1)          | NO   |     | NULL                |                |
| name           | varchar(255)     | NO   |     | NULL                |                |
| email          | varchar(255)     | NO   | UNI | NULL                |                |
| password       | varchar(60)      | NO   |     | NULL                |                |
| memo           | varchar(100)     | YES  |     | NULL                |                |
| created_at     | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
| updated_at     | timestamp        | NO   |     | 0000-00-00 00:00:00 |                |
+----------------+------------------+------+-----+---------------------+----------------+

MemberのModelは以下のような定義になります。

...
class Member extends Model
{
    protected $table = 'member';
    protected $primaryKey = 'member_id';
    protected $fillable = ['name', 'email'];
    ...
}

$timestamps$incrementingの定義は必要ないです。両方ともデフォルトでtrueなので。

入力フォームは、

Laravel 2016-08-31 20-51-04

で、email, password, nameの項目を入力できます。

以下のコントローラで、入力フォームから入ってきた値は以下のコードでDBに保存できます。

...
class MemberController extends Controller
{
    ...
    public store(Request $request)
    {
        $member = new Member;
        $member->fill($request->all())->save();
        ...
    }
    ...
}

しかし、先ほどの$fillable定義により、DBに保存されるのは、emailとnameのみです。実行されるSQLは以下で、他の入力された値は無視されるので、passwordはDBはデフォルトの空のままです。つまり、$fillableは、DBに入力したい値をリストするホワイトリストです。ちなみに、created_at, updated_atの項目は、Eloquentにより自動的に保存時の日時を記録します。

逆に、DBに入れたくない項目をリストするなら、つまりブラックリストを定義したいなら、$fillableの代わりに、$guardedを使用します。

...
class Member extends Model
{
    protected $table = 'member';
    protected $primaryKey = 'member_id';
    protected $guarded = ['member_id', 'active_flag', 'password'];
    ...
}

以上がマスアサイメントの使用の仕方で、意図的あるいは間違って入力フォームから、DBへ保存されるのを防いでくれます。

$fillable$guardedの目的を理解したところで、2つ問題。

まず、

active_flagやpasswordなどマスアサインメントで相手にしない項目の値はどうやってDBに保存するのでしょう?

これは、通常のオブジェクトの値のアサインメントで行います。

...
    public store(Request $request)
    {
        $member = new Member;
        $member->active_flag = 'Y'; //デフォルト
        $member->password = bcrypt($request->password); //ハッシュ値に変換
        $member->fill($request->all())->save();
        ...
    }
...

次に、
入力画面によりDBに入れたい項目が違う場合は、どう$fillableを設定?

例えば、管理画面で会員の情報を編集する画面。そこでは、会員が有効か無効のフラッグ(active_flag)、そして会員に関するノート(memo)も付加情報としてDBに保存したいです。もちろん、裏で使用するのは、同じMemberのクラスなので、同じ$fillableは使えないですね。

1つは、先の値のアサイメント使用する方法。

    $member->active_flag = $request->active_flag;
    $member->memo = $request->memo;
    $member->fill($request->all())->save();

しかし、これではもっと項目が増えたら面倒です。

$fillableを使用するのではなく、先のように$guardedをMemberで定義して、以下のようにコントローラにおいて、独自の$fillableを使用します。

...
    public store(Request $request)
    {
     $fillable = ['email', 'name'];
        $input = array_only($request->all(), $fillable);
        $member = new Member;
        $member->active_flag = 'Y';
        $member->password = Hash::make($request->password);
        $member->fill($input)->save();
        ...
    }

    public edit(Member $member, Request $request)
    {
        $fillable = ['email', 'name', 'active_flag', 'memo'];
        $input = array_only($request->all(), $fillable);
        $member->fill($input)->save();
        ...
    }
...

array_onlyの関数は、Laravelのヘルパー関数です。

Laravel 5.3 タイムスタンプのDB項目名の指定

Laravel 5.3に更新して、Eloquentのモデルの設定において嬉しいこと発見しました。

LaravelのEloquentでは、指定のDBテーブルにおいて、作成日時と編集日時に、規定のcreated_atupdated_atの項目名が使用されているなら、いちいち、

use Users;
use Carbon\Carbon;

$user = new Users;
...
$user->created_at = $user->upddated_at = Carbon::now();

$user->save();

というようなことを、DBレコードの追加や編集の際に、書かなくとも自動で作成日時と編集日時に値を入れてくれます。

私のケースでは、既存のプロジェクトのDBにおいて、作成日時と編集日時には、date_createddate_modifiedと違う名前を使用していて、今まで「いちいち」コードで指定していました。

しかし、Laravel 5.3では、以下のようにモデルの定数(const)を指定することで、項目名を指定できるようになりました。


namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Company extends Model
{
	protected $table = 'company';
	protected $primaryKey = 'company_id';
	public $timestamps = true; //デフォルトではtrueなので、指定する必要はない

	const CREATED_AT = 'date_created';
	const UPDATED_AT = 'date_modified';
..

これは大変便利です。

もう1つ便利なことで最近見つけたことは、最初の例で使用したCarbonは、Laravelをインストールしたら一緒にインストールされるパッケージですが、使い勝手あります。

例えば、DBに記録した日時から現在までの「経過日数」を計算するには、

$days_past = (new Carbon($user->updated_at))->diff(Carbon::now())->days;

簡単でわかりやすいですね。これをphpでやろうとすると、文字列から秒数に変換してなどと大変です。

最後に、日時の使用で忘れてならないのは、必ずアプリの設定ファイルで、タイムゾーンを設定すること。

..
    'timezone' => env('APP_TIMEZONE', 'Asia/Tokyo'),
..
Top