tohokuaikiのチラシの裏

技術的ネタとか。

「Laravelリファレンス」3 データベース

マイグレーション

つらつらっと読んで気になったところだけメモ

artisanのmake:migrationコマンドで作るClass名の重複に注意

コマンド引数の実行内容(下記の場合はcreate_authors_table)は同じものが被るとClass名として被ってしまい、Fatalになるので気を付ける。

./artisan make:migration create_authors_table --create=authors

migrationファイルが不要になった時は composer dump-autoloadしてautoloadファイルを作り直す

マイグレーションファイルを削除したのに亡霊のように出てきてオカシイ

$ ./artisan migrate:refresh


  [ErrorException]
  include(/home/vagrant/laravel/sample/3/vendor/composer/../../database/migrations/2016_12_26_105
  300_create_authors_table.php): failed to open stream: No such file or directory

何かと思ったら、 ./vendor/composer/autoload_static.php ./vendor/composer/autoload_classmap.php でこの作ったファイルをローディングしていた。

$ composer dump-autoload

して解決。

migrateコマンドの--pretendオプションが効かないっポイ

SQLを見せてくれるはずのオプションなんだけど

$ ./artisan migrate --pretend


  [Doctrine\DBAL\Schema\SchemaException]
  There is no column with name 'name' on table 'authors'.

ちゃんと動かない・・・。

多分、ALTERが入っている時にはそのTableが既に作られて無いとダメなんだと思う。

同じように、

