Edited at

実運用中のWordPressサイトのデータベース(合計46テーブル)を解析し、全て組み替えLaravelで再構築した話

こんにちは tyamahori (ちゃまほり) です。

副業で働いているNoSchoolのCTO・名人の記事が好評です。

【実録】WordPressサイトをAWS+Laravel+Nuxtにフルリプレイスした話(技術選定編)

ありがたいことに僕の名前を出していただきました。


プロローグ

それは2019年の1月中旬でした。NoSchoolの脱WordPressプロジェクトにおいて、Laravelへの移行に向けて、WordPressのDBを組み直すというタスクを担当することになりました。WordPressのDBを移行するという、途方も無いタスクを目の前に僕は、心の中で、「あ〜、どうしよう〜・・・」となんども呟いていた時でした。


この記事について

WordPressのDB移行というタスクをどのようにこなしていったのかをまとめていきます。


何を知れるのか?


  • どうやって仕様を把握したのか?

  • 仕様をもとに実際どうやって移行したのか?

  • 新規のDB設計においての失敗談や意識するべきだと思った点


対象読者について


  • WordPressからの脱却をしたい人


    • 特にLaravelに移行を考えている人



  • 理想のDB設計を目指すために、考えるべきことを知りたい人


どうやってやったのか?

全体的にやったことを以下にまとめます。


  • WordPressサイトの仕様を整理する

  • WordPressのDB構成の解析をする


    • どのテーブルにどんなデータがあるのかを把握する

    • 必要なデータが何かを把握する



  • Laravelにて使用するDB設計を行う


    • テーブル設計、データ設計などを行う



  • Laravelのartisanコマンドを使用して、オリジナルのマイグレーション処理を実装する

それでは各項目に関してまとめていきます。


WordPress側にて行ったこと


サイトの仕様を把握

何はともあれサイト仕様やDBの構成を把握することに努めます。サイト仕様の確認はチーム内にてGitHub issueを使いやり取りを進めました。以下のキャプチャは全て実際のやりとりです。

WPDB.jpg

めちゃくちゃ長いんですね。。w


DB構造の解析

実際のWordPressのDBをお見せするのはちょっとあれなので、今回はWordPressの初期DBを参考に例示していきます。

Screenshot from Gyazo

ここでは各テーブルにどんなデータが入っているのかを把握していきます。例えば、


  • wp_usersとwp_usermetaの関係

  • wp_termsとwp_termmetaの関係

  • wp_postsとwp_postmetaの関係

おそらくここら辺の関係を把握することが大切です。

今回は wp_usersとwp_usermetaテーブルを見てみます。

※今回のデモ用に作ったデータです。メールアドレスは僕が公開しているやつです。

wp_usersテーブル

Screenshot from Gyazo

wp_usermetaテーブル

Screenshot from Gyazo

いわゆる縦持ちテーブルになっていることがわかります。

これでは移行後のDBで縦持ちテーブルは色々と不都合なので、横持ちにしていきます。

実際は全てのmeta_keyを移行するのは現実的ではありません。そこはWordPress全体の仕様を解析した上で、ユーザーが持つべきデータを精査する必要があります。プロジェクト関係者とすり合わせをしていきましょう。

今回は初期データなので、項目が少ないですが、ある程度稼働しているサービスの場合だと、これがユーザー数 x meta_key (注意; あくまで単純計算) と増えていくはずです。


Laravel側にて行ったこと

以下に行ったことの内容をまとめてみます。


  • DB設計を行う

  • migrationファイルを作成して、migrationを実行する。

  • オリジナルのmigrationコマンドを作成して実行する


DB設計を行う

こちらに関しては移行後のシステムにおけるDBの設計を行います。手法については他に良い文献がいくらでもあるのでご自身で色々と調べてみてください。今回は理想のDB構成を目指すうえで失敗したこと、やっておけばよかったことをまとめます。


失敗談

その1.当初テーブル名の命名がゴチャゴチャになっていた。

チームメンバーに一目でテーブル名の意図が伝わらなかったことがありましたので、命名規則は守ったうえで、意図が伝わりやすい命名を意識することが大切です。

