読者です 読者をやめる 読者になる 読者になる

Wordpressの記事データをRailsのDBに移行する

目次

おおざっぱな流れ

大体の流れはこんな感じ。

  1. SQLでいじくっていい感じのCSVを作成
  2. mysqlでimportする

移行するデータ

以下のデータをWordpressからRailsのアプリケーションに移行します。

  • 記事データ
  • カテゴリのデータ
  • カテゴリと記事の関連付けデータ
  • タグのデータ
  • タグと記事の関連付けデータ
  • Primary Categoryの関連付けデータ
  • 画像データ
  • サムネイル画像と記事の関連付けデータ

1. CSVを抽出

ということで各データの抽出時に気をつけたことなど。

記事データ

wp_postsCSVで抽出。

この際、revisionやattachmentが混ざらないようにpost_typepostであるもののみを抽出してexport。

SQLは例えばこんな感じ。

SELECT ID AS id, post_title AS title, post_content AS body_source, post_date AS published_at, post_status AS status
FROM  `wp_posts` 
WHERE post_type =  "post" AND post_status="publish";

カテゴリ

wp_termsをexportします。wp_termsはカテゴリとタグどちらも扱うテーブルなので、そこに注意します。

wp_termsの中身がなんなのか、という関係性は wp_term_taxonomy というテーブルで管理されてます。

JOINしてwp_term_taxonomy.taxonomyがcategoryのものだけとってきます。

SELECT wp_terms.term_id AS id, wp_terms.name AS name, wp_terms.slug AS name_en
FROM wp_terms
JOIN wp_term_taxonomy ON wp_terms.term_id = wp_term_taxonomy.term_id
WHERE wp_term_taxonomy.taxonomy = 'category';

wp_term_relationshipswp_termswp_postsの関連付けをしているのでそちらも。railsアプリの方のdb schemaに合わせてSQLをつくります。

SELECT object_id AS article_id, term_taxonomy_id AS category_id
FROM  `wp_term_relationships`;

タグ

タグは、カテゴリの移行とほぼSQLになります。

JOINしてwp_term_taxonomy.taxonomypost_tag のものだけとってきます。

SELECT wp_terms.term_id AS id, wp_terms.name AS name
FROM wp_terms
JOIN wp_term_taxonomy ON wp_terms.term_id = wp_term_taxonomy.term_id
WHERE wp_term_taxonomy.taxonomy = 'post_tag';
SELECT object_id AS article_id, term_taxonomy_id AS tag_id
FROM  `wp_term_relationships`;

Primary Category

ついでに、yoastというプラグインでPrimary Categoryというものを設定していたので、そちらもimportします。

primary categoryの管理方法は以下の質問が参考になりました。

php - How to get primary category from DB tables in wordpress? - Stack Overflow

以下のようにすれば、記事のIDとprimary categoryのidだけを取り出すことができます。

SELECT post_id AS id, meta_value AS primary_category_id 
FROM `wp_postmeta` 
WHERE meta_key="_yoast_wpseo_primary_category";

画像データ

画像のデータは、これまでにuploadされたものに関してはWordpressでuploadしたURLが挿入されるので、そのURLの整合性を保てるように移動します。

具体的には、 /wp-content/uploads/ ディレクトリをそのままRailsアプリの public に移します。

そうすると、例えば以下のようなURLの画像が、そのまま整合性を保ったまま新しいアプリケーションでも使用することができます。

http://my-media.com/wp-content/uploads/2016/08/image-sample.jpg

サムネイル

サムネイル画像は、後々そのままWordpressでuploadしたURLが挿入することを考慮して、そのまま画像のデータをURLの整合性を保てるようにRailsのpublicディレクトリの静的ファイルに移行します。

ということで、記事に対応するサムネイルのURLをstringで取り出すことを目指します。

Wordpressfeatured_imageの構造がちょっとややこしかったです。以下が分かりやすかった。

Where is the post featured image link stored in the WordPress database? - Stack Overflow

The featured image ID is stored in wp_postmeta with a meta_key called _thumbnail_id. The actual thumbnail link is then contained in wp_posts with a post_type of attachment.

ということで、wp_postmetameta_key_thumbnail_idのものだけを抽出すればよさそうですね。

その中のpost_idmeta_valueが、記事とサムネイルのrelationになってます。(サムネイルの画像はwp_postsに、post_typeattachmentのデータとして保存されています。)

