Archive for May 2008
Integerにする方法はないのかな?
どうしてかちゃんと調べようとしたところで昨晩はダウン。今週は仕事が忙しいのでさすがに疲れ気味です。
group byしてcount取れば行けるのはSQLレベルでは確認したのですが,なかなかRailsでうまく行きません。生成したSQLを見るとselectにcountが入っていません。
さらに理由を調べると,どうやら:includeオプションを使うと:selectは使われないそうな。:includeは結構膨大なテーブル検索をかけるのでこれも効率悪そうなので,:joins使って実現することにします。テーブル名を直接指定しないといけないのでちょっと面倒ですが。
がんばれ>自分
とはいってもまだOpenID自体生半可にしか理解していません。例えばYahoo!ジャパンのOpenIDにだけ対応して,そのサイトにユーザーがアクセスしにくると,Yahoo!でログインしているかどうかを調べて,自動的に自分のところもログインするなんてことはできるのでしょうか。
なんでそんなことを考えているのかというと,今プライベートでYahoo!グループ(昔のeGroups)を使っているのですが,これだけだと機能が低いので,自サイトのサービスを使って拡張できないか,ただそのときに新たに認証登録などはしないで済ませられないかと思っているのです。コンピュータ・リテラシがあまり高くない人も結構いるので,今のOpenIDのサイトみたいにギークでないと使えないようなものは無理。なるべく自動的にしてあげないといけないのです。
中間テーブルのTagCategoryでは次のように取り扱います。
Item側ではitemオブジェクトに対してitem.categoriesとすればカテゴリ一覧を,item.tagsとすればタグ一覧を表示するために条件を加えてやります。
2行目で:source=>:categoriesとしているのはTagCategoryの belongs_to:categoiesを参照しなさいという意味。
これで :conditionsのところの条件でカテゴリとタグを分けます。
という部分です。どうしても上位互換にするなら,これらのテンプレート変数が来たときは,検索してトップに来たカテゴリーを代表とするとかってことにする手もありますが,どうですかねえ(こうすればシングル・カテゴリーで使っている人の互換性は一応保たれます)。昔考えたときはitemテーブル内のicatをデフォルト・カテゴリという扱いにする,といった実装も考慮していたと思いますが,あまり前向きでないので,それは避けたいところ。
とりあえず,MultipleCategories使っている人はこの部分をそのままにしておくはずはないだろうから,MultipleCategoriesというテンプレート変数をそのまま使ってしまって,というのが当面の実装になりそうです。
で,肝心なことは,複数カテゴリが標準のときのテンプレートにおけるカテゴリー指定はどう書くのがいいか,ということ。<%categories%>とかってテンプレート変数を作って,それが呼ばれたときにテンプレート内の専用の項目を見に行くとかっていうのがNucleusの今の仕様に一番近い形ですが,セパレータの指定をどう書くかなど,ちょっと悩ましい部分があります。
あとは,これまで使用を封印してきたネスト付きパーサで,テンプレート内テンプレートをやらせてしまう,というのもあるけど,やっぱり今のモデルと極端に違うものを加えるのは良くないだろうなあ。
ということに気付いたのは,あるブログに属するカテゴリー一覧を取得するとき。実はこれまでCategoryクラスの方でget_listというメソッドを作ってブログオブジェクトをパラメータに取得するようにしていました。こうしておくと,ブログが指定されないときの一覧やブログが複数あるときの一覧とかも同じメソッドで取れるというのがメリットなのですが,見直してみるとCategory.get_list ... と入るのはあまりきれいじゃないです。やはり blog.categories と書いた方がスマート。
これだと順序付けができないのかと思って,この記述を使っていなかったというのもあるのですが,APIを見たら has_many のリレーション指定の中で :order を使って指定できることが分かったので,今回はそちらに切り替えました。
ただ,しっくりこない面がないわけではありません。これだとBlogクラスの中でCategoryのテーブルの内部情報を使って :order のところを書くことになるため,Categoryの実装に完全に依存してしまいます。Categoryの内部構造が変わったらBlogクラスも変えなければいけないのでメンテナンス性が悪くなります。
どうするのがベストなんでしょう。
詳しくは後日説明しますが,大部分は2年前にNucleusにこれらの機能を入れようとしたときのものをベースにしています(コードそのものはもう忘れたので別物ですが,考え方など)。
カテゴリー・テーブルを拡張してツリー型の管理およびタグの管理ができるようにします。ツリー管理ではNested Setと呼ばれる,SQL1回の呼び出しで子ツリーを全部取り出せる方法を取ります(説明はこちらの方が分かりやすいかも)。
ツリー構造はブログごとに持ちます。また,タグもブログごとに持ちます。これはタグからカテゴリーへの変換やその逆が楽にできるようにするためです。
タグの場合はブログ横断で検索したいケースもあると思うので,複数のタグが共通の「親タグ」(あるいは「別名タグ」)を参照するようにします。例えばブログ1に「Rails」というタグがあって,IDが10だとします。ブログ2にも「Rails」というタグがあってIDが20だとします。このときにaliasidというフィールドはどちらも10になります(別にどちらも20にしても構いません。共通であればいいだけなので)。このaliasidのフィールドを使うとブログを串刺しにしたタグ検索が可能です。
アイテムとタグ,カテゴリーを紐付けるために新しいテーブルを作ります。ここにはアイテムのIDとタグ・カテゴリーのIDがカテゴリーやタグの数だけ入ります。このテーブル上ではタグとカテゴリーの区別はなく,あくまでもカテゴリー・テーブルにおける違いだけになります。
RailsのActiveRecordにおいては,このテーブルを介してアイテムと,タグ・カテゴリーが多対多のリレーションを持つことになります。このとき「has and belongs to many(HABTM)」あるいは「has_many :through」という二つの方法があり,前者では中間テーブルはRailsが管理し,ActiveRecordで直接いじることはできません。ここでは自由度が高い後者の方法を使っています。
裏方仕事は多少ありますが,それは当然のこと。
MultipleCategoriesのコンバート部分も基本的にはできたはず。両方できてからじゃないとチェックするのが面倒なのでまだ動かしていませんが。
というわけでルート定義は次のような簡単なもの
map.resources :plugins, :member_path => '/plugin/:permalink'
plugins_controllerもいたってシンプル
delegateで委譲してしまっているので,ほとんどやることがありません。唯一パラメータを受け渡せるようにしておかないといけないので,FoodynPlugin::Mainクラスにparamsインスタンス変数を加えています。
attr_accessor :params
ちょっと注意しないといけないのはプラグインのメインの中ではidex, show, new, edit, create, update, destroyメソッドは外部から簡単に呼ばれてしまうことです。セキュリティホールになる恐れがあります。
プラグイン関係は,実装はまだまだやることがありますが,基本的な仕組みは全部できたので,マルチカテゴリーとタグのサポートを次に実装しようかと思います。
この前動かなかったのはなぜ?
というわけでeventのところはNucleusよりももうちょっと簡単に書けます。プラグインではevent~~というメソッドを用意するだけ。イベント一覧を返すメソッドがなくてもメソッド名を調べて登録します。呼び出し側もイベント名でnotifyするだけ。
プラグイン関係はあとactionが実行できるようになれば,一応一通り仕組みはできたことになります。
表示されていないだけで
countはちゃんと取れていました。ただし型がStringになる点に注意が必要です。Integerにする方法はないのかな?
:joinsに変えたら
SQLは正しく発行できているのを確認しました。でもオブジェクトとしてcountが取れていないみたい。どうしてかちゃんと調べようとしたところで昨晩はダウン。今週は仕事が忙しいのでさすがに疲れ気味です。
:includeと:selectは共存できない
まだTagCloudではまっているのですが,何かというとタグクラウドを作るためにはタグごとに使われているアイテム数を調べる必要があるのです。タグごとに調べるのであればActiveRecord使って簡単に書けますが,どうみても効率が悪そう。SQL文一発で実行したいのです。group byしてcount取れば行けるのはSQLレベルでは確認したのですが,なかなかRailsでうまく行きません。生成したSQLを見るとselectにcountが入っていません。
さらに理由を調べると,どうやら:includeオプションを使うと:selectは使われないそうな。:includeは結構膨大なテーブル検索をかけるのでこれも効率悪そうなので,:joins使って実現することにします。テーブル名を直接指定しないといけないのでちょっと面倒ですが。
TagCloudがめんどくさい
探せばタグクラウド作るプラグインくらい,あるんでしょうけど,下手な作り方をすると結構効率が落ちそうな気がするのでなるべく自前でやろうとしています。アルゴリズムが難しいわけではないのですが,クエリーが結構複雑なので,なかなか手が進みません。がんばれ>自分
OpenIDの使い方
最近,実験的なものが中心だとは思いますがOpenIDで認証可能なサービスがいくつか出てきました。今のところFoodynは自前の認証しかしていませんが,プラグインで拡張可能な形にはする予定です。そんなに難しくなくて,自前の認証の前にプラグイン用のイベントを用意しておき,そこで認証が行われたときは自前の認証はしない,というだけで済むはず。とはいってもまだOpenID自体生半可にしか理解していません。例えばYahoo!ジャパンのOpenIDにだけ対応して,そのサイトにユーザーがアクセスしにくると,Yahoo!でログインしているかどうかを調べて,自動的に自分のところもログインするなんてことはできるのでしょうか。
なんでそんなことを考えているのかというと,今プライベートでYahoo!グループ(昔のeGroups)を使っているのですが,これだけだと機能が低いので,自サイトのサービスを使って拡張できないか,ただそのときに新たに認証登録などはしないで済ませられないかと思っているのです。コンピュータ・リテラシがあまり高くない人も結構いるので,今のOpenIDのサイトみたいにギークでないと使えないようなものは無理。なるべく自動的にしてあげないといけないのです。
テンプレート表示
前回書いたようなモデル化までが済んでいれば,テンプレートでマルチカテゴリをサポートするのは簡単です。とりあえずテンプレートにと書いたときに展開するというだけのものをあげておきます。これをテンプレートのパースのクラスに置くだけでOKです。
def parse_multiplecategories params
temp = ""
separator = nil
item.categories.each do |cat|
temp += separator.to_s
separator ||= ','
temp += link_to( cat.cname, {:controller=>'categories', :catid=>cat.id.to_s, :action=>'show'})
end
return temp
end
カテゴリとタグのリレーション設定
Foodynではタグとカテゴリは1テーブルで管理し,どのアイテムがどのカテゴリ,あるいはタグに属しているかを中間テーブルを使って記す方法を取っています。Ruby on Railsではこれはhabtn(has and belongs to many)あるいはhas_many throughという方法で取り扱います。ここでは中間テーブルも明示的にいじれるように後者の実装を採用します。中間テーブルのTagCategoryでは次のように取り扱います。
class TagCategory < ActiveRecord::Base
set_base_name 'tag_category'
belongs_to :categories, :class_name=>'Category', :foreign_key=>'category_id'
belongs_to :items, :class_name=>'Item', :foreign_key=>'item_id'
endItem側ではitemオブジェクトに対してitem.categoriesとすればカテゴリ一覧を,item.tagsとすればタグ一覧を表示するために条件を加えてやります。
has_many :categories, :through => :tag_category, :conditions=>"cleft is not null"
has_many :tags, :through => :tag_category, :source=>:categories, :conditions=>"caliasid is not null"
has_many :tag_category
2行目で:source=>:categoriesとしているのはTagCategoryの belongs_to:categoiesを参照しなさいという意味。
これで :conditionsのところの条件でカテゴリとタグを分けます。
マルチカテゴリのテンプレート指定
マルチカテゴリを標準機能として採用すると,Nucleusのテンプレート・モデルと合わない部分が出てきます。具体的には<%catname%>や<%catlink%>といったテンプレート変数は意味がなくなります。例えばdefault/indexのテンプレート中における<a href="<%categorylink%>" title="Category: <%category%>"><%category%></a>
という部分です。どうしても上位互換にするなら,これらのテンプレート変数が来たときは,検索してトップに来たカテゴリーを代表とするとかってことにする手もありますが,どうですかねえ(こうすればシングル・カテゴリーで使っている人の互換性は一応保たれます)。昔考えたときはitemテーブル内のicatをデフォルト・カテゴリという扱いにする,といった実装も考慮していたと思いますが,あまり前向きでないので,それは避けたいところ。
とりあえず,MultipleCategories使っている人はこの部分をそのままにしておくはずはないだろうから,MultipleCategoriesというテンプレート変数をそのまま使ってしまって,というのが当面の実装になりそうです。
で,肝心なことは,複数カテゴリが標準のときのテンプレートにおけるカテゴリー指定はどう書くのがいいか,ということ。<%categories%>とかってテンプレート変数を作って,それが呼ばれたときにテンプレート内の専用の項目を見に行くとかっていうのがNucleusの今の仕様に一番近い形ですが,セパレータの指定をどう書くかなど,ちょっと悩ましい部分があります。
あとは,これまで使用を封印してきたネスト付きパーサで,テンプレート内テンプレートをやらせてしまう,というのもあるけど,やっぱり今のモデルと極端に違うものを加えるのは良くないだろうなあ。
設計とメソッド名と内部構造
どうも頭がまだ手続き型言語から抜けていないようで,メソッド名を付けるときに,ついつい「get~~」みたいに動詞を入れてしまいがちです。ということに気付いたのは,あるブログに属するカテゴリー一覧を取得するとき。実はこれまでCategoryクラスの方でget_listというメソッドを作ってブログオブジェクトをパラメータに取得するようにしていました。こうしておくと,ブログが指定されないときの一覧やブログが複数あるときの一覧とかも同じメソッドで取れるというのがメリットなのですが,見直してみるとCategory.get_list ... と入るのはあまりきれいじゃないです。やはり blog.categories と書いた方がスマート。
これだと順序付けができないのかと思って,この記述を使っていなかったというのもあるのですが,APIを見たら has_many のリレーション指定の中で :order を使って指定できることが分かったので,今回はそちらに切り替えました。
ただ,しっくりこない面がないわけではありません。これだとBlogクラスの中でCategoryのテーブルの内部情報を使って :order のところを書くことになるため,Categoryの実装に完全に依存してしまいます。Categoryの内部構造が変わったらBlogクラスも変えなければいけないのでメンテナンス性が悪くなります。
どうするのがベストなんでしょう。
週末に
タグとカテゴリーは表示系まで終わらせるつもりだったのですが,意外とデータ移行に手間取り,表示系に行きつきませんでした。残念!タグとカテゴリーのサポートについて
Foodyn CMSではツリー型のマルチカテゴリーとタグを標準でサポートします。機能的にはNucleusのMultipleCategoriesとTagEXと同等以上のものになるでしょう。詳しくは後日説明しますが,大部分は2年前にNucleusにこれらの機能を入れようとしたときのものをベースにしています(コードそのものはもう忘れたので別物ですが,考え方など)。
カテゴリー・テーブルを拡張してツリー型の管理およびタグの管理ができるようにします。ツリー管理ではNested Setと呼ばれる,SQL1回の呼び出しで子ツリーを全部取り出せる方法を取ります(説明はこちらの方が分かりやすいかも)。
ツリー構造はブログごとに持ちます。また,タグもブログごとに持ちます。これはタグからカテゴリーへの変換やその逆が楽にできるようにするためです。
タグの場合はブログ横断で検索したいケースもあると思うので,複数のタグが共通の「親タグ」(あるいは「別名タグ」)を参照するようにします。例えばブログ1に「Rails」というタグがあって,IDが10だとします。ブログ2にも「Rails」というタグがあってIDが20だとします。このときにaliasidというフィールドはどちらも10になります(別にどちらも20にしても構いません。共通であればいいだけなので)。このaliasidのフィールドを使うとブログを串刺しにしたタグ検索が可能です。
アイテムとタグ,カテゴリーを紐付けるために新しいテーブルを作ります。ここにはアイテムのIDとタグ・カテゴリーのIDがカテゴリーやタグの数だけ入ります。このテーブル上ではタグとカテゴリーの区別はなく,あくまでもカテゴリー・テーブルにおける違いだけになります。
RailsのActiveRecordにおいては,このテーブルを介してアイテムと,タグ・カテゴリーが多対多のリレーションを持つことになります。このとき「has and belongs to many(HABTM)」あるいは「has_many :through」という二つの方法があり,前者では中間テーブルはRailsが管理し,ActiveRecordで直接いじることはできません。ここでは自由度が高い後者の方法を使っています。
思ったより大変でない
前の記事で書いたTagEXデータのコンバートについてですが,どちらにしてもモデルで,そのタグがそのブログで新規かどうかのチェックなどをしないといけないので,コンバート自体についてはあまり深く考えなくてもそのメソッドを呼び出すだけでできそうです。裏方仕事は多少ありますが,それは当然のこと。
MultipleCategoriesのコンバート部分も基本的にはできたはず。両方できてからじゃないとチェックするのが面倒なのでまだ動かしていませんが。
TagEXデータのコンバート
TagEXのタグってブログごとになっていないということを発見。カテゴリーとタグをひとつのテーブルで管理する場合,ブログごとに分けておかないと面倒なのでチェックしないといけないなあ。結構大変かも。URLによるプラグインへのアクセス
プラグインの機能として,外部から呼び出すこと(Nucleusで言えばActionのところ)が必要なわけですが,一応RESTfulを意識して実装しておきました。以前から気になっていたresource_hacksプラグインを使うことでmap.resourcesに対して名前付きのアクセスができるようにしています。将来はほかの部分もこれを使って書きなおすかもしれません。というわけでルート定義は次のような簡単なもの
map.resources :plugins, :member_path => '/plugin/:permalink'
plugins_controllerもいたってシンプル
class PluginsController < ApplicationController
before_filter :find_plugin
delegate :index, :show, :new, :edit, :create, :update, :destroy, :to=>"@plugin.main"
private
def find_plugin
if !(@plugin = Plugins.find_by_plugin_name(params[:permalink]))
return false
else
@plugin.main.params = params
end
end
enddelegateで委譲してしまっているので,ほとんどやることがありません。唯一パラメータを受け渡せるようにしておかないといけないので,FoodynPlugin::Mainクラスにparamsインスタンス変数を加えています。
attr_accessor :params
ちょっと注意しないといけないのはプラグインのメインの中ではidex, show, new, edit, create, update, destroyメソッドは外部から簡単に呼ばれてしまうことです。セキュリティホールになる恐れがあります。
プラグイン関係は,実装はまだまだやることがありますが,基本的な仕組みは全部できたので,マルチカテゴリーとタグのサポートを次に実装しようかと思います。
動かなかった理由がよく分かりませんが
今日,もう一回イベントのところを動かしてみたら何の問題もなく動いていました。この前動かなかったのはなぜ?
というわけでeventのところはNucleusよりももうちょっと簡単に書けます。プラグインではevent~~というメソッドを用意するだけ。イベント一覧を返すメソッドがなくてもメソッド名を調べて登録します。呼び出し側もイベント名でnotifyするだけ。
プラグイン関係はあとactionが実行できるようになれば,一応一通り仕組みはできたことになります。