この記事はMovable Type Advent Calendar 2015 - Adventar20日目の記事のはずでした。忘れてました。ごめんなさい。
ふと思い立って現段階の最新のRailsでMTのデータを扱う実験をしてみました。
サンプルコード
takeyuweb/mtrails-example · GitHub
ActiveRecordを用いた例
検索
# 公開中で # ウェブサイトの記事(blog_parent_id = null)で # 記事本文にtarget_letterを含む MT::Entry .release .where(blog: MT::Blog.where(blog_parent_id: nil)) .where('entry_text like ?', "%#{target_letter}%") # Blog ID=1の記事 MT::Entry.where(entry_blog_id: 1) # Blogオブジェクトの関連でも MT::Blog.find(1).entries # さらに公開中で絞り込んでみたり MT::Blog.find(1).entries.release # 先頭の記事 MT::Entry.first
Arelを使ってカスタムフィールドdata1の値が100~200の記事を検索
entry_meta = MT::Entry::Meta.arel_table cond1 = MT::Entry .joins(:meta) .where(entry_meta[:entry_meta_type].eq('field.data1') .and(entry_meta[:entry_meta_vchar_idx].in(100..200))) # 「公開中」の条件とマージ MT::Entry.release.merge(cond1)
データのマッピング
entry = MT::Entry.first # 関連 entry.blog # => #<MT::Blog blog_id: 1, ...> entry.category # => #<MT::Category category_id: 1, ...> entry.categories # => #<ActiveRecord::Associations::CollectionProxy [#<MT::Category category_id: 1 ...>, ...]> entry.author # => #<MT::Author author_id: 1, ...> # カスタムフィールド entry.meta # => #<ActiveRecord::Associations::CollectionProxy [#<MT::Entry::Meta entry_meta_entry_id: 12, entry_meta_type: "field.data1", ...>, ...]> entry.meta.pluck(:entry_meta_type) # => ["field.data1", "field.data2", "revision"] # 属性値 entry.entry_title # => "TITLE" entry.entry_status # => "release"
実装のポイント
他(MT)のDBへの接続
ActiveRecord::Base.establish_connection
今回は、config/initializers/mt.rbで設定し、MT::Object
をincludeしたModelで適用されるようにしました。
https://github.com/takeyuweb/mtrails-example/blob/master/config/initializers/mt.rb
require_dependency 'mt/object' Rails.application.config.to_prepare do MT::Object.connection_configuration = { adapter: 'mysql2', encoding: 'utf8', host: 'localhost', username: 'root', database: 'mt62', socket: '/var/lib/mysql/mysql.sock' } end
※database.ymlに記載した設定を使うには、:mt
のように指定すればOKです。
https://github.com/takeyuweb/mtrails-example/blob/master/app/models/mt/object.rb
require_dependency 'mt' module MT::Object def self.connection_configuration=(spec) @connection_configuration = spec end def self.connection_configuration @connection_configuration end def self.included(klass) klass.class_eval do self.table_name_prefix = 'mt_' self.table_name = ['mt', name.split('::').last.underscore].join('_') self.primary_key_prefix_type = :table_name_with_underscore self.establish_connection ::MT::Object.connection_configuration end end end
※MT::Objectを親クラスにして継承しないのは、ActiveRecordクラスの継承を行うと子クラスでtable_nameを設定しても、QueryInterfaceで親クラスのtable_nameが使われたりと、STI前提の感じでうまく動かなかったためです。
MTのテーブル名規約
ActiveRecord::Base.table_name=
includeされたときに、対象のクラス名からmt_entry
のようなテーブル名を設定するようにしました。
MTの主キー名規約
ActiveRecord::Base.primary_key=
ActiveRecord::primary_key_prefix_type = :table_name_with_underscore
のどちらか。
includeされたときに、対象のクラス名からentry_id
のような主キーを設定するようにしました。
記事の公開状態
MTではentry_statusの値により、記事の公開状態(公開、下書きなど)を決定します。
https://github.com/takeyuweb/mtrails-example/blob/master/app/models/mt/entry.rb
enum entry_status: { hold: 1, release: 2, review: 3, future: 4, junk: 5, unpublish: 6 }
こうしておくことで、
entry.status # => "release" entry.status = "review"
のようにステータスを人間に読みやすい形で取得・設定したり、
entry.repease! # => 公開で保存、エラーで例外送出
のようなメソッドや
MT::Entry.release # => 公開中の記事を検索
のようなスコープがメタプログラミングにより自動的につくられます。
Rails標準でないbelongs_to
記事のブログはentry_blog_id
でとれます。これを使って、ActiveRecordのbelongs_to関連を設定します。
belongs_to :blog, class_name: 'MT::Blog', foreign_key: 'entry_blog_id'
foreign_keyで外部キー名を指定します。belongs_toの場合、自分のテーブルのカラムになります。
また、クラス名もRailsの規約からははずれているので、class_nameで設定しておきます。
entry.blog # => #<MT::Blog blog_id: 1, ...>
Rails標準でないhas_one,has_manyと多対多(has_many :through)
記事のカテゴリはmt_placement
を中間テーブルとした多対多関連になります。
メインカテゴリについては中間テーブルのplacement_is_primary
を見ればOKです。
これをActiveRecordで設定するにはこんな感じになります。
# メインカテゴリ has_one :placement, ->{ where(placement_is_primary: 1) }, class_name: 'MT::Placement', foreign_key: 'placement_entry_id' has_one :category, class_name: 'MT::Category', through: :placement, source: :category # カテゴリ(メインカテゴリ含む) has_many :placements, class_name: 'MT::Placement', foreign_key: 'placement_entry_id' has_many :categories, class_name: 'MT::Category', through: :placements, source: :category
中間テーブルはforeign_keyで外部キー名を指定すると共に、has_many :throughの方で、中間テーブルのどの関連を使うかをsource
で指定します。
実際にsource
がどんな関連なのかは、MT::Placement
で設定しています。
https://github.com/takeyuweb/mtrails-example/blob/master/app/models/mt/placement.rb
class MT::Placement < ActiveRecord::Base include MT::Object belongs_to :entry, class_name: 'MT::Entry', foreign_key: 'placement_entry_id' belongs_to :category, class_name: 'MT::Category', foreign_key: 'placement_category_id' end
ActiveRecord Modelごとに存在するActiveRecord Model
MTのメタデータやカスタムフィールドのデータは、対象のテーブルごとに別々のテーブルやカラム名で存在します。(記事ならmt_entry
に対するmt_entry_meta
)
個別にモデルクラスを作っても良いのですが、そうすると対応オブジェクトを増やすごとに重複コード増えていくので、メタプログラミングの絶好の機会です。
カスタムフィールドに対応するモデルクラスで、MT::Meta
をincludeすると、Model名::Meta
というモデルクラスを作り、必要な関連を設定するようにします。
https://github.com/takeyuweb/mtrails-example/blob/master/app/models/mt/entry.rb
class MT::Entry <ActiveRecord::Base include MT::Object include MT::Meta # <= (!)
MT::Meta
はこのような具合です。
module MT::Meta def self.included(klass) meta_class = Class.new(ActiveRecord::Base) klass.const_set('Meta', meta_class) meta_class.table_name = [klass.table_name, 'meta'].join('_') meta_class.class_eval do establish_connection ::MT::Object.connection_configuration end klass.class_eval do has_many :meta, class_name: [name, 'Meta'].join('::'), foreign_key: [name.split('::').last.underscore, 'meta', primary_key].join('_') end end end
MT::EntryでMT::Metaをincludeすると、以下を機械的に行います。
- ActiveRecordモデルクラス
MT::Entry::Meta
をつくり - そのテーブルとして
mt_entry_meta
を設定 MT::Entry.meta
としてメタデータの関連を設定
以上、かんたんにですが、とりあえずDBに接続して必要そうなデータを検索したりできるようになりました。 もっとも、実際に使えるものにするには、たとえばカスタムフィールドの検索をもっとわかりやすく書けるようにしたり、バリデーションを考えたり、ということが必要ですけれど・・・。
これで、僕たちRails使いが、MT::Objectに悩まされず、QueryInterfaceやArelによる条件組み立て、条件のマージなど馴染み深い方法で行えるようになります。