Making New ActiveRecord Models (Let's Try It Again)
If we want to make a new instance of an ActiveRecord model with particular attributes, we have a number of options.
We can pass the attributes in as a hash:
u = User.new(first_name: "Jordan", last_name: "Knight")
We can set the attributes after creating the object:
u = User.new
u.first_name = "Jordan"
u.last_name = "Knight"
And there's a third option - we can also pass new
a block:
u = User.new do |user|
user.first_name = "Jordan"
user.last_name = "Knight"
end
When Could We Use This? (The Block)
Let's say we have a system that members of a band use to check their tour schedule. Band members are users, and when we add a member, we want to make a user for them.
def add_member(first_name:, last_name:)
@members << User.new(first_name: first_name, last_name: last_name)
end
Additionally, users have a username attribute, and we want to keep that unique within a given band. We also want the system to define the username when we add a band member.
def add_member(first_name:, last_name:)
username = "#{band_name}_#{last_name}"
if @members.pluck(:last_name).include?(last_name)
username << unique_value
end
@members << User.new(
first_name: first_name,
last_name: last_name,
username: username,
)
end
def unique_value
...
end
However, if we prefer the aesthetic, we can also define those attributes in a block:
def add_member(first_name:, last_name:)
@members << User.new do |user|
user.first_name = first_name
user.last_name = last_name
user.username = "#{band_name}_#{last_name}"
if @members.pluck(:last_name).include?(last_name)
user.username << unique_value
end
end
end
Seeing The Result (The Right Stuff)
Let's check our work to see the usernames of our band members.
[1] pry(main)> band = Band.new("New Kids on the Block")
[2] pry(main)> band.add_member(first_name: "Jordan", last_name: "Knight")
[3] pry(main)> band.add_member(first_name: "Donnie", last_name: "Wahlberg")
[4] pry(main)> band.add_member(first_name: "Jonathan", last_name: "Knight")
[5] pry(main)> band.members.pluck(:username)
=> ["New_Kids_on_the_Block_Knight",
"New_Kids_on_the_Block_Wahlberg",
"New_Kids_on_the_Block_Knight_65"]
Jonathan's username has additional characters appended to it, as Jordan already claimed the username "New_Kids_on_the_Block_Knight"
.
Tap Dance (Step By Step)
If you're familiar with Ruby's tap
method, you might be wondering what all the fuss is about. We can do the same thing with tap
:
def add_member(first_name:, last_name:)
@members << User.new.tap do |user|
user.first_name = first_name
user.last_name = last_name
user.username = "#{band_name}_#{last_name}"
if @members.pluck(:last_name).include?(last_name)
user.username << unique_value
end
end
end
This works with any Ruby object, not just those that inherit from ActiveRecord::Base
, so why bother with having to know if we can pass a block to new
or not, based on what the object inherits from?
That's fair, but new
is not the only ActiveRecord method that takes a block. Others include create
, build
, and find_or_initialize_by
. There the differences with tap
start to show:
new_user = User.create(first_name: "Jordan", last_name: "Knight").tap do |u|
u.first_name = "Jonathan"
end
Our new_user
has the first name of Jonathan, resulting from the call to tap
:
new_user.first_name
=> "Jonathan"
However, that's only persisted in memory - not in the database. What we stored in the database is what we passed to create
.
new_user.reload.first_name
=> "Jordan"
We can also pass a block to create
directly:
new_user = User.create(first_name: "Jordan", last_name: "Knight") do |u|
u.first_name = "Jonathan"
end
And in that case, the first name of the user in memory and in the database is Jonathan.
new_user.first_name
=> "Jonathan"
new_user.reload.first_name
=> "Jonathan"
Why would we mix setting attributes with create
both by passing a hash and a block, either with or without tap
? Other than to explain quirks and differences in what method you're passing a block to, I am also interested in knowing. If you have real-world use cases, let me know!
Finding Blocks in Rails Source Code (Face the Music)
If you're curious about where in Rails' source code new
is set up to take a block, we can start by looking in ActiveRecord::Base
. As of the time this article was published, there's not much implementation in that class. Instead, we have to look in the Core
module to find the initialize
method that takes a block.
Initializing an ActiveRecord model with a block is also defined in the documentation.
Thanks for hangin' tough to the end of this article. I hope you learned a thing or two about passing blocks to ActiveRecord methods.
This post originally published on The Gnar Company blog.
Learn more about how The Gnar builds Ruby on Rails applications.
Top comments (0)