[Laravel] EloquentモデルでJSON型のカラムを扱う方法

MySQL 5.7ではカラム型にJSONを扱えるようになりました。もちろんLaravelのEloquentも対応しており、簡単に読み取りや検索を行うことができます。

実践的なアプリケーションでMySQLのJSON型を扱う方法について検証してみましょう。

テーブルの作成

JSON型を扱うテーブルのModelと、migrationファイルを作成します。

$ php artisan make:model Archive -m

今回はarchivesというテーブルにユーザー情報を持つmetaカラムがあるとします。

// database/migrations/2018_03_28_013327_create_archives_table.php

<?php

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

class CreateArchivesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('archives', function (Blueprint $table) {
            $table->increments('id');
            $table->json('meta');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('archives');
    }
}

JSON型に対応していないバージョンの場合はTEXT型で作成しましょう。

Eloquentモデルのcastsプロパティに値を指定することで、データベースから取得した値を指定したフォーマットへ変更することができるようになります。

  • int (integer)
  • real (float, double)
  • string
  • bool (boolean)
  • object
  • array (json)
  • collection
  • date
  • datetime (custom_datetime)
  • timestamp

metaカラムをJSONへキャストするように設定しておきましょう。

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

class Archive extends Model
{
    protected $guarded = ['*'];

    protected $casts = ['meta' => 'json'];
}

Factory (ダミーデータ) の作成

データの登録を簡単にするために、Archiveに対してのFactoryを作成しておきましょう。

$ php artisan make:factory ArchiveFactory

metaカラムに対して、名前フリガナ住所メールアドレスが登録されるものとします。

Fakerを利用してダミーデータを登録できるようにします。

config/app.php'faker_locale' => 'ja_JP'を追加することで日本語に対応したダミーデータの作成が可能になります。

<?php

use Faker\Generator as Faker;

/** @var \Illuminate\Database\Eloquent\Factory $factory */
$factory->define(App\Archive::class, function (Faker $faker) {
    return [
        'meta' => [
            'name' => $faker->name,
            'kana' => $faker->kanaName,
            'address' => $faker->address,
            'email' => $faker->email
        ]
    ];
});

Factoryが作成できたらtinkerを使ってダミーデータを登録しておきましょう。

$ php artisan tinker
Psy Shell v0.8.17 (PHP 7.1.10 — cli) by Justin Hileman
>>> factory(App\Archive::class, 100)->create();

コントローラー&Viewファイルの作成

登録されたArchive情報を取得するためのコントローラーを作成します。

$ php artisan make:controller ArchiveController

対応するルーティングも追加しておきましょう。

// routes/web.php

Route::get('archive', 'ArchiveController');

Archive情報の一覧表示に加えて、metaカラムのJSON情報を検索できるようにしてみましょう。

以下のようなクエリを発行することで、JSON型のカラムに対しての条件検索ができます。

select * from `archives` where `meta`->'$."name"' like '%鈴木%'

これをEloquentのORMで利用するには以下のように記述します。

Archive::where('meta->name', 'like', '%鈴木%');

Illuminate\Database\Query\Grammars\MySqlGrammar::wrapJsonSelectorにより、->に対してラップ処理が行われ、%s->'$.%s'に置換されてJSON型の検索が可能になります。

/**
 * Wrap the given JSON selector.
 *
 * @param  string  $value
 * @return string
 */
protected function wrapJsonSelector($value)
{
    $path = explode('->', $value);

    $field = $this->wrapValue(array_shift($path));

    return sprintf('%s->\'$.%s\'', $field, collect($path)->map(function ($part) {
        return '"'.$part.'"';
    })->implode('.'));
}

Archiveテーブルの検索条件に使うパラメーターが?q=name:田中,address=東京都の用に送られてくるとして、ここから必要な条件のトリミングを行いkey:valueをもつコレクションを作成します。

以上をふまえて以下のようなコントローラーを作成します。

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use App\Archive;

class ArchiveController extends Controller
{
    protected $metaKeys = ['name', 'kana', 'address', 'email'];

    public function __invoke(Request $request)
    {
        $searchMetas = array_reduce(explode(',', $request->q), function($meta, $q) {
            $key = str_before($q, ':');
            $value = str_after($q, ':');

            if (in_array($key, $this->metaKeys) && filled($value)) {
                $meta->put($key, $value);
            }

            return $meta;
        }, new Collection);

        $model = Archive::query();

        foreach ($this->metaKeys as $metaKey) {
            if ($searchMetas->has($metaKey)) {
                $model->where('meta->'.$metaKey, 'like', '%'.$searchMetas->get($metaKey).'%');
            }
        }

        $archives = $model->get();

        return view('archive', compact('archives', 'searchMetas'));
    }
}

resources/viewsarchive.blade.phpを作成します。

Laravelに同封されているBootstrapを利用して以下の用に作成します。

<!doctype html>
<html lang="ja">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>{{ config('app.name') }}</title>

    <link href="{{ asset('css/app.css') }}" rel="stylesheet">
    <meta name="csrf-token" content="{{ csrf_token() }}">
</head>

<body>

<div class="container my-5">
    <form class="form-row" action="{{ URL::current() }}">
        <div class="col">
            <div class="form-group mx-sm-3">
                <input type="text" class="form-control" name="q" value="{{ Request::get('q') }}" placeholder="key1:value1,key2:value2...">
            </div>
        </div>

        <div class="col">
            <button type="submit" class="btn btn-primary">Search</button>
        </div>
    </form>
</div>

<div class="container my-5">
    <div class="list-group">
        @foreach ($archives as $archive)
        <a href="#" class="list-group-item list-group-item-action flex-column align-items-start">
            <div class="mt-2">
                <dl class="row">
                    <dt class="col-sm-2">名前</dt>
                    <dd class="col-sm-10 meta-name">{{ $archive->meta['name'] }}</dd>
                    <dt class="col-sm-2">フリガナ</dt>
                    <dd class="col-sm-10 meta-kana">{{ $archive->meta['kana'] }}</dd>
                    <dt class="col-sm-2">住所</dt>
                    <dd class="col-sm-10 meta-address">{{ $archive->meta['address'] }}</dd>
                    <dt class="col-sm-2">メールアドレス</dt>
                    <dd class="col-sm-10 meta-email">{{ $archive->meta['email'] }}</dd>
                </dl>
            </div>
        </a>
        @endforeach
    </div>
</div>

<script src="{{ asset('js/app.js') }}"></script>

</body>
</html>

/archiveへアクセスすると一覧が表示されます。

laravel-eloquent-json_01

検索フォームにname:田と入力して検索していみます。

laravel-eloquent-json_02

meta情報のnameが入っている情報が出力されました。

(番外編) 検索にヒットした文字列をハイライトする

Laravelとは関係ありませんが、検索した文字列をハイライトさせることで、より視覚的に判断することができます。

Javascriptのmark.jsを利用してマーキングを行います。

js/app.jsを読み込んでいる下に以下のコードを追加します。

<script src="{{ asset('js/app.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mark.js/8.11.1/jquery.mark.min.js"></script>

<script>
    @foreach ($searchMetas as $key => $value)
      $(".meta-{{ $key }}").mark('{{ $value }}');
    @endforeach
</script>

検索したキーワードがハイライトされます。

laravel-eloquent-json_03

© Xzxzyzyz