Несколько внешних ключей для одной записи в Rails 3? - PullRequest
4 голосов
/ 12 мая 2011

Я работаю над приложением, которое будет управлять студентами, зачисленными на курс. В приложении будут пользователи, которые могут входить в систему и манипулировать студентами. Пользователи также могут комментировать студентов. Итак, три наших основных класса - это «Студент», «Пользователь» и «Комментарий». Проблема в том, что мне нужно связать отдельные комментарии с обеими другими моделями: User и Student. Итак, я начал с некоторого базового кода, подобного этому ...

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)

Проблема с этой опцией заключается в том, что мне нужно перечислить student_id в attr_accessible в моей модели комментариев. Но я понимаю, что это создает угрозу безопасности, поскольку кто-то может технически прийти и связать комментарий с другим учеником, используя массовое задание.

Это приводит к дальнейшему вопросу о моделировании данных в целом с использованием Rails. Приложение, которое я сейчас создаю в Rails, - это то, которое я написал несколько лет назад на PHP / MySQL. Когда я впервые изучал SQL, большое значение придавалось идее нормализации. Так, например, если у вас есть таблица контактов, в которой хранятся имена и адреса, вы должны использовать множество связей с внешним ключом, чтобы избежать повторения данных. Если у вас есть столбец состояний, вы бы не хотели перечислять состояния напрямую. В противном случае вы могли бы потенциально иметь тысячи строк, которые содержат строковые значения, такие как «Техас». Гораздо лучше иметь отдельную таблицу состояний и связать ее с таблицей контактов, используя отношения внешнего ключа. Мое понимание хорошей теории SQL заключалось в том, что любые значения, которые могут повторяться, должны быть разделены на их собственные таблицы. Конечно, чтобы полностью нормализовать базу данных, вам, скорее всего, понадобится немало внешних ключей в таблице контактов. (state_id, пол_ид и т. д.)

Так, как можно поступить об этом «по пути рельсов»?

Для пояснения (извините, я знаю, что это становится длинным), я рассмотрел два других распространенных подхода: "has_many: through =>" и полиморфные ассоциации. Насколько я могу судить, ни одна из них не решает вышеуказанную проблему. И вот почему:

"has_many: through =>" отлично работает в случае с блогом. Итак, у нас есть комментарии, статьи и пользовательские модели. У пользователей есть много комментариев через статьи. (Такой пример появляется в Beginning Rails 3 от Apress. Кстати, отличная книга.) Проблема в том, что для того, чтобы это работало (если я не ошибаюсь), каждая статья должна принадлежать конкретному пользователю. В моем случае (где моя модель ученика здесь аналогична статье) ни один пользователь не владеет учеником. Поэтому я не могу сказать, что у пользователя есть много комментариев через студентов. Может быть несколько пользователей, комментирующих одного и того же ученика.

Наконец, у нас есть полиморфные ассоциации. Это прекрасно работает для нескольких внешних ключей, при условии, что ни одна запись не должна принадлежать более чем одному внешнему классу. В эпизоде ​​RailsCasts # 154 Райан Бейтс приводит пример, где комментарии могут принадлежать статьям ИЛИ фотографиям ИЛИ событиям. Но что, если один комментарий должен принадлежать более чем одному?

Итак, в общем, я могу заставить мой сценарий Пользователь, Студент, Комментарий работать вручную, назначив один или оба внешних ключа, но это не решает проблему attr_accessible.

Заранее спасибо за любой совет!

Ответы [ 3 ]

4 голосов
/ 12 мая 2011

У меня был ваш ТОЧНЫЙ вопрос, когда я начал с рельсов. Как аккуратно установить две ассоциации в методе create, при этом гарантируя, что association_ids защищены.

Вукерплан прав: вы не можете установить вторую ассоциацию посредством массового присвоения, но вы все равно можете назначить 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 при его сохранении.

2 голосов
/ 12 мая 2011

Я думаю, что полиморфные ассоциации - это путь. Я бы порекомендовал использовать плагин вместо того, чтобы «катиться самостоятельно». У меня были отличные результаты с ActsAsCommentable ( на 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, например, для хранения всех районов и округов. Но если все, что вам нужно знать о состоянии учащихся, текстовое поле вполне подойдет. Это также верно для пола и тому подобное.

1 голос
/ 12 мая 2011

Ну, для первой части.Если я правильно понял ваш вопрос, я думаю, что, поскольку комментарии перечислены в контроллере студентов, вы должны связать их через модель Студента (мне это просто кажется логичным).Однако, чтобы защитить его от присвоения неправильного идентификатора пользователя, вы можете сделать что-то вроде этого

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

current_user может быть помощником, который выполняет User.find session[:user_id] или что-то подобное.

has_many: throughассоциация здесь не имеет смысла.Вы можете использовать его, чтобы связать Пользователя и Студента через Комментарий, но не Пользователь и Комментарий через Студента

Надеюсь, что это поможет.

...