その2.テーブルのリレーション設定の考慮不足があり、手戻りがあった。

ユーザーテーブルの仕様において色々と情報を集約し過ぎてしまいました。テーブルの分割を行い、リレーションを設定するという作業を行なったため、余計な時間を割いてしまいました。

その3.カラムのデータ型をしっかりと設定しきれなかった。

カラムのデータ型に関して適当な部分を残してしまいました。

その4.テーブルやカラムの用途を明文化して文書化していなかった

設計することに手いっぱいになっており、文書を残していませんでした。。時間が経って、後から入ってきたメンバーにテーブル、カラムのことについて聞かれて際に、設計当初の意図や考えを思い出すことができなくなっていました。新メンバーに対して申し訳ない思いを現在進行形でしています。


大切なポイント

自分の失敗談をもとに考えてたことですが、デーブルやカラムの役割や目的をキチンと定義して言語化することが大切だと思います。ここを曖昧にすると後々に混乱を招きます。以下にDB設計時において、それぞれ意識するべき点をまとめてみました。


テーブル


  • 用途は何か?

  • どうして必要なのか?

  • 本当に必要なのか?

  • どうしてそのテーブル名にしたのか?

  • そのテーブル名は何を意図しているのか?

  • 他に適切なテーブル名はないのか?

  • 他のテーブルとどうしてリレーションするのか?

  • そのリレーションは本当に必要なのか?

  • 別のリレーションで事足りるのでないか?


カラム


  • 用途は何か?

  • どうして必要なのか?

  • 本当に必要なのか?

  • どうしてそのカラム名なのか?

  • どうしてそのデータ型なのか?

  • どこのテーブルのカラムと連携しているのか?

  • どうしてそこと連携している必要があるのか?


migrationファイル作成と実行

DB設計を形にしていきます。今回はサンプルとしてusersテーブルを作ります。

公式からの引用ですが、migrationコマンドを叩きます。

今回はサンプルとしてusersテーブルを作ります。

$ php artisan make:migration create_users_table --create=users

作成したファイルにカラムを定義します。


migration.php

<?php

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

class CreateUsersTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/

public function up()
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');

$table->string('display_name', 30);
// 表示名(文字列) 30文字までに設定

$table->string('pass_word', 50);
// パスワード(文字列) ハッシュ化を見据えて、余裕を持って50文字までに設定

$table->string('first_name', 30);
// 名前(文字列) アルファベットでの登録を見据えて、余裕を持って30文字までに設定

$table->string('last_name', 30);
// 苗字(文字列) アルファベットでの登録を見据えて、余裕を持って30文字までに設定

$table->string('email', 255);
// email(文字列) メールアドレスの設定が可能な最長の文字列に合わせる

$table->string('description', 200);
//ディスクリプション(文字列) ユーザーの自己紹介欄で使われることを想定。twitterより多少多く設定

$table->string('locale', 20);
// 市町村(文字列) 20文字あれば市町村を入れるのに十分と判断

$table->timestamps();
});
}

/**
* Reverse the migrations.
*
* @return void
*/

public function down()
{
Schema::dropIfExists('users');
}
}


前述をしていますが、どうしてそのカラムにしたのか。どうしてそのデータ型にしたのかはキチンと説明できる方が良いです。後々に仕様を見直したときに迷子になりません。今回の設定根拠はコード内にコメントとして記載しました。実際にはキチンと設定する必要があります。また、uniqueやindexも必要に応じて設定します。

そしてmgiration コマンドを叩きます。

$ php artisan migration

サンプルキャプチャはこちら

Screenshot from Gyazo


artisan commandを作り、WordPressからのmigrationコマンドを作る

さて、ここが味噌です。WordPressのDBを組み替える作業に入ります。

まずは、artisan commandを作ります。コマンド名はわかりやすいものにしてください。

$ php artisan make:command MigrateFromWordPress

その結果として、次の様なファイルが生成されました。ところどころに調整は加えています。


MigrateFromWordPress.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;