<?php
    public function up()
    {
        if (Schema::hasTable('authors')) {

ってやったらちゃんと判定してくれなかった。

実migrate実行時には正しく判定してたのでおそらくConnectionをしてない状態でやってるからだと思う。

スキーマビルダ

これもメモ程度で。

  • columnの削除はBlueprint $table->dropColumn(カラム名)で。*1
  • columnの作成とプロパティの設定は、$table->string('name')->nullable()のようにチェインでColumnオブジェクトを使う。
  • indexの作成は、$table->index('name') あるいは、$table->string('name', 255)->index() とする。どちらも追加引数としてインデックス名を指定できる。
  • indexの作成はインデックス名を省略できるけど、削除の際はできない。カラム名を指定しても勿論ダメ。
  • columnの変更の場合は、取得したcolumnに->change()メソッドを掛けてやることで変更の指示が加わる。
<?php
    public function up()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->index('name');
            $table->string('katakana')->index();
            $table->string('furigana', 10)->change()->nullable();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('authors', function (Blueprint $table) {
            $table->dropIndex('authors_name_index');
            $table->dropIndex('authors_katakana_index');
        });
    }

これ、実際には何度かmigrate → rollbackを繰り返してチェックしないときれいなMigrationは作れないな。

シーダー(あるいは初期投入テスト用データ)

  • 最初のLaravelプロジェクトの作成時に作られるdatabase/seeds/DatabaseSeeder.php のrunメソッドが実行される。
  • run()実行の際にEloquentのMass Assignment制約を解除するためにModel::unguard();を実行する。
  • 各テーブルのデータ投入については、 ./artisan make:seeder Hoges でHogesテーブルのSeedを作り、できたファイルのrunメソッドを実装する。
  • ↑のrunメソッドでは、各カラムを要素に持った配列を作ってDB::table('authors')->insert($author)
  • ↑のrunメソッドでは、Eloquentを利用したデータの挿入も行える。\App\Author::create([...])
  • ↑のクラスにtruncateTable($table)をつくり、各Seedのrun内の$this->call()の前にそのテーブルのtruncateを実行させる。
  •  ↑truncateTableの際に外部キー制約を解除するためDB::statement('SET foreign_key_checks = 0');を実行する。(MySQLの場合)

シーダーの実行

./artisan db:seed

でシード投入

./artisan db:seed --class=AuthorTableSeeder

でAuthorTableSeederのみ実行。

./artisan migrate:refresh --seed

マイグレーションの再実行(rollback&migrate)からseedの投入まで一発

DBファサード

select

<?php
$result = DB::select(select文);
$result = DB::select(プレースホルダ付きselect文, [param1 => "foo" , param2 => "bar"]);
foreach ($result as $r){
    echo $r->name;
}

プレースホルダは、

select from authors where name=:name and email=:email

というSQLに対して、第二引数を

<?php
["name" => "ITOH", "email" => "itoh@example.com"]

という感じで与える。

あるいは、

select from authors where name=? and email=?

に引数を

<?php
["ITOH", "itoh@example.com"]

いずれもプレースホルダはクオートで囲む必要は無い。

返り値は、config/database.phpの'fetch' => PDO::FETCH_CLASSで設定したものになる。

selectの繋ぎ先

read/writeでDBを分けている場合、readの方に繋ぐ。強制的にwriteの方に繋ぎたい場合は、DB::selectFromWriteConnection()を使う。

INSERT

DB::insert(INSERT文); でBooleanが返ってくる。

UPDATE

DB::update(UPDATE文); で影響を受けた行数が返ってくる。

DELETE

DB::delete(DELETE文); で影響を受けた行数が返ってくる。

ステートメントの変更

DB::statement(SET文やDROP文など); でBooleanが返ってくる。

接続先の変更

普通にDB::select()などをした場合、config/database.phpの「'default' => env('DB_CONNECTION', 'mysql'),」で指定されているものに繋ぎに行く。この場合、.envにDB_CONNECTIONが無かったらmysqlに行くようになる。

config/database.phpのconnectionsキーの別のものに繋ぎにいきたい場合、DB::connection()を使う。

<?php
DB::connection('mysql2')->select(...)

トランザクションの指定

DB::transaction()とクロージャを使う

<?php
DB::transaction(function(){
    /// ここにさまざまなSQL処理を行う。
});

特にRollbackやBeginを行う必要は無い。一応手動で、DB::commit()/ DB::rollback()/ DB::begin()はある。

クエリログをとりたい場合

<?php
DB::enableQueryLog(); // これでクエリログが残る

////// ここにさまざまなSQL処理を行う。

$logs = DB::getQueryLog(); // クエリログを取得
dd($logs); // ddはブラウザにきれいに出力してくれるvar_dump()

実行SQLをとりたい場合

<?php
$sql = DB::pretend(function(){
/// ここにさまざまなSQL処理を行う。
});
Log::debug($sql); // ログファイルにデバッグ出力

クエリビルダ

プレースホルダを使わなくてもいいっていう所が若干楽かも。

Eloquent ORM

やっぱりクエリビルダよりもORM欲しいですよね。

LaravelはFacadeとフォーム周り(特にValidateorとエラー処理)が肌感覚にあうので、あとはこのORMがしっかりしてればもう言うこと無し。

デフォルト設定

テーブルや主キーについて、以下の設定で作られている。

  • テーブル名はClass名と同じ。table_name => TableNameという感じの対応。変更する場合は、$tablesプロパティで指定。
  • 主キーは、idフィールド。変更する場合は、$primaryKeyプロパティで指定。
  • タイムスタンプが必ず付く。created_atとupdated_at。$timestampsプロパティをfalseにするとタイムスタンプ無し
  • タイムスタンプのフォーマットは、Y-m-d H:i:sだがUnixtimestampにしたい場合は $dateFormatを"U"にする。
  • $connection => database.phpの設定値。必ずどこのDBに取りにいくか決まっている場合
  • incrementing 主キーが自動増加か。デフォルトはtrue

select系

  • all()メソッドで全件取得
  • where('name', "=", "itoh")などのクエリビルダが使える。

insert/update系

  • newして、propertyをセットしてsave()メソッド。(new User())->save()
  • あるいは、updateメソッドの引数に新しいプロパティ配列を渡して実行(save()の際の引数にこれを渡しても無効)
  • updateメソッドの便利なところはEloquent::where()->で取ってきたものに対してそのまま->update()をチェインできるので複数のデータに対して処理が一括でできること。

MassAssignmentエラー

サンプルのUser.phpは既に対策済みなのでこのエラーが出なかった。

要は、update()やsave()においてHTTPからの入力を許可して良いものと悪いものがあるということか。

正確には、HTTPからというより $model->propertyA みたいに直接プロパティを変更しようとするとダメってことね。

$fillable/$guardedのどちらかを使うとこのエラーは出なくなる。

ホワイトリスト形式で混入を防ぐ

Model継承クラスに $fillable プロパティを設定し、このプロパティの中に直接プロパティを変更してもよいものを羅列する。 これ以外のプロパティを変更しようとしても変更はできない。

ブラックリスト形式で購入を防ぐ

$fillableとどうように$guardedを作り、この中のプロパティは設定しようとしても変更できない。

$guardedと$fillableは同時には使えない。エラーとかにはならない。$guardedが優先されるようだ。

変更を制限されたプロパティを変更したい場合は

Modelに独自にSetterなどのメソッドを用意してやればいい。

削除

<?php
Author::find(1)->delete();
Author::destroy(1); // これは上と全く同じ
論理削除

Illuminate\Database\Eloquent\SoftDeletesを使う

  1. tableにdeleted_atフィールドをtimestamp型で作成する
  2. ModelクラスにSoftDeletesをtraitさせる。
  3. Modelクラスの$datesプロパティ配列にdeleted_atを加える。*2

すると、destroy()やdelete()が論理削除になる。

論理削除すると、Modelのメソッドのfindやallではselectの対象にならない。その他、以下のような特徴がある。

  • $model->trashed() で論理削除済みかどうかが分かる。
  • Model::withTrashed()->find()を使うと論理削除済みのものも取得できる。
  • restore()で復帰できる
  • forceDelete()で物理削除

usersテーブルを例にとってやってみる。

migrationを作る
./artisan make:migration  alter_table_user_add_softdeletes --table=users

できたClassファイルに

<?php

use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AlterTableUserAddSoftdeletes extends Migration
{
    public function up()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->softDeletes()->after('id'); // add (afterは無くても大丈夫)
        });
    }

    public function down()
    {
        Schema::table('users', function (Blueprint $table) {
            $table->dropSoftDeletes(); // add
        });
    }
}

