[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/views
へarchive.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></title>
<link href="" rel="stylesheet">
<meta name="csrf-token" content="">
</head>
<body>
<div class="container my-5">
<form class="form-row" action="">
<div class="col">
<div class="form-group mx-sm-3">
<input type="text" class="form-control" name="q" value="" 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"></dd>
<dt class="col-sm-2">フリガナ</dt>
<dd class="col-sm-10 meta-kana"></dd>
<dt class="col-sm-2">住所</dt>
<dd class="col-sm-10 meta-address"></dd>
<dt class="col-sm-2">メールアドレス</dt>
<dd class="col-sm-10 meta-email"></dd>
</dl>
</div>
</a>
@endforeach
</div>
</div>
<script src=""></script>
</body>
</html>
/archive
へアクセスすると一覧が表示されます。
検索フォームにname:田
と入力して検索していみます。
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>
検索したキーワードがハイライトされます。