MovableType のデータベースを Ruby から使う + 脱線

Posted by
ぴろり
Posted at
2012/12/20 14:18
Trackbacks
関連記事 (0)
Post Comment
コメントできます
Category
MovableType カテゴリ
カバーイメージ

 この記事を Movable Type Advent Calendar 2012 の 20 日目に贈ります。

 MovableType の管理画面は Perl で書かれた CGI として動作しており、ウェブサイトやブログ記事などなど、様々なデータはその後ろにあるデータベースに格納されています。また、ダイナミック パブリッシング機能については PHP で書かかれていますので、MovableType において Perl および PHP によるデータベースへのアクセス手段が標準で提供されています。そのデータベースのテーブル構造も難しいものではありませんから、Perl、PHP 以外の他のプログラミング言語からも、MovableType のデータを容易に利用することができます。そこで今回、Ruby を用いてゴニョゴニョしてみました。

この記事を Delicious に追加する   このエントリーをはてなブックマークに追加  

O/R マッパの簡単な解説

 データベース上のデータ レコードを操作する際、一般的には SQL という言語が用いられます。SQL を用いれば、データに対してありとあらゆる操作が可能ですが、MovableType などのような実際のアプリケーションでは、そこまでプリミティブなデータ操作が必要なかったり、また、アプリケーション側で SQL を直接記述すると、可読性や保守性が著しく悪くなるなどの問題があります。
 そこで、アプリケーション プログラムからデータベースを操作する際には、O/R マッパー という仕組みを利用することがほとんどです。データベースに格納されたデータ レコードを、プログラム内からはあたかもオブジェクトの様に扱うことができるようになります*1。実際にデータベースにデータを格納したり、データを取得する処理は SQL によって行われるのですが、煩雑になりがちなこれらの手続きは、O/R マッパーが裏側で頑張ってくれるため、プログラムは SQL を全く意識する必要がなくなります。簡単な具体例を見てみましょう。

ブログ記事のタイトルを取得

$data = $dbh->selectrow_hashref('SELECT * FROM `mt_entry` LIMIT 1')
    or die 'No Data';
print $data->{entry_title};
$entry = MT::Entry->load()
    or die 'No Data';
print $entry->title;

ID=5 でブログ記事を新規保存または上書き保存

$r = $dbh->do('SELECT * FROM `mt_entry` WHERE `entry_id` = 5');
$r == 0
    # レコードが無かったら INSERT
    ? $dbh->do('INSERT INTO `mt_entry` (`entry_id`, `entry_title`) VALUES (5, \'まそっぷ!\')')
    # 存在したら UPDATE
    : $dbh->do('UPDATE `mt_entry` SET `entry_title` = \'まそっぷ!\' WHERE `entry_id` = 5');
$entry = MT::Entry->new;
$entry->id(5);
$entry->title('まそっぷ!');
$entry->save;

 コードの詳細な説明は省きますが、O/R マッパを利用したプログラムの方が、動作の意図を感覚的に簡潔に読むことができると思います。上記の短いサンプル コード程度ではあまり実感できませんが、データベース上のテーブル名やカラム名などの元のアイデンティファイアを、オブジェクト内に隠蔽することができるため、プログラムを簡潔に記述できることの他にも、データベース製品ごとの SQL 文法の違いを吸収してくれるなど、O/R マッパには様々なメリットがあります。

Ruby から MovableType のデータを取得する

 Ruby では ActiveRecord ライブラリを利用することで、簡単にこれを実現できます。

# 必要なライブラリを読込む
require "rubygems"
require "active_record"

# データベースに接続
ActiveRecord::Base.establish_connection(
   :adapter => 'mysql',
   :host => 'localhost',
   :username => 'root',
   :password => 'root',
   :database => 'mt_test51'
)

# 命名規則を調整
ActiveRecord::Base.pluralize_table_names = false
ActiveRecord::Base.table_name_prefix = "mt_"
ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore

# ブログ記事オブジェクトを定義
class Entry < ActiveRecord::Base
end

# 一番最初のブログ記事を取得
entry = Blog.find(:first)
puts(entry.entry_title)

# タイトルを書き換えて保存
entry.entry_title='まそっぷ!'
entry.save

 ActiveRecord を用いて MovableType のテーブルにアクセスする際に注意する点があります。"設定より規約"を重んじる ActiveRecord では—— Ruby on Rails でもそうですが——、規約に従う部分については、開発者がわざわざそれを明示的に指定する必要がありません。20 行目の Entry クラスは規約に従って勝手に(?) entries テーブルに接続されますし、25 行目の entry_title メソッドは同じく規約に従って勝手に entries テーブルの entry_title カラムへのアクセサとして定義されます。
 これは開発者が、Entry クラスを Recipe テーブルにわざわざ接続したり、entry_titlehogehoge カラムに当て嵌めたりする設計の方がむしろ例外的なケースであって、そのような"至極当たり前な"テーブル名やカラム名を開発者に考えさせたり、その稀有な例外のために定型的な設定を開発者にいちいち強いたりするくらいならば、『「クラス名=テーブル名、アクセサ名=カラム名」という規約に従って動作するぞ!』としてしまった方が、開発者にとってもハッピーだということです。そして、その規約から外れる場合にのみ、設定を行わせればよい。これはなかなか面白い考え方です。

 さて、実際に MovableType のテーブル設計は ActiveRecord の規約に則っていませんので、その例外を設定する必要があります。具体的に見てみましょう。

  • 15 行目 pluralize_table_names ... ActiveRecord のクラスが接続されるテーブル名は、そのクラス名から決定され、例えば、Product クラスは標準で products テーブルに接続されます。このフラグはテーブル名を複数形にしません。MovableType の場合、Entry クラスは mt_entry テーブルに格納されますので、false(複数形に変換しない)を指定します。
  • 16 行目 table_name_prefix ... ActiveRecord のクラスが接続されるテーブル名の接頭句を定義します。MovableType の場合、Entry クラスは mt_entry テーブルに格納されるので、mt_ を指定します。
  • 17 行目 primary_key_prefix_type ... ActiveRecord のレコードは主キーとして標準で id が定義されます。このフラグは、主キーにテーブル名の接頭句を追加します。MovableType の場合、Entry クラスの主キーは entry_id なので、この指定が必要です。

 このあたりの解説は Rubyist Magazine - RubyOnRails を使ってみる 【第 3 回】 ActiveRecord が詳しいです。

