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
-
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– Vogel6122017年03月01日 21:17:09 +00:00Commented Mar 1, 2017 at 21:17
2 Answers 2
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.
-
\$\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\$Mirv - Matt– Mirv - Matt2017年03月02日 17:47:06 +00:00Commented 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 forUser.find
which can return nil \$\endgroup\$Marc Rohloff– Marc Rohloff2017年03月02日 21:15:35 +00:00Commented 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\$Greg Burghardt– Greg Burghardt2017年03月17日 16:58:09 +00:00Commented Mar 17, 2017 at 16:58
All of the if
s 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
Explore related questions
See similar questions with these tags.