とする。2行加えただけ。 ./artisan migrate で実行。

Modelクラスを変更

NamespaceでuseしてTraitでuse。$datesプロパティ設置

<?php
use Illuminate\Database\Eloquent\SoftDeletes;

class User extends Model 
{
    use SoftDeletes;

    $dates = ['deleted_at'];

}

アクセサとミューテータ(getterとsetterの仲介者)

get(set)プロパティ名Attributeがそれにあたる。これは、Modelのset/getマジックメソッドで使っているものでその媒介に使われるもの。

getの場合はAttributeの生値$valueを受けてそれをどう変形するかを記載する。

setの方は、Model::setAttributeを上書きする形で$this->attributesに値を納めるような処理をする。

日付の扱い

$datesプロパティを配列にしてにフィールドを入れると、$model->date_at がCarbonクラスのンスタンスになる。$model->created_at,$model->updated_atと同じ扱い。

シリアライズ あるいはModelのプロパティを全て一気に表示したい場合の話

toArray/toJsonメソッドを使う。1つのModelでも複数のModelでも同じ。

<?php
$u = User::find(1)->toArray();
$a = Author::all()->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
dd($u, $a);

toJsonの引数は表示オプション。

シリアライズに含めたくないパラメータ

ブラックリスト方式 → $hidden にそのProperty名を入れる。

ホワイトリスト方式 → $visibleにそのProperty名を入れる。

リレーション

リレーションを作りたい場合は、Modelクラスに対応するTableの名称のメソッドを作る。

1対1の場合は、メソッド名が単数形(にすると分かりやすいというだけで必須ではない)+hasOne

<?php
class Author extends Model {

    public function phone()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->hasOne(Phone::class, 'author_id', 'id');
    }

1対多の指定はメソッド名が複数形(にすると分かりやすいというだけで必須ではない) + hasMany

<?php
class Author extends Model {

    public function books()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->hasMany(Book::class, 'author_id', 'id');
    }
books()をクエリビルダとしてコールする

メソッドとしてコールした場合、クエリビルダが返ってくるので、こういうこともできる。

<?php
Author::find(1)->books()->where('title', 'LIKE', 'Laravel%');

(Properyコールのマジックメソッドとして呼ばれた場合はModelのCollectionが返ってくる。)

逆向きのリレーション

phoneテーブルにauthor_idがあり、PhoneオブジェクトからAuthorを取得しようとする場合。