class MigrateFromWordPress extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/

protected $signature = 'migrate:wordpress';

/**
* The console command description.
*
* @var string
*/

protected $description = 'Migrate data from WordPress data base';

/**
* Create a new command instance.
*
* @return void
*/

public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/

public function handle()
{
// ここにゴリゴリとmigration処理を書いていく
}
}


そしてサンプルとして実装した結果がこちらです。


MigrateFromWordPress.php

<?php

namespace App\Console\Commands;

use DB;
use Illuminate\Console\Command;

class MigrateFromWordPress extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/

protected $signature = 'migrate:wordpress';

/**
* The console command description.
*
* @var string
*/

protected $description = 'Migrate data from WordPress data base';

/**
* Create a new command instance.
*
* @return void
*/

public function __construct()
{
parent::__construct();
}

/**
* Execute the console command.
*
* @return mixed
*/

public function handle()
{
// usermetaテーブルの縦持ちを横持ちに変更する処理
$userMetas = DB::table('wp_usermeta')->select(DB::raw(
"user_id,
max(CASE WHEN meta_key = 'nickname' THEN meta_value ELSE null END) AS nickname,
max(CASE WHEN meta_key = 'first_name' THEN meta_value ELSE null END) AS first_name,
max(CASE WHEN meta_key = 'last_name' THEN meta_value ELSE null END) AS last_name,
max(CASE WHEN meta_key = 'description' THEN meta_value ELSE null END) AS description,
max(CASE WHEN meta_key = 'locale' THEN meta_value ELSE null END) AS locale
"

))->groupBy('user_id')->orderBy('user_id', 'asc')->get();

// 横持ちに格納されているデータをusersテーブルに配置するためforeachで回す
foreach ($userMetas as $userMeta) {

// wp_usermetaテーブルのuser_idをもとにwp_usersテーブルから該当するデータを取得する
$wpUser = DB::table('wp_users')->find($userMeta->user_id);

// usersテーブルに必要なデータを格納する。
DB::table('users')->insert([
'id' => $wpUser->ID, // wp_usersのIDを流用
'email' => $wpUser->user_email, // wp_usersのuser_emailを流用
'pass_word' => $wpUser->user_pass,
'display_name' => $userMeta->nickname,
'first_name' => $userMeta->first_name,
'last_name' => $userMeta->last_name,
'description' => $userMeta->description,
'locale' => $userMeta->locale,
'created_at' => $wpUser->user_registered,
'updated_at' => $wpUser->user_registered,
]);
}
}
}


処理の流れとしては、


  1. wp_usermetaテーブルの持ち方を変更する

  2. wp_usermetaテーブルのuser_id毎に処理を回す

  3. wp_usersテーブルのテータをwp_usermetaテーブルのuser_id毎に引っ張る

  4. usersテーブルにwp_usermeta, wp_usersテーブルのデータを格納していく

といった感じになります。

そして artisanコマンドを叩きます。

$ php artisan migrate:wordpress

順調にいけば処理が終わります。エラーが出たら原因を探って対処してみましょう。

出来上がりのサンプルキャプチャはこちらです。

Screenshot from Gyazo

これにてusersテーブルは一旦完成です。


まとめ

今回はusersテーブルのmigrationに的を絞って記事にしてみました。本当はwp_posts、wp_commentsやwp_termsなど考慮が必要なことはいくつもあります。今回はサンプルなのでシンプルですが実際はとても複雑です。仕様を整理しながら対応していくことをお勧めします。


実際の作業を振り返って

今年の年明けから稼働しているNoSchoolのサービスを動かしながらDBの移行コマンドを作成していましたが、とてつもなく地味な作業でした。正直今思えば無駄な実装をしていたなとか、もっとうまく実装ができたなと思うことがありました。しかしながらなんとかやり遂げることができたので、大きな達成感を感じています。もう一度やれと言われたら正直やりたくはありませんが、DBの設計を意識する良い経験となりました。もしご感想あればお気軽にtwitterにてご連絡ください。何かしらWordPressのDB移行のアドバイスはできるかもしれません。