开发者

Setting up a mutual belongs_to in Ruby on Rails

开发者 https://www.devze.com 2022-12-19 15:04 出处:网络
I\'m creating a wiki.Each Article has_many Revisions, and an Article belongs_to a single current_revision.So in the database, Articles have a single reference to a Revision\'s id, and Revisions each h

I'm creating a wiki. Each Article has_many Revisions, and an Article belongs_to a single current_revision. So in the database, Articles have a single reference to a Revision's id, and Revisions each have a single reference to the Article they belong to. Before I continue, does this seem like a sane way to do things? It strikes me as fairly unorthodox, but logical, and I'm not sure how others in similar situations set things up.

The trouble is that this type of mutual belongs_to relationship seems to really throw Rails off when creating the models. When I first create an Article, I'd like to also create an initial Revision to go with it.

I added a before_create method and did something like:

initial_revision = self.revisions.build
self.current_revision = initial_revision

but this would cause a stack overflow on save, as Rails apparently tries in a loop to first save the Article, so it has an article_id to stick in the Revision, and then first save the Revision, so it has a current_revision_id to stick in the Article.

When I break things up, and don't create them simultaneously (but still in a transaction), the first one created doesn't get its reference set. For example:

initial_revision = Revisions.create
self.current_revision = initial_revision
initial_revision.article = self

would leave the rev开发者_开发技巧ision with a null article_id as it missed the save.

I think I could get around this by calling an after_create method as well, just to initialize the variable with an update and save, but this is turning into a gigantic mess, and I feel like in Rails that usually means I'm doing something wrong-headedly.

Can anyone help, or am I stuck creating a little after_create method that saves in the change?


I has similar problem recently. You need to declare only one way of association. Can your Article be created without Revision, and then Revision added to existing Article?

Or can you point from Article to Revision which is not pointing back? If that should be not possible, then you need to declare Revision as belongs_to :article, and Article :has_many :revisions and has_one :revision, :conditions => { ... }. And add flag 'main revision' to revision model or get last revision by date.

This way you don't provide cyclic dependencies, so it should be easier.

Edit:
This is how I tested it and make it work:

class Article < ActiveRecord::Base
  has_many :revisions
  has_one :current_revision, :class_name => "Revision", :conditions => { :tag => "current" }

  before_validation do |article|
    # add current revision to list of all revisions, and mark first revision as current unless one is marked as current
    article.current_revision = article.revisions.first unless article.current_revision.present?
    article.revisions << article.current_revision if article.current_revision.present? and not article.revisions.member?(article.current_revision)
  end

  after_save do |article|
    article.current_revision.mark_as_current if article.current_revision.present?
  end
end

class Revision < ActiveRecord::Base
  belongs_to :article

  def mark_as_current
    Revision.update_all("tag = ''", :article_id => self.article_id)
    self.tag = "current"
    save!
  end

end

And this is how it works now (dump from script/console):

$ ./script/console
Loading development environment (Rails 2.3.5)
>> a1 = Article.new :name => "A1"
>> a1.revisions.build :number => 1
>> a1.save
>> a1.reload
>> a1.revisions
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 1  | 1          | 1      | current | 2010-02-03 19:10:37 UTC | 2010-02-03 19:10:37 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+
>> a1.current_revision
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 1  | 1          | 1      | current | 2010-02-03 19:10:37 UTC | 2010-02-03 19:10:37 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+
>> a1r2 = a1.revisions.build :number => 2
+------------+--------+-----+------------+------------+
| article_id | number | tag | created_at | updated_at |
+------------+--------+-----+------------+------------+
| 1          | 2      |     |            |            |
+------------+--------+-----+------------+------------+
>> a1r2.mark_as_current
>> a1.revisions
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 1  | 1          | 1      | current | 2010-02-03 19:10:37 UTC | 2010-02-03 19:10:37 UTC |
| 2  | 1          | 2      | current | 2010-02-03 19:11:44 UTC | 2010-02-03 19:11:44 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+
>> a1.revisions.reload
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 1  | 1          | 1      |         | 2010-02-03 19:10:37 UTC | 2010-02-03 19:10:37 UTC |
| 2  | 1          | 2      | current | 2010-02-03 19:11:44 UTC | 2010-02-03 19:11:44 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+
>> a1.current_revision
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 1  | 1          | 1      | current | 2010-02-03 19:10:37 UTC | 2010-02-03 19:10:37 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+
>> a1.reload
>> a1.current_revision
+----+------------+--------+---------+-------------------------+-------------------------+
| id | article_id | number | tag     | created_at              | updated_at              |
+----+------------+--------+---------+-------------------------+-------------------------+
| 2  | 1          | 2      | current | 2010-02-03 19:11:44 UTC | 2010-02-03 19:11:44 UTC |
+----+------------+--------+---------+-------------------------+-------------------------+

Watch for problem with two revisions marked as current before reload revisions collection on article. When you mark one of revisions as current, then you need to reload you whole article object (if you want to use current_revision field) or only revision collection.

And you should probably treat current_revision only as a read-only pointer. If you try to assign another revision to it, then you'll loose previous revision which was pointed by article as current (Rails will remove old referenced object, because of has_one).


A Revision is just a version of an Article, right? There's a great Railscast on Model Versioning using the vestal_versions gem which should solve your problem.


I think the best way to have it is to have each Revision belong to an Article. Instead of the cyclical association of each Article belonging to a Revision (Current). Use a has_one relationship to link an article to the latest revision.

class Revision < ActiveRecord::Base
  belongs_to :article
  ...
end

class Article < ActiveRecord::Base
  has_many :revisions
  has_one :current_revision, :order => "version_number DESC"
  ...
end

However in the event of a rollback, you'll have increase the version number of the revision rolled back to.

Also... you can eliminate the version_number field and just order on id if a.version_number > b.version_number and only if a.id > b.id. Which means that rollbacks will result in cloned records with higher ids than the last version.


I've had the same problem in my own app, and although my structure is slightly different I've finally found a solution.

In my app I have something more like this:

class Author < ActiveRecord::Base
  has_many :articles
  has_many :revisions
end

class Article < ActiveRecord::Base
  has_many :revisions
  belongs_to :author
end

class Revision < ActiveRecord::Base
  belongs_to :article
  belongs_to :author
end

So I have a 3-model loop instead.

In my case, I want to save the whole hierarchy (from new) at once. I've found that I can do this by creating a new author, then adding the articles to the author as normal, but when I want to create the revisions, I do it like this (from within the Author class):

def add_new_revision(@author)
  article.revisions = article.revisions.push(Revision.new(:author => @author))
end

(Note that here @author hasn't been saved yet either)

Somehow this works. I've noticed that in the logs, activerecord is inserting the revision after the author and the article have been saved (just like using the after_create handler). I'm not sure why this is treated differently to doing build, but it seems to work (although I wouldn't be surprised if it didn't work for anyone else!)

Anyway, I hope that helps! (Sorry it's so long after you posted the question!)

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号