0

I have 2 models, User and Stash.

A User has many stashes, and a default_stash. A Stash has one owner (User).

Before a User is created I want it to build a Stash AND assign that Stash to self.default_stash. As per the instructions here Rails - Best-Practice: How to create dependent has_one relations I created the Stash relation, however I cannot 'create' the belongs_to relation at the same time.

User.rb

has_many :stashes, foreign_key: :owner_id
belongs_to :default_stash, class_name: 'Stash', foreign_key: :owner_id

before_create :build_default_stash
def build_default_stash
  self.default_stash = stashes.build
  true
end

Stash.rb

belongs_to :owner, class_name: 'User'

At this point the Stash can be found in user.stashes but user.default_stash remains nil, as stashes.build does not return an id in the before_create block.

What I need can be achieved by adding the following to User.rb

after_create :force_assign_default_stash
def force_assign_default_stash
  update_attribute(:default_stash, stashes.first)
end

But I'd much prefer to keep everything within the before_create block if possible, for validations etc.

Community
  • 1
  • 1
johnrees
  • 327
  • 3
  • 11
  • If a user belongs to a default_stash, it must also have a default_stash_id, and therefore doesn't use a foreign key into the default_stash table (owner_id). I think what you mean is a user has_one default_stash – cdesrosiers Jun 10 '12 at 23:50

1 Answers1

1

I agree with you that what you describe should work, but if you're building associated records in memory and expecting them to save together properly -- with everything hooked up -- then ActiveRecord is particularly finicky about how you define them.

But you can make it work without changing a thing in your before_create.

The usual problem is that you need to give AR hints about which relationships are inverses of each other. The has_many and belongs_to methods take an :inverse_of option. The problem in your case is that you have one side of a relationship (Stash#owner) that is actually the inverse of two on the other (User#stashes and User#default_stash), so what would you set as the inverse_of for Stash#owner?

The solution is to add a has_one to Stash (call it something like owner_as_default), to balance things out. Then you can add inverse_of to all of the definitions, each identifying its inverse on the other side. The end result is this:

class User < ActiveRecord::Base
  has_many   :stashes,       foreign_key: :owner_id, inverse_of: :owner
  belongs_to :default_stash, class_name: "Stash",    inverse_of: :owner_as_default

  ...
end

class Stash < ActiveRecord::Base
  belongs_to :owner,            class_name: "User", inverse_of: :stashes
  has_one    :owner_as_default, class_name: "User",
    foreign_key: :default_stash_id, inverse_of: :default_stash
end

(Also, you don't need a foreign key on your belongs_to.)

From there, your before_create should work as you've written it.

Yes, it seems that this is a lot of redundant defining. Can't ActiveRecord figure it all out from the foreign keys and whatnot? I always feel like I'm breaking some walls by making one side so aware of what it's the inverse of. Maybe in some future version of Rails this will be ironed out.

Rob Davis
  • 15,597
  • 5
  • 45
  • 49
  • This works great and makes sense, I don't mind helping ActiveRecord out a bit when it is doing so much legwork behind the scenes! Thanks Rob. – johnrees Jun 11 '12 at 07:54