この記事はMovable Type Advent Calendar 2015 - Adventar20日目の記事のはずでした。忘れてました。ごめんなさい。
ふと思い立って現段階の最新のRailsでMTのデータを扱う実験をしてみました。
Railsで非RailsなDBを扱うサンプルにもなるかも。
サンプルコード
takeyuweb/mtrails-example · GitHub
検索
MT::Entry
.release
.where(blog: MT::Blog.where(blog_parent_id: nil))
.where('entry_text like ?', "%#{target_letter}%")
MT::Entry.where(entry_blog_id: 1)
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
entry.category
entry.categories
entry.author
entry.meta
entry.meta.pluck(:entry_meta_type)
entry.entry_title
entry.entry_status
実装のポイント
他(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の値により、記事の公開状態(公開、下書きなど)を決定します。
Rails 4.1以降であればenumが使えます。
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
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
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
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による条件組み立て、条件のマージなど馴染み深い方法で行えるようになります。