Twitter風のフォローする/されるの考え方
これまで作ってきたTwitter風シンプルアプリTwは、参加者全員の発言を全ての参加者の画面に表示していました。しかし実際のTwitterでは、画面に表示されるのは、自分の発言と、自分が「フォロー」している参加者の発言だけです。「 フォロー」とは「この参加者の発言を読む」と決めることです。注意すべきなのは、フォローした参加者の発言は読めるようになりますが、「 フォローされる」側ではそうはならないということです。お互いの発言が読めるようになるには、お互いが明示的にフォローする必要があります。
この参加者同士の関係をTwでも組み込んでみましょう。
関連(association)
Twでは一人のユーザが複数の発言を行います。したがってUserモデルには次の宣言がありました。
Userモデルに設定した関連
one_to_many : words , : class => Tw :: Models :: Word
これを「関連」と呼びます。Wordモデルには、これと逆の関連(many_to_one)が宣言されています。Ruby on RailsのActiveRecoredにもまったく同様の機能があり、Railsユーザになじみやすいように、Waves(のORパーであるSequel)ではbelongs_toやhas_manyといったメソッド名が使えるようになっています。
WavesのO/RマッパーであるSequelのサポートする関連には次の3種類があります。
one_to_many : 一対多
mant_to_one : 多対一
many_to_many : 多対多
フォローを関連に置き換える
Twitterのフォローは「あるユーザが別のユーザをフォローしている」という関連です。一人のユーザは何人のユーザでもフォローすることができますし、何人のユーザにフォローされることも可能です。つまりフォローする/されるは多対多の関連になります。
関連は異なるテーブル間で宣言されるケースが多いのですが、フォローでは「ユーザがユーザをフォローする/される」なので、自分自身に対して関連を宣言する必要があります。これを自己参照型多対多の関連と呼ぶことがあります。
交差テーブル(join table)
多対多の関連を作るには、関連する2つの(自己参照のときは1つの)テーブルの他に、交差テーブル(join table)と呼ばれるものが必要になります。交差テーブルは関連する2つのモデルのidの対応表です。今回はUserテーブル同士の対応をとるので、ユーザのidを別のユーザのidに対応付けるテーブルになります。
ActiveRecoredでは交差テーブルはidカラムを持ってはならないなどの制約がありますが、Sequelでは特に無いようです。
フォローする/されるの実装
交差テーブルを作る
交差テーブルを作るためにマイグレーションファイルを生成します。
交差テーブルのマイグレーション用のファイルを作る
rake schema:migration name=add_join_table
schema/migrations/003_add_join_table.rb(ファイル名先頭の数値はマイグレーションをファイルを作った回数に応じて変化するので003とは限りません)が生成されますので、以下のように修正します。
交差テーブルのマイグレーション用のファイル
class AddJoinTable < Sequel :: Migration
def up
create_table : followed_following do
foreign_key : followed_id , : table =>: users
foreign_key : following_id , : table =>: users
end
end
def down
drop_table : followed_following
end
end
交差テーブルの名前はfollowed_followingです。2つのカラムfollowed_idとfollowing_idを持ち、それぞれUserテーブルを参照しています。
次にこの交差テーブルを使って多対多を実装します。models/user.rbを以下のように修正してください。
自己参照型多対多の設定
module Tw
module Models
class User < Sequel :: Model (: users )
many_to_many : followed_users ,
: class => Tw :: Models :: User ,
: join_table =>: followed_following ,
: left_key =>: following_id ,
: right_key =>: followed_id
many_to_many : following_users ,
: class => Tw :: Models :: User ,
: join_table =>: followed_following ,
: left_key =>: followed_id ,
: right_key =>: following_id
one_to_many : words , : class => Tw :: Models :: Word
after_create do
set (: created_on => Time . now ) if columns . include ? : created_on
end
before_save do
set (: updated_on => Time . now ) if columns . include ? : updated_on
end
end
end
end
many_to_manyを宣言するわけですが、いくつかのパラメータ設定が必要になります。
関連の名前
many_to_manyの最初のパラメータ「:followed_users」は、その関連の名前です。Rubyのコードからはこの名前でアクセスできるにようになります。その後ろにはHash型のパラメータになります。
classパラメータ
この関連が参照している対象のモデルのクラス名を宣言します。省略した場合には、関連の名前を使います。
join_tableパラメータ
交差テーブルの名前をシンボルで指定します。省略した場合には、関連付けられる2つのモデル名を複数形にしてアルファベット順に並べアンダースコアで接続した名前になります。
left_key、right_keyパラメータ
left_keyは、交差テーブルで自分をさす外部参照カラムの名前をシンボルにしたもの。right_keyは、交差テーブルで相手をさす外部参照カラムの名前をシンボルにしたものになります。
その他のパラメータ
selectパラメータにセレクトする対象のカラム名(属性名)を指定できます。デフォルトは「自分のテーブルの全カラム」で、交差テーブルの属性は取得できません。交差テーブルにid以外の情報を持たせたいときは(例えば生成時刻) 、ここに指定すれば取得できるようになります。
マイグレートする
マイグレーション用のファイルからデータベースを設定する
rake schema:migrate
これで新しい交差テーブルができます。
コンソールで試してみる
これでUserモデルに以下のような新しいメソッドが追加されたはずです。
followed_users
自分をフォローしているユーザを返すデータセット(後述)
following_users
自分がフォローしているユーザを返すデータセット(後述)
add_followed_user
パラメータのユーザを、新たに自分をフォローするユーザに追加する
add_following_user
パラメータのユーザを、新たに自分がフォローしているユーザに追加する
remove_followed_user
パラメータのユーザを、自分をフォローするユーザから削除する
remove_following_user
パラメータのユーザを、自分がフォローしているユーザから削除する
正しくフォローする/される関係が作れたか、waves-consoleで試して見ましょう。Usersテーブルにはfooという名前のユーザとhogeという名前のユーザが存在するものとします。
コンソールで確認する
>> u1 = Tw::Models::User.find(:name=>'foo')
=> #<Tw::Models::User @values={:name=>"foo", :password=>"bar", :id=>2}>
>> u2 = Tw::Models::User.find(:name=>'hoge')
=> #<Tw::Models::User @values={:name=>"hoge", :password=>"hoge", :id=>3}>
>> u1.following_users
=> #<Sequel::SQLite::Dataset: "SELECT * FROM users INNER JOIN followed_following ON (followed_following.followed_id = 2) AND (fo
llowed_following.following_id = users.id)">
>> u1.following_users.all
=> []
>> u1.add_following_user(u2)
=> #<Tw::Models::User @values={:name=>"hoge", :password=>"hoge", :id=>3}>
>> u1.following_users.all
=> [#<Tw::Models::User @values={:following_id=>3, :followed_id=>2, :name=>"hoge", :password=>"hoge", :id=>3}>]
>>
「u1 = Tw::Models::User.find(:name=>'foo') 」で、Usersテーブルからnameカラムの値が'foo'であるようなユーザを取り出しています。u2も同様に設定します。
「u1.following_users」では、フォローしているユーザの配列(今は何も設定していないので空配列)が返るのではなく、Sequelのデータセット が返ってきます。
データセットとはRDBのビューのようなものです。対象となるデータを取得するための手続きで、データセットにallやfindなどのメソッドを呼ぶことで、初めて実際のデータを得ることができます。「 u1.following_users.all」が、それです。空配列が返っているのが分かります。
ここに実際にフォローするユーザを「u1.add_following_user(u2)」で追加します。「 u1.following_users.all」でu2のユーザのオブジェクトの配列が返ってきています。
ちょっとした問題
ここでもう一度「u1.add_following_user(u2)」するとどうなるでしょうか。実は何のエラーも表示されずにu2が再度追加されしまいます。「 u1.following_users.all」では同じオブジェクトが2つ入った配列になります。これはまずいので、Twでは追加前にチェックするようにします。
まとめと次回の予定
Twitterのメンバー間に存在するフォローする/されるという関係をTwでもまねてみました。Userモデル間に多対多の関連を設定することで、シンプルに実装できることが分かると思います。次回はUIを作ってTwを完成させたいと思います。