Rails 3中单个记录的多个外键?

Posted

技术标签:

【中文标题】Rails 3中单个记录的多个外键?【英文标题】:Multiple Foreign Keys for a Single Record in Rails 3? 【发布时间】:2011-08-24 01:14:30 【问题描述】:

我正在开发一个应用程序来管理注册课程的学生。该应用程序将拥有可以登录和操纵学生的用户。用户还可以对学生发表评论。所以我们的三个主要类是学生、用户和评论。问题是我需要将单个 cmets 与其他两个模型相关联:用户和学生。所以我从一些像这样的基本代码开始......

class Student < ActiveRecord::Base
  has_many :comments
end

class User < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :student
  belongs_to :user
  attr_accessible :comment
end

所以在 cmets 表中,单个记录将具有以下字段:

id
comment
student_id
user_id
created_at
updated_at

这带来了几个问题。首先,用于创建关联对象的好 Rails 语法被破坏了。如果我想发表新评论,我必须在外键之间进行选择。所以...

User.comments.create(attributes=)

Student.comments.create(attributes=)

另一种选择是任意选择一个外键,然后手动将其添加到 attrs 哈希中。所以...

User.comments.create(:comment => "Lorem ipsum", :student_id => 1)

这个选项的问题是我必须在我的 Comment 模型中的 attr_accessible 下列出 student_id。但我的理解是,这会带来安全风险,因为从技术上讲,有人可能会出现并使用批量作业将评论与其他学生重新关联。

这引出了一个关于使用 Rails 进行一般数据建模的进一步问题。我目前在 Rails 中构建的应用程序是几年前我最初用 php/mysql 编写的应用程序。当我第一次学习 SQL 时,非常重视规范化的思想。因此,例如,如果您有一个存储姓名和地址的联系人表,您将使用大量外键关系来避免重复数据。如果您有一个 state 列,您不会希望直接列出这些州。否则,您可能会有数千行都包含字符串值,例如“Texas”。拥有一个单独的状态表并使用外键关系将其与您的联系人表相关联要好得多。我对好的 SQL 理论的理解是,任何可能重复的值都应该分开到它们自己的表中。当然,为了完全规范化数据库,您最终可能会在联系人表中使用相当多的外键。 (state_id、gender_id 等)

那么如何以“Rails 方式”来解决这个问题?

为了澄清(抱歉,我知道这有点长)我考虑了另外两种常见的方法:“has_many :through =>”和多态关联。据我所知,两者都不能解决上述问题。原因如下:

"has_many :through =>" 在博客之类的情况下工作得很好。所以我们有评论、文章和用户模型。用户通过文章有很多评论。 (这样的示例出现在 Apress 的Beginning Rails 3 中。顺便说一句,这本书很棒。)问题在于,要使其工作(如果我没记错的话)每篇文章都必须属于特定用户。在我的情况下(我的学生模型在这里类似于文章)没有单个用户拥有学生。所以我不能说一个用户通过学生有很多cmets。可能有多个用户对同一个学生发表评论。

最后,我们有多态关联。假设没有一条记录需要属于一个以上的外部类,这对于多个外键非常有效。在 RailsCasts 第 154 集中,Ryan Bates 给出了一个示例,其中 cmets 可能属于文章或照片或事件。但是,如果一条评论需要属于多个呢?

总而言之,我可以通过手动分配一个或两个外键来使我的用户、学生、评论场景工作,但这并不能解决 attr_accessible 的问题。

提前感谢您的任何建议!

【问题讨论】:

【参考方案1】:

我认为多态关联是要走的路。我建议使用插件而不是“自己动手”。我使用ActsAsCommentable (on Github) 取得了不错的成绩。

至于您的attr_accessible 问题:您是对的,这样做更安全。但它不会阻止您尝试做的事情。

我假设你有一些持有当前用户的东西,在我的例子中是current_user

@student = Student.find(params[:id])
@comment = Comment.new(params[:comment]) # <= mass assignment
@comment.student = @student              # <= no mass assignment
@comment.user    = current_user          # <= no mass assignment
if @comment.save
  # ...
else
  # ...
end

attr_accessible 可以保护您免受有人将params[:comment][:student_id] 偷偷带入,但它不会阻止以其他方式设置属性。

您仍然可以通过has_many :comments 关联获取用户的所有cmets,但您还可以通过belongs_to :user 关联显示谁评论了某个学生:

<h1><%= @student.name %></h1>

<h2>Comments</h2>

<%- @student.comments.each do |comment| -%>
    <p><%= comment.text %><br />
    by <%= comment.user.name %></p>
<%- end -%>

加:

不要过度设计您的应用。拥有state:string 字段非常好,除非您想对State 对象做一些有意义的事情,例如存储所有地区和县。但是,如果您只需要了解学生状态,那么文本字段就可以了。这也适用于性别等。

【讨论】:

