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

时间:2011-05-12 11:29:46

标签: sql ruby-on-rails foreign-keys normalization

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

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

因此,在评论表中,单个记录将包含以下字段:

id
comment
student_id
user_id
created_at
updated_at

这提出了几个问题。首先,用于创建关联对象的漂亮Rails语法分解。如果我想发表新评论,我必须在外键之间进行选择。所以......

User.comments.create(attributes={})

OR

Student.comments.create(attributes={})

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

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

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

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

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

为了澄清(对不起,我知道这已经很久了)我考虑了另外两种常见方法:“has_many:through =&gt;”和多态关联。我能说的最好,也没有解决上述问题。原因如下:

“has_many:through =&gt;”在像博客这样的情况下工作得很好。所以我们有评论,文章和用户模型。用户通过文章有很多评论。 (这样的例子出现在Apress的Beginning Rails 3中。顺便说一句好书。)问题是,为了使这个工作(如果我没有记错),每篇文章必须属于特定的用户。在我的情况下(我的学生模型在这里类似于文章)没有一个用户拥有学生。所以我不能说用户通过学生有很多评论。可能有多个用户评论同一个学生。

最后,我们有多态关联。这适用于多个外键,假设没有一个记录需要属于多个外来类。在RailsCasts第154集中,Ryan Bates给出了一个例子,其中评论可能属于文章或照片或事件。但是如果单个评论需要属于多个评论呢?

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

提前感谢任何建议!

3 个答案:

答案 0 :(得分:4)

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

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关联。

答案 1 :(得分:2)

我认为多态关联是要走的路。我建议使用插件而不是“自己滚动”。我使用ActsAsCommentableon 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关联获取用户的所有评论,但您还可以通过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 -%>

PLUS:

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

答案 2 :(得分:1)

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

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

current_user可能是User.find session[:user_id]之类的帮助者。

has_many:通过关联在这里没有意义。您可以使用它通过评论关联用户和学生,但不能通过学生使用用户和评论

希望有所帮助。