脱線 1. MovableType のテーブルって変かも?

 先のソースコードの 25 行目、28 行目に違和感がありませんでしょうか? Entry オブジェクトにタイトルを取得/設定している部分で、entry という文字列が重複しています。これは ActiveRecord の規約に因る部分でもありますが、そもそも MovableType のテーブルの命名規則って変かもしれません。

 まず、各テーブル名ですが、MovableType が利用するデータベース内に存在するテーブルは、基本的かつ当たり前には MovableType が利用するものなので、テーブル名の接頭句である mt_ は不要な気がしないでもありません。仮に MovableType のデータベースを MovableType 以外のシステムと共用することがあっても、それは例外的なことであって、接頭句が標準で必要な理由にはなり得ません。恐らくは最初の設計がそうであったのと*2、バージョンアップでデータベースを引き継げる必要性から、そのままズルズルと来てしまったんだと思いますケド...
 次に、テーブル内のカラム名についても同じことが言えます。mt_entry のタイトルを表すカラムは title であれば十分なのに、なぜか entry_title と定義されています。これが先の 25 行目、28 行目の違和感の理由です。そのため、mt_permission テーブルには permission_permissions カラムがあったりします(´・ω・`)くどいよ!

脱線 2. MovableType のテーブルを COOL にする!

 そんでもって、んじゃ、このくどい名前はどこで生成しているんだろう? ということを調べると...

sub db_column_name { $_[2] }
sub db_column_name {
    my $dbd = shift;
    my ( $table, $col ) = @_;
    return $1 if $col =~ m/\!(.*)\!/;

    $table =~ s{ \A mt_ }{}xms;
    return join( '_', $table, $col );
}

 おまえかー! ということで、オーバーライドしている db_column_name をコメントアウトしてやれば、素直な(?)カラム名でアクセスできるようになります。ざっと見た範囲で、普通に使っている範囲内では、MT::Object の後ろ側に手を突っ込んでカラム名を直接指定するようなコードは見つからなかったので動作すると思います。ただし、データベースに直接アクセスするようなプラグインや拡張は、作り様にも拠りますがたぶん動作しません。

 ...と言いますか、このお手軽ハックだけでは初期インストールとアップグレードが動作しない大問題があります。というのも、初期インストールやアップグレードの際に、テーブルやカラムを作ったりする処理において、カラム名を上記の db_column_name メソッドを使わずに自前で生成するようハードコーディングされている部分が散在しておりまして(´・ω・`) そう簡単には直せませんし、影響範囲もちょっとアレな感じです。例えば;

    return
          $field_prefix . '_' 
        . $name . ' ' 
        . $type
        . $nullable
        . $default
        . $key;
sub column_defs {
    ...
    my $field_prefix = $class->datasource;
    ...
        next if $colname !~ m/^\Q$field_prefix\E_/i;
        $colname =~ s/^\Q$field_prefix\E_//i;

 これらの部分を目grepで探してゴリゴリと修正したところ、何とか初期インストールも成功し、スマートなカラム名が生成されました。 ...という訳で、労力の割りにあんまり需要が無さそうですが、このあたりの動作も行儀良くして頂けると、他言語プラットホームへの展開もし易くなるのになぁ...と思うのでした。

 今回、Ruby の ActiveRecord に加えて、Java の Hibernate についても書くつもりだったんですが、環境作るのメンドクセーってなって書けませんでした( ´∀`)σ)Д`) MovableType のコンテンツ出力方向への hack も面白いですが、MovableType を/が司る根源であるデータベースを基点にして他方面に展開してみるのも面白いかもしれません。おしまい。

この記事を Delicious に追加する   このエントリーをはてなブックマークに追加  

  1. *1 オブジェクト関係マッピング - Wikipedia
  2. *2 古いバージョンのソースは確認していません

この記事を読んだ人はこんな記事も読んでいます記事リコメンデーションについて

カバー画像:西暦を和暦に変換する MovableType プラグイン:JapaneseYear

関連記事/トラックバック

関連記事/トラックバックはまだありません

この記事にトラックバックを送るには?

コメントを投稿する

 
 (必須, 匿名可, 公開, トリップが使えます)
 (必須, 匿名可, 非公開, Gravatar に対応しています)
 (必須)
スパム コメント防止のため「投稿確認」欄に ランダムな数字 CAPTCHAについて を入力してから送信してください。お手数ですがご協力のほど宜しくお願いいたします。