感谢您的回复。我很高兴使用插件,但我仍然看不到多态方法如何解决我的问题。例如,如果单个评论与学生相关联,那么我如何将其与用户相关联?在查看多态表的示例时,我只看到一个类列。因此,如果我将其与学生相关联,那么我将无处可记录用户 ID。也许这是 ActsAsCommentable 解决的问题? 用户和学生是不同的东西还是每个学生都是用户?您是否有更多像教授或员工这样的“类似人”的实体? 用户和学生是不同的。例如,Bob 是一名学生,他的个人数据记录在应用程序中,但 Bob 无权访问该应用程序,甚至不知道该应用程序。 (该应用程序完全是一个内部实用程序。)Bill 和 Sara 是登录到该应用程序以管理 Bob 等学生的用户。比尔(用户)在鲍勃(学生)的页面上发表评论说:“嘿,没有人邮寄鲍勃的新教科书。” Sara(用户)回复:“好的,我明天发送。”所以 Bill 的评论属于他,也属于 Bob 的学生记录。 Sara 的回复属于她,也属于同一个学生记录 (Bob)。 好的,现在我明白了。所以 Student 是一个可评论的对象,而 User 是评论的对象。如果您有更多可以评论的对象(主题、教室等),我强烈建议您使用像 ActsAsCommentable 这样的多态关联。我也更新了我的答案。 感谢您的信息。我一定会检查的。我查看了来自 ActsAsCommentable 的迁移示例,并注意到除了经典的多态字段(如 commentable_id 和 commentable_type)之外,还有一个 user_id 字段。所以我假设即使它是多态的,它仍然在同一条记录中存储多个外键。【参考方案2】:

好吧,对于第一部分。如果我正确理解了您的问题,我认为,由于 cmets 列在学生控制器中,您应该通过学生模型将它们关联起来(这对我来说似乎是合乎逻辑的)。但是,为了防止它分配错误的用户 ID,您可以这样做

@student = Student.find params[:id]
@student.comments.create :user => current_user

current_user 可能是执行User.find session[:user_id] 或类似操作的助手。

has_many :through 关联在这里没有意义。您可以使用它通过 Comment 关联 User 和 Student,但不能通过 Student 关联 User 和 Comment

希望对您有所帮助。

【讨论】:

好的,听起来不错。我确实有一个 current_user 方法,所以那里没问题。您是否建议不要说评论属于用户,而应该将用户列为 cmets 表中的字段?这似乎没问题,但是如果我想搜索特定用户制作的所有 cmets 怎么办?是否会以某种范围匹配包含该用户 ID 的所有记录来运行搜索?再次感谢您的信息! 不,伙计。拥有Comment belongs_to User 关联非常好。你不需要删除它。【参考方案3】:

当我开始使用 Rails 时,我有你的确切问题。如何在 create 方法中整齐地设置两个关联,同时确保关联 ID 受到保护。

Wukerplank 是对的——你不能通过批量赋值来设置第二个关联,但你仍然可以直接在新的一行中分配 Association_id。

这种类型的关联分配非常常见,并且在我的代码中随处可见,因为在很多情况下一个对象具有多个关联。

另外,需要明确的是:多态关联和 has_many :through 根本无法解决您的问题。您有两个独立的关联(评论的“所有者”和评论的“主题”) - 它们不能合理化为一个。

编辑:你应该这样做:

@student = Student.find_by_id(params[:id])
@comment = @student.comments.build(params[:comment]) #First association is set here
@comment.user = current_user #Second association is set here
if @comment.save
  # ...
else
  # ...
end

通过使用 Object.associations.build,Rails 会自动创建一个新的“关联”对象,并在您保存它时将其与 Object 关联。

【讨论】:

编辑:我应该说 Wukerplank 关于多态关联是错误的——你根本不需要使用它。不过,除了第一句话,他的答案是正确的——他的代码示例应该正是您所需要的。 好的,谢谢您的信息。您能否详细说明在新行上分配 Association_id 的想法?我想看一个例子。你是在说@comment.student = @student,然后是@comment.user = @user,然后是@comment.save?我同意,Wukerplank 的代码看起来不错,但它不包含与 User 模型的任何关联。 嗨,乔希 - 用应该让你到达那里的代码编辑了我的答案。如果这不起作用,请告诉我。 嘿,Rob,好吧,看起来真不错。我是否正确地假设我可以将外键字段排除在 attr_accessible 之外?非常感谢! 是的,这两个外键都不需要访问才能工作。很高兴它帮助了你!

以上是关于Rails 3中单个记录的多个外键?的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 LINQ 中的外键列检索单个记录?

Rails活动记录单个错误修补程序

Rails - 如何查找单个记录(用户名)在主页中向任何访问者显示它

Rails:为什么我可以通过控制器将单个记录属性发送到新记录而不是数组?

在单个单元格中根据多个条件选择记录

将单个记录分成多个