Webフレームワークの新しい波 Waves探訪記

第5回Twのユーザにフォローする/されるの関係を導入する

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を完成させたいと思います。

おすすめ記事

記事・ニュース一覧