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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Mar 1, 2017 at 18:40
\$\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\$ 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.

answered Mar 2, 2017 at 17:36
\$\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.

answered Mar 17, 2017 at 17:01
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.