僕の場合は以下のような感じでwp_posts内の記事とサムネイルだけのrelationを取り出しました。

SELECT wp_postmeta_relation.ID AS id, wp_posts.guid AS featured_image
FROM (
    SELECT ID, meta_value
    FROM wp_posts AS wp_posts1
    JOIN wp_postmeta ON wp_posts1.ID = wp_postmeta.post_id
    WHERE wp_postmeta.meta_key =  "_thumbnail_id"
) AS wp_postmeta_relation
JOIN wp_posts ON wp_postmeta_relation.meta_value = wp_posts.ID;

ということで、これでほしいCSVファイルを全てdumpできました。

2. mysqlでimportする

あとはこれらをMySQLでimportすれば作業完了です。

【MySQL】CSVファイルをインポートするコマンド - Qiita

以下のようなエラーが出たときは、

The used command is not allowed with this MySQL version

下のとこを参考にしてみてください。

MySQL: Enable LOAD DATA LOCAL INFILE - Stack Overflow

記事データ

mysql> LOAD DATA LOCAL INFILE '~/wp_posts.csv' INTO TABLE articles FIELDS TERMINATED BY ',' ENCLOSED BY '"' (id, title, body_source, published_at, status);

カテゴリデータ

mysql> LOAD DATA LOCAL INFILE '~/wp_terms_categories.csv' INTO TABLE categories FIELDS TERMINATED BY ',' ENCLOSED BY '"' (id, name, name_en);

記事とカテゴリの関連付けデータ

mysql> LOAD DATA LOCAL INFILE '~/wp_term_category_relationships.csv' INTO TABLE category_relations FIELDS TERMINATED BY ',' ENCLOSED BY '"' (article_id, category_id);

タグデータ

mysql> LOAD DATA LOCAL INFILE '~/wp_terms_tags.csv' INTO TABLE tags FIELDS TERMINATED BY ',' ENCLOSED BY '"' (id, name);

記事とタグの関連付けデータ

mysql> LOAD DATA LOCAL INFILE '~/wp_term_tag_relationships.csv' INTO TABLE tag_relations FIELDS TERMINATED BY ',' ENCLOSED BY '"' (article_id, tag_id);

Primary Category

Tagのようにひとつの記事に対して複数のCategoryを設定できるようになっていて、パンくずなどにも用いるためのprimary categoryも同時に設定できるようにしています。

こちらは、articlesのテーブルにprimary_category_idというカラムがあるみたいな構造なので、一度temporaryのtableをcreateして、そちらを用いてarticlesのレコードをupdateしていきます。

mysql> CREATE TABLE temp_table (article_id int(10), primary_category_id int(10));

つくったtableにimportします。

mysql> LOAD DATA LOCAL INFILE '~/wp_postmeta_primary_category.csv' INTO TABLE temp_table FIELDS TERMINATED BY ',' ENCLOSED BY '"' (article_id, primary_category_id);

INNER JOINを使ってUPDATE文を書きます。

mysql> UPDATE articles
INNER JOIN temp_table ON temp_table.article_id = articles.id
SET articles.primary_category_id = temp_table.primary_category_id;

そしたらさきほどのtableを削除します。

mysql> DROP TABLE temp_table;

参考:

mysql - Import CSV to Update rows in table - Stack Overflow

サムネイル画像

こちらもPrimary Categoryと同じような方法で移行していきます。

mysql> CREATE TABLE temp_table (article_id int(10), featured_image varchar(255));

つくったテーブルにimportします。

mysql> LOAD DATA LOCAL INFILE '~/wp_featured_images.csv' INTO TABLE temp_table FIELDS TERMINATED BY ',' ENCLOSED BY '"' (article_id, featured_image);

INNER JOINを使ってUPDATE文を書きます。

mysql> UPDATE articles
INNER JOIN temp_table ON temp_table.article_id = articles.id
SET articles.featured_image = temp_table.featured_image;

そしたらさきほどのtableを削除します。

mysql> DROP TABLE temp_table;

参考サイト

データベース構造 - WordPress Codex 日本語版

データベースの基本構造とWordPressのテーブル設計に見るデータモデリング | 株式会社LIG

Googleのモバイルフレンドリーテストのデバイス判定が適切にされないとき

Googleのモバイルフレンドリーテストの際に、mobile用のViewが適切に読み込まれていないことに気付きました。これはページランクに影響が出る可能性大なので早急に解決すべき問題。

