DEV Community

Kevin Murphy for The Gnar Company

Posted on • Updated on • Originally published at blog.thegnar.co

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")
Enter fullscreen mode Exit fullscreen mode

We can set the attributes after creating the object:

u = User.new
u.first_name = "Jordan"
u.last_name = "Knight"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Our new_user has the first name of Jonathan, resulting from the call to tap:

new_user.first_name
=> "Jonathan"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)