3
\$\begingroup\$

I'm looking for advice / best practice to avoid doing a deeply nested set of IF/THEN statements in Rails. The intention is to seed a database.


Short version - without specific examples

My basic pattern is 1 .. n

IF(1st_object_exists) THEN
 2nd_object = 1st_object.relationship.create!(args)
 IF (2nd_object) THEN
   3rd_object ... so on & so forth down 9 levels ...
 END
END

Where the issue exists is I have two additional, unrelated relationships which should populate at each step back at each step & potentially 4 or 5 that need to populate.


Long version - with specific examples

Basic setup: each game is a campaign. Each campaign will have players, each player will have a country (each country also has a join table back to the campaign), each country a state...

I'm hoping there's an elegant way of doing this instead without using too many object chains (".")'s, but perfectly happy to do so if that's the only way.

Originally, I was doing Campaign.Players.Countries.create!(args), which errors with not valid method for Countries or Players until I eliminate one or the other. Eliminating this leaves me seeding the relationship value for the other has_many else where though...

I also had tried using the same join tables with models using has_many, through: for this pattern ...

  user = User.first
  if(user) then
    campaign = user.campaigns.create!(args)
    if(campaign) then
      new_player = game_instance.players.create!(args)
      if(new_player) then
        new_country = new_player.countries.create!(args)
    if(new_country) then
      new_country.states.create!(args)
    end
  end
end
  end

My models are a bit of a mess but all present ...

There are several primary tables & a join table between each (including a belongs_to matching the has_many's pointed at it from other files)

user.rb

  has_many :userplays
  has_many :players, through: :userplays
  has_many  :usercamps
  has_many  :campaigns, through: :usercamps

campaign.rb

  belongs_to :users
  has_many  :campplays
  has_many  :players, through: :campplays
  has_many  :campcounts
  has_many  :countries, through: :campcounts

player.rb

  has_many  :countries
  has_many  :campplays
  has_many  :campaigns, through: :campplays
  has_one   :userplay
  has_one   :user, through: :userplay

country.rb

  has_many  :states
  has_many  :counties, through: :state
  belongs_to  :player
  belongs_to  :campcount

As stated above, all join tables follow this format, but after the first 4 or 5 letters I truncate the table name for ease of length...

<1stTableName><2ndTableName>.rb

belongs_to :1stTableName
belongs_to :2ndTableName
\$\endgroup\$
1
  • 1
    \$\begingroup\$ As we all want to make our code more efficient or improve it in one way or another, try to write a title that summarizes what your code does, not what you want to get out of a review. Please see How to get the best value out of Code Review - Asking Questions for guidance on writing good question titles. \$\endgroup\$
    – Vogel612
    Commented Mar 1, 2017 at 21:17

2 Answers 2

2
\$\begingroup\$

On a technical note create! will always return a valid object (or raise an exception)

What I usually do is put it in a separate method and use an early return:

user = User.first
return unless user
campaign = user.campaigns.create!(args)
return unless campaign
new_player = game_instance.players.create!(args)
return unless new_player
new_country = new_player.countries.create!(args)
return unless new_county
new_country.states.create!(args) 

You can combine the return as follows:

user = User.first                                 || return
campaign = user.campaigns.create!(args)           || return 
new_player = game_instance.players.create!(args)  || return
new_country = new_player.countries.create!(args)  || return
new_country.states.create!(args)

Or even

campaign = User.first &. campaigns.create!(args)
return unless campaign

game_instance.players.create!(args)  &.
      countries.create!(args)  &.
      states.create!(args)

But I prefer not to use this programming style

-- Update 1

To clarify by it I mean these lines of code. Depending on what you do following this you might want to not put all the code in the same place.

\$\endgroup\$
3
  • \$\begingroup\$ Oh, now that's an answer! I'm not sure what is entailed by putting "it" in a separate method & early return...mainly because if you have a method called, you need to test it's return value before cascading down to the next call to activerecord object create or are you relying on the fact that when it passes the error - the next activerecord call will fail automatically? \$\endgroup\$ Commented Mar 2, 2017 at 17:47
  • \$\begingroup\$ create! will always return a valid, saved object or raise an exception. In the latter case it will bypass the following statements so the check is irrelevant. So in this case a check is not necessary except for User.find which can return nil \$\endgroup\$ Commented Mar 2, 2017 at 21:15
  • \$\begingroup\$ Since the create! method raises and exception on failure, there is no need to check the object it returns. If it fails, the next line doesn't get executed anyhow. \$\endgroup\$ Commented Mar 17, 2017 at 16:58
1
\$\begingroup\$

All of the ifs checking to make sure the object is created are superfluous and hurt readability. If the call to create! fails, it fails by raising an exception. This causes the application to come crashing to a halt, meaning the next line of code that uses the returned object doesn't even get executed.

All you really need is:

object2 = object1.relationship.create! args
object3 = object2.relationship.create! args
object4 = object3.relationship.create! args
object5 = object4.relationship.create! args
object6 = object5.relationship.create! args
object7 = object6.relationship.create! args
object8 = object7.relationship.create! args
object9 = object8.relationship.create! args

It's turtles, all the way down.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.