k0kubunさんのrack-user_agentを使って、RailsのAction Pack Variantsを用いてViewリソースの出し分けを行っている状態で、この問題が起きました。

検索してもあんまりヒットしなかったので記事にしたら助かる人がいるかなと思ったので記事にしてみます。

検索してみると、以下の記事で同じ現象が。

www.suzukikenichi.com

ということで、User Agentの判定ミスかなと思い設定を見てみましたが、GoogleのロボットのUser Agentが分からない(そして変わる可能性もあるとGoogleが公式ページで言及していた。と思う。)ので、そちらからこれ以上できることはなさそうだなと思いました。

すべての Googlebot ユーザー エージェントは、自身を特定の携帯端末と見なすため、そうした携帯端末の場合とまったく同様に扱う必要があります。たとえば、スマートフォン用 Googlebot が自身を iPhone と見なしている場合は、iPhone ユーザーに対するのと同じレスポンス(リダイレクト、最適化されたコンテンツなど)を返す必要があります。 https://developers.google.com/webmasters/mobile-sites/mobile-seo/dynamic-serving?hl=ja

GoogleクローラのUser Agentなどの情報。

Google クローラ - Search Console ヘルプ

うーん、どうしようかなぁと考えていたら以下のページにたどり着く。

動的な配信  |  Mobile Friendly Websites  |  Google Developers

ということで、Googleの提示するVary HTTP ヘッダーを用いて対応してみます。

以下の記事が参考になったので、Vary HTTPヘッダーて何?て方は読んでみとくのがいいと思います。

web-tan.forum.impressrd.jp

ということでnginxでgzipのvary http headerの設定をonにします。

Set Vary: Accept-Encoding Header (nginx) - Stack Overflow

nginxでのvaryヘッダーに関しては以下も参考に。

qiita.com

これでgoogleの推奨する設定になったかと思います。

余談

gzip圧縮できてるかどうか確認する方法。

qiita.com

関連

totutotu.hatenablog.com

React x Railsでデータの同時編集ロック機能を実装する

検討

競合する可能性が大いにあり、かつデータの整合性が非常に重要な部分の実装なので、今回の場合は楽観的ロックではつらそう。では悲観的ロックではどうか。

悲観的ロックとは、

悲観的ロックとは、DBMSの行ロック機能(SELECT FOR UPDATE句)を利用して、並行した更新作業を制限する方法です。

ロックはレコード取得時にかかるので、同時にレコードを取得しようとした場合は他方がロック解除になるまで待機するため、同時に同じレコードを取得できないようになります。

via: http://kray.jp/blog/activerecord%E3%81%A7%E8%A1%8C%E3%83%AD%E3%83%83%E3%82%AF%E3%82%92%E3%81%8B%E3%81%91%E3%82%8B%E6%96%B9%E6%B3%95/

今回ロックしたいデータはReact componentからAjaxAPIを叩いているので、その点を考慮しながら悲観的ロックの実装を行う必要がある。

だけど、今回はロックをかけたいレコードがモデルをまたがるような構造になっているので、行ロックをテーブルをまたがって複数かけるひつようがありそうだ。

あと、SELECT FOR UPDATEが解除されるのがどのタイミングか調べてもいまいちわからなかったので、updateしたタイミングだとしたらユーザがupdateせずブラウザを離れる場合などもあり得るので、ちょっと怖いなとなった。

そして、そのページの中で、さらに紐付いてるデータも独立してばんばんcreateされることも考慮しないといけない。。。これは行ロックでは実現できない。

うーーんどうするか。。。

そこで今回は、ボタンをおした後に他のトランザクションが走っていることに気づく、という仕様ではなく、そもそも他の人の編集中には#editアクションのpathにアクセスできない、という仕様にすることにしました。

ですが、こちらの方法にも課題が合って、Webの仕組みというかさがというか、クライアントがそのページを離れたかどうかって、どうしてもWebの世界の仕組み上確証をとれないんですよね。。。

beforeunloadだってメッセージを表示するために特化した関数なのでそれでごちゃごちゃしようとした場合かなりブラウザごとに挙動の違いが出てくるみたいだし、そもそもPCの電源が落ちたときなど、このイベントさえ呼ばれないということだって全然ありえるだろうし。。

