163 :自引用关联( 查看原视频 )
下面的页面来自一个简单的社交网络程序。用户可以注册,登陆,然后和其他用户进行交流。这个页面显示的是一个用户列表,通过每个用户旁边的链接你可以把他们加为好友。
不过这些链接现在还都不起作用。这一集,我们打算加一些代码让用户能够加其他用户为好友。为此,我们需要创建自引用关联:建立用户和用户之间的关系,这是建立在同一数据模型的两个实体之间的关系,而不是不同数据模型之间的关系。
生成正确的动作
用户页面中的“添加朋友”链接还没有指向任何其它地方。
<%= link_to "Add Friend" %>
在一些地方,我们需要把这个链接与某个控制器中的某个动作关联起来。那么,当它被点击的时候,它会调用哪个控制器的哪个动作呢,这个值得琢磨琢磨。我们其实已经有了一个 UsersController 控制器,所以在这里来处理朋友相关的东西看上去理所当然,因为朋友不就是其它一些用户吗。我们可以在 UsersController 中来添加和删除朋友。
def add_friend
end
def remove_friend
end
但是,考虑到各种因素,这种做法其实是很有问题的。当你创建像这样的控制器方法的时侯,在你脑海中要敲响的第一个警钟就是,这些方法不在七个标准的 RESTful 方法之内(索引,显示,新建,创建,编辑,更新和删除)。另外一个要注意的事情是这些方法都有一个 _friend 后缀。这就告诉我们应该把这些方法放到他们自己的命名空间中去。最后一个说明这不是个好方法的苗头是: add 和 remove 表明我们其实是要创建和删除一个资源。所有这些,让我们想到我们应该创建一个新的控制器来处理朋友相关的东西。
创建朋友关系
我们新创建一个叫 FriendshipsController 的控制器。控制器的命名非常重要。我们也可以把它叫做 FriendsController ,但是在我们的程序中一个朋友其实也就是一个用户,我们要用这个新的控制器来创建和删除用户间的关系,而不是创建和删除用户。
一个用户可以有很多个朋友,同时也可以是很多其他人的朋友, 所以我们需要创建多对多的关系。在 Rails 中,我们有两种方法来定义这种关系: has_and_belongs_to_many 和 has_many :though 。(你可以看看 47 集的视频 ,里面讲了很多这两种方法的内容)。目前来说,大家更多地是在用 has_many :though ,那么我们也将用它来定义我们的朋友关系。
为了使用 has_many :though ,我们需要创建一个连接数据模型,我们把它命名为 Friendship 。这个模型有两个字段, user_id 代表当前要加朋友的用户, friend_id 代表被加为朋友的用户。
我们象往常一样来创建我们新的数据模型,
script/generate model Friendship user_id:integer friend_id:integer
然后运行数据迁移任务。
rake db:migrate
随着数据模型的创建,我们需要生成之前说的 FriendshipController 控制器。
script/generate controller Friendships
因为我们是把 Friendship 作为一种资源,所以我们需要在 /config/routes.rb 中加入下面一行代码。
map.resources :friendships
搞定了数据模型和控制器,我们可以来定义一下 Friendship 如何工作了。在我们的数据模型类中,让我们先来定义 User 和 Friendship 之间的关系。
class Friendship < ActiveRecord::Base
belongs_to :user
belongs_to :friend, :class_name => 'User'
end
一个 Friendship 从属于一个用户,也就是添加朋友的一方。它同时还从属于一个 Friend ,也是一个用户,是被加做朋友的一方。对于 friend 关系,我们需要显式地指明它的类名,因为 ActiveRecord 没法通过关联中的名字来找出对应的数据模型。
同样,我们需要在 User 数据模型中定义另外一半数据关系。
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships
# rest of class omitted.
end
这里定义的关系和通常的多对多关系一样,我们不需要指定一个特定的名字,因为从关系名字中我们都可以推导出来。
接通“添加朋友”链接
定义好了数据模型间的关系,我们也有了 FriendshipsController 控制器,我们可以在用户的索引视图上给“添加朋友”链接做点什么了。
<% for user in @users %>
<div class="user">
<p>
<strong><%=h user.username %></strong>
<%= link_to "Add Friend", friendships_path(:friend_id => user), :method => :post %>
<div class="clear"></div>
</p>
</div>
<% end %>
这个链接需要调用 FriendshipsController 控制器中的 create 方法。为此我们让这个链接以 POST 方式指向 friendships_path 。此外,我们还得传入我们要加为朋友的用户,把它作为参数传入 friendships_path 当中。这样这个用户的 id 就会被传入 create 动作中,从而让这个动作知道我们要加哪个用户为朋友。
写好了链接的代码,我们继续在 FriendshipsController 控制器中来写我们的 create 动作。
def create
@friendship = current_user.friendships.build(:friend_id => params[:friend_id])
if @friendship.save
flash[:notice] = "Added friend."
redirect_to root_url
else
flash[:notice] = "Unable to add friend."
redirect_to root_url
end
end
通过当前用户,我们可以创建一个新的朋友关系,传入的参数是我们要加为朋友的用户 id 。使用 build 方法意味着新的 Friendship 中的 user_id 属性会被自动填上,再加上我们以参数传入的 friend_id ,这个朋友关系就完全建好了。保存好这个朋友关系,我们可以重定向回主页面,给用户显示一条提示信息说朋友已经添加了。如果因为某种原因,这个朋友关系不能保存,我们就要显示另外一个提示信息。当然,如果这是一个要上线的程序,我们需要告诉用户更多具体的信息让他们知道请求为什么失败了。
查看我们的朋友
一个登陆用户现在可以点击“添加朋友”链接来把另一个用户加为朋友了。但是他们还没法看到所有朋友的列表。让我们改一下用户的基本信息页面,使他们看到他们都把谁加为朋友了。基本信息页面也就是一个用户的显示页。当前,这个页面上只有用户的名字,和一个用来找到其他朋友的链接。
<% title "My Profile" %>
<p>Username: <%=h @user.username %></p>
<p><%= link_to "Find Friends", users_path %></p>
为了显示朋友列表,我们可以循环遍历用户的朋友集合来显示每一个朋友。
<h2>Your Friends</h2>
<ul>
<% for user in @user.friends %>
<li><%= h. user.username %></li>
<% end %>
</ul>
现在,在基本信息页面上可以看到我们的朋友列表了。
删除朋友
如果我们能在朋友列表的每个朋友旁加一个链接,让我们能把一个用户从我们的朋友中踢出去,会是一个很有用的功能。实现这个功能,我们要加一个链接来调用 FriendshipController 控制器的 destroy 动作,传入的参数是朋友关系的 id 。这里让人稍稍有点晕,我们是在循环遍历所有的用户,没法获得 Friendship 数据模型,那我们怎么拿到朋友关系的 id 呢?对于 Rails 菜鸟来说,他们在使用多对多关系的时候,经常会遇到这种问题,他们可能忘了,其实他们可以直接访问那个连接数据模型,而不是直接从用户就跳到朋友那里去。
现在,我们循环遍历用户的朋友关系,而不是他的朋友来创建这个列表。视图中的代码修改后如下所示:
<h2>Your Friends</h2>
<ul>
<% for friendship in @user.friendships %>
<li>
<%= h friendship.friend.username %>
(<%= link_to "remove", friendship, :method => :delete %>)
</li>
<% end %>
</ul>
现在我们通过 friendship.friend.username 来得到每个朋友的名字,然后传入朋友关系作为参数来创建这个删除链接,我们还要声明使用 DELETE 方法,让它去调用 destroy 动作。
说到 destroy 动作,让我们现在就来在 FriendshipsController 控制器中实现它。
def destroy
@friendship = current_user.friendships.find(params[:id])
@friendship.destroy
flash[:notice] = "Removed friendship."
redirect_to current_user
end
注意,这个动作的第一行代码中我们只是在当前用户的朋友列表中进行查找。如果我们调用 Friendship.find(params[:id]) ,那么一个恶意用户就有可能删除任何两个其他用户间的朋友关系,用户应该被限制只能删除它们自己创建的朋友关系。剩下的部分,这个朋友关系被删除,然后页面重定向到用户的基本信息页。
基本信息中现在可以看到这些链接,我们可以点每个朋友旁边的链接来删除和他们的朋友关系。
反向关系
当创建自引用关系的时候,需要特别记住的是我们仅仅创建的一个单向的关系。虽然在上面的页面中 paul 被列为 eifion 的朋友,如果我们打开 paul 的基本信息页面,我们看不到 eifion 在他的朋友列表中,除非 paul 也加他为朋友。 创建一个双向的朋友关系,我们需要有两条 Friendship 记录。
结束这一集之前,我们还要给基本信息页加一个列表,让用户可以看到都有谁把他们加为了朋友,这样我们也可以从另一方向来看朋友关系。为此,我们需要在 User 数据模型中新加两个关系。
class User < ActiveRecord::Base
has_many :friendships
has_many :friends, :through => :friendships
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
has_many :inverse_friends, :through => :inverse_friendships, :source => :users
#rest of class omitted.
end
不容易想到一个合适的名字来命名反向的朋友关系,所以我们就加上“ inverse ”前缀来定义 inverse_friendships 和 inverse_friends 。我们还要加上其他一些选项来实现这些关系。对于 inverse_friendships ,我们还需要指明数据模型的名字,因为没法从关系名中推导出来。另外我们还需要定义 friend_id 作为外键。对于 inverse_friends 关系,我们需要指明 users 作为 source ,因为这个也不能从关系名字中推导出来。
回到基本信息页,我们想显示把我们加为好友的用户的列表。为此我们只需要循环遍历我们的 inverse_friends ,然后显示那些用户的名字。
<h2>Users Who Have Befriended You</h2>
<ul>
<% for user in @user.inverse_friends %>
<li><%= h user.username %></li>
<% end %>
</ul>
如果我们以用户 paul 登陆,并且添加 eifion 做为他的一个朋友,然后再以 eifion 登陆,我们就能看到列表中 paul 添加了 eifion 作为朋友了。
本集就这么多内容。我们还没有重造出一个 Facebook ,但是希望你能够对于如何在 Ruby on Rails 中使用自引用的关系有所体会。