views:

249

answers:

4

I have a Map model, which defines the details for an ASCII-art map for a given area in a MUD. I want to be able to reach each map's page using its name instead of its ID, so if I have a map of the Western Ithmia, I want to be able to access it from /maps/western_ithmia (but also /maps/withmia).

I have a second model, MapAlias, which contains all of the existing shortnames that can be used in a path to reach a map. A foreign key is stored with each alias, referring to the ID of its associated Map.

What I want to do is create a new MapAlias simultaneously with each newly-created Map. Unfortunately, it doesn't seem like I can get at my new Map's ID until it's been saved, but I don't want to save it until I've validated/saved its new MapAlias. What makes this worse is that I want each Map to have knowledge of which MapAlias is its primary alias, i.e. the one that generated links will refer to. This seems like a circular dependency to me...

I'm still pretty new to Rails, so I'm not sure what I should be doing, but I've explored model callback hooks (like before_save) with only limited success - I still have the circular dependency to deal with. Any ideas? =/

+1  A: 

Wrap it all in a database transaction:

Map.transaction do
  # ... save all your stuff ...
end

Don't try to re-implement transactions with callbacks... let the database do what it is good at. It'll all get saved, or none of it will get saved.

Dave Pirotte
A: 

What I generally do is manually create the two Models at the same time. That is, if I have two models, Cause and Effect, then I create I look at all the ways a Cause can be created (usually from the controller), and include a line that creates an Effect if the Cause is created, passing in appropriate parameters from Cause.. That way, even though it's not saved, I have access to it's ID (because the object still exists in the method, even if it's not saved).

It might not be as automatic as a before_save method, but I like being able to see what happens, and when, without rooting around in before/after methods.

Jenny
A: 

If you are trying to get this going via an html form, you can call build on the new Map instance on the controller.


# maps_controller.rb
def new
  @map = Map.new
  @map_alias = @map.map_aliases.build
end

There's a bit more to it (creating virtual attributes), but you can take a look at this screencast for some ideas: http://railscasts.com/episodes/75-complex-forms-part-3

hgimenez
A: 

Okay, after a fair bit of time, I think I have a working solution. When I wrote this question, the Map has_many MapAlias'es, and each MapAlias belongs_to a Map. I recently realized that what I wanted was another association: a Map belongs_to its primary name, which the website uses in links to that map. I added that and three hooks - two before_validation_on_* for setting/creating the alias, one after_create for setting the alias's map_id foreign key for the original associations - and it works like a charm.

To the controller it's just a Map.save call, but internally, the alias is saved first:

belongs_to :shortname, :class_name => "MapAlias" # primary name

before_validation_on_create :add_map_alias
def add_map_alias
  self.create_shortname(:title => self.title)

  @alias_failed = !(self.shortname.errors.empty?)

  nil
end

before_validation_on_update :set_map_alias
def set_map_alias
  self.shortname.title = self.title
  self.shortname.save(false)

  @alias_failed = !(self.shortname.errors.empty?)

  nil
end

after_create :set_alias_map_id
def set_alias_map_id
  self.shortname.update_attribute :map_id, self.id

  nil
end

It's probably not the most ideal solution, but it works just fine for my purposes. Thanks for everyone's advice, at any rate!

Twisol