と、どうしようか悩んだのですが、Wordpressがこの方式を取っていることに気付き、ソースコードWordpressのミラー版がGitHubに公開されていたので参考にしてみることにしました。以下のコードあたりがそれっぽい。

https://github.com/WordPress/WordPress/blob/493f76a3d2ef8c030ab5dcd4333f9a401208f534/wp-admin/js/post.js#L156

heartbeatって名前が秀逸だな。。。

30秒ごとに脈を打って編集情報を更新してるみたい。

この方法とてもよい!と思い、この方法をRails + Reactで実装してみることにしました。

実装

今回の想定は、記事データを扱うPostというモデルの同時編集をロックするというもので実装していきます。Reactを使います。

編集情報を扱うモデルを作成

まずは編集情報を扱うPostUpdatingというモデルを作成します。

$ bin/rails g model PostUpdating user_id:integer post_id:integer

そしたら、PostとPostUpdatingは1:1の関係になるように各モデルのファイルとspecを実装しておいて下さい。(手抜きすみません。。)

APIを実装

続いてこのモデルのAPIを実装します

$ bin/rails g controller Api::V1::PostUpdating
class Api::V1::PostUpdatingController < ApplicationController
  def show
    post_updating = PostUpdating.find_by(id: params[:id])
    render json: post_updating
  end

  def update
    post_updating = PostUpdating.find_by(id: params[:id])

    # Update updated_at
    post_updating.touch

    if post_updating.update_attributes(post_updating_params)
      render json: post_updating, status: :ok
    else
      render json: post_updating.errors, status: :unprocessable_entity
    end
  end

  private

  def post_updating_params
    params.require(:post_updating).permit("user_id")
  end
end

ひとつつまずいたのは、Railsインスタンスが更新されていなかったらupdate_attributesなどでsaveされてもupdated_atを更新しない、ということでした。

これは post_updating.touch など、touchを使うことで解決できます。

React Componentを実装

setIntervalを使えば自動的にPostUpdatingの更新情報をupdateすることができそうです。

ReactでのsetIntervalの実用例はこちらとか参考になりました。

http://stackoverflow.com/questions/36299174/setinterval-in-a-react-app

const YourComponent = React.createClass({
  ...
  componentWillMount() {
     var result = this.check_if_multiple_user_editing();
     if(result == false){
       this.heartbeat();
       // 30 seconds
       setInterval(this.heartbeat, 30000);
     }
  },

  // 記事の更新情報をupdateする
  heartbeat() {
    var result = $.ajax({
      url: "/your/api/url/" + this.props.post_updating_id + "/",
      dataType: 'json',
      type: 'PATCH',
      timeout: 10000,
      data: { post_updating: { user_id: this.props.user_id, updated_at: Date.now() } },
      success: function(result) {
        // console.log("throb!"); // debug
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    }).responseJSON;
    return result;
  },

  fetchPostUpdating(post_updating_id) {
    var result = $.ajax({
      url: "/your/api/url/" + post_updating_id + "/",
      async: false,
      dataType: 'json',
      type: 'GET',
      timeout: 10000,
      success: function(result) {
        // console.log("Success");
      }.bind(this),
      error: function(_xhr, status, err) {
        console.error(this.props.url, status, err.toString());
      }.bind(this)
    }).responseJSON;
    return result;
  },

  check_if_multiple_user_editing() {
    var result = this.fetchPostUpdating(this.props.post_updating_id);
    editing_date = new Date(result["updated_at"]);
    now = Date.now();
    var diff = now - editing_date.getTime();
    // 秒
    var diff_second = diff / 1000;

    // 10秒以内にheartbeatにより更新されていて、編集ユーザが異なる場合は
    // 警告を出して前のページにリダイレクト
    if(diff_second < 10 && this.props.user_id != result["user_id"]) {
      alert("このページは他ユーザによって編集されています");
      location.href = "/go/ahead/your/favorite/url";
    }
  },
  
  ...

  render: function() {
    return (
      <div>
        <YourComponent />
      </div>
    );
  }
});

これで、このComponentをmountしてるViewで、他の人が同時にアクセスすることを防ぐ機能を実装できました。わーーーい。

参考資料

楽観的ロックと悲観的ロック - Qiita

Rails4で楽観的ロックを実装する - Rails Webook

Rails4で悲観的ロックを実装する - Rails Webook

Railsで悲観的ロックできないの? - babie steps