ActiveRecord's New Takes a Block, Kid
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.