<?php
class Phone extends Model
{
    public function author()
    {
        // コード規程に則ったものであれば、author_id, idの2つの引数は不要。
        return $this->belongsTo(Author::class, 'author_id', 'id');

ところで何気に複数形ってどうやって決めてるの?

何故EloquentModelはクラス名を複数形にしたテーブルを自動で見に行けるのか - QiitaでPluralizerクラスがやってるっていうのでそこだけ抜き出して調べてみた。

<?php
use Illuminate\Support\Pluralizer;

$r = ['fish', 'carp', 'salmon', 'trout', 'sheep', 'cattle', 'deer', 'Japanese', 'Chinese', 'Korean' ];
foreach ($r as $_r){
    printf("%s => %s<br>", $_r, Pluralizer::plural($_r)); 
}
fish => fish
carp => carp
salmon => salmon
trout => trout
sheep => sheep
cattle => cattles
deer => deer
Japanese => Japanese
Chinesse => Chinesses
Korean => Koreans

実態は、vendor/doctrine/inflector/lib/Doctrine/Common/Inflector/Inflector.php らしい。これ見たら沢山単語があった。

ということで、テーブルの複数形が気になったらこのクラスを使って実際に複数形を見てみることが大切なような気がする。

中間テーブルのあるリレーション

中間テーブルの規約
  • テーブル名は「Aテーブル名(単数形)_Bテーブル(単数形)」である。
  • 中間テーブルには、Aテーブルの主キーとBテーブルの主キーのフィールドとIDとタイムスタンプのみ
SELECT

このリレーション先を取得するメソッドを作成し、belongsToManyの第一引数に中間テーブル越しのリレーション先のClassを指定する。

以下は、authors => author_author_type => author_types とRDBMSの存在する際の例。

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class);
    }

中間テーブルにある別のフィールドが欲しい場合は

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class)->withPivot('id');
    }

とすると、

<?php
foreach (Author::find(1)->types as $type){
     echo $type->pivot->id ;
}

とすることで、中間テーブルのIDが取得できる。

タイムスタンプが欲しい場合は

<?php
class Author extends Model
{
    public function types()
    {
        return $this->belongsToMany(AuthorType::class)->withTimestamps();
    }

とすると、$type->pivot->created_atが使えたりする。

ちなみに、types()メソッドはBuilderを返すので、

<?php
Author::find(1)->types()->where('name', 'like', '%i%')->get();

とすることで、AuthorTypesクラスのインスタンスをそのまま配列で受け取れる。

->typesも->types()もどちらもAuthorTypesインスタンスのコレクションを返すようになっている。全く同じであり、typesはtypes()の省略形(最後の->get()が不要)である。

簡単にAuthorTypesの単一フィールドnameだけを一覧で欲しい場合は、

<?php
Author::find(1)->types()->where('name', 'like', '%i%')->lists('name');

とするとStringの配列で取得できる。

hasメソッドでデータのあるもののみを取得
<?php
$book_authors = Author::has('books')->get();
// 3つ以上あるauthorを取得
$book_authors = Author::has('books', '>=', 3)->get();
whereHasメソッドでリレーション先のデータのフィールドのWhere句を作る
<?php
$php_book_authors = Author::whereHas('books', function($query){
    $query->where('title', 'like', '%PHP%');
})->get();

これで作られるSQL

select * from `authors` 
    where (select count(*) from `books`
              where `books`.`author_id` = `authors`.`id` and
                    `title` like "%18%")
           >= 1
Eager Loading

使う時はその後foreachでまわすような場合。

<?php
Book::with('author')->all();
複数のプロパティをEager Loadingする場合(引数を2つ)。更に一段向こうのテーブルからも含める場合(ドット区切りで示す)。
<?php
$pref = Prefecture::with('author', 'author.phone')->where('id', 13)->get();
// これだと動作しない。Builderからwith()は効かない。
// $pref = Prefecture::find(13)->with('author', 'author.phone');
foreach ($pref as $p){
    foreach ($p->author as $a){
        if ($a->phone){
            echo $a->phone->phone_number."<br>";

これで使われるSQL

select * from `prefectures` where `id` = 13;
select * from `authors` where `authors`.`deleted_at` is null and `authors`.`prefecture_id` in (13);
select * from `phones` where `phones`.`author_id`
    in ("1","2","3","4","5","6","7","8","9","10","11","12","13","14","15","16","17","18","19","20","21");

の3本。author.phoneが無くても動作するが、SQLの発行数が多くなる。

Eager Loadingの対象から更に絞込み

対象となるテーブルの指定を配列にして、'table' => クロージャで指定。

<?php
$pref = 
  Prefecture::with(['author' => function($query){
                                    $query->where('name', 'like', '%3');
                                }],
                    'author.phone')->where('id', 13)->get();
元となるデータを取得したあとにEager Loading

取得結果に対してloadメソッドを使う。

<?php
$pref = Prefecture::where("name", 'LIKE', '%県')->get();
$pref->load('author');
1対N × 1対Nの場合(Has Many Through)

1つのModelのIDに複数のModelがからみ、さらにそのModelに複数のModelが絡んでいる場合。

「(著者の)出身県」→「著者一覧」→「(著者の)本一覧」

というリレーションの場合に、出身県別の本を取得したい場合。

<?php
class Prefecture extends Model
{
    public function books()
    {
        return $this->hasManyThrough(Book::class, Author::class);
//      引数を全て書く場合は下記のとおり。中間テーブルが持つ自分の主キー・最終テーブルが持つ中間テーブルの主キー・自分の主キー
//      return $this->hasManyThrough(Book::class, Author::class, 'prefecture_id', 'author_id', 'id');

Preferctureから直接Bookを取るメソッドをはやす。このメソッドでは、hasManyThroughを使う。引数の順番は「最終的に取るもの」が最初。

主キーが規則どおりで無い場合は逐一指定する。

CREATE/UPDATE
リレーション先への更新

親となるIDを含んだまま更新して欲しいということで。

こんな感じ。

<?php
$author = new Author();
$author->fill([
            'name' => '伊藤貴史',
            'prefecture_id' => 14,
            'furigana' => 'フリガナ',
            'romaji' => 'itoh takashi',
            ])->save();

$phone = new Phone();
$phone->fill([
            'phone_number' => '09000000000'
            ]);
$author->phone()->save($phone);

あるいは $author=Author::create(['name'...])で得てもかまわない。

ただ、$phoneは自ら$phone->save()しようとすると、外部キー制約に引っかかりINSERTできない。必ず、$author->phone()からsaveしてやる必要がある。

リレーションしている親を更新する場合

自分がbelongsしている親を変える場合は、associate()メソッドを使う。

<?php
$phone = Phone::find(28);
echo $phone->author_id; // 36
$phone->author()->associate(Author::find(35));
echo $phone->author_id; // 35
N対Nの場合の更新

中間テーブルがある場合、どちら側からにしても

<?php
$author = Author::find(1);
$type_ids = $author->types()->lists('name'); // 0,1,2,3,4
$author->types()->detach(6);
$type_ids = $author->types()->lists('name'); // 0,1,2,3,4,6 

逆側から

<?php
$type = AuthorType::find(1);
$type->authors()->attach(20);

子モデルが変わった際に、親モデルのupdated_atを変更する

自分がbelongsTo()しているテーブルに対してその通知を記録したい場合

<?php
class Phone extends Model
{
    protected $touches = ["author"];

    public function author()
    {
        $this->belongsTo(Author::class);
    }

$touchesプロパティに対象(単数形)を入れることで自動的に記録される。

ちょっと思ったこと・・・プロパティ呼び出しとメソッド呼び出しのどちらを使うのか判定

リレーションがある場合、

$model->relation か $model->relation() かどっちを使う?っていう場合。

単純にインスタンスとしてのリレーションが欲しかったら、$model->relationで、relationの先に何かをつなぎたい場合。。。。relationをwhere句で狭めたり、relationのAssosiationIDを変更したり、という場合は $model->relation()を使うって言う感じかな。

*1:何故かこの本ではdropIndex()は説明してるのにこれが無かった。まぁ想像できるけどさ。。。

*2:これは論理削除の実行に特に要らないみたいだが、日付をCarbonインスタンスとして扱うのにやっておくと便利