Today I Learned

hashrocket A Hashrocket project

270 posts about #rails surprise

saved_changes & previous_changes in Rails

In Ruby on Rails, saved_changes and previous_changes are methods used to track changes in Active Record models. saved_changes is used after an object is saved to the database. It provides a hash of all the attributes changed when persisting an object, including their original and final values. This method helps understand what changes have just persisted. On the other hand, previous_changes is used after an object is saved but before reloading. It holds the same information as saved_changes but becomes available only after the save operation and before the object is reloaded. It is helpful for actions triggered immediately after a save, like callbacks or logging changes.

Both methods are instrumental in tracking attribute changes and responding to them effectively in a Rails application.

Verify current password with has_secure_password

Now with Rails 7.1, has_secure_password can now automatically verify the current password when updating the password. This is useful to check if the user who is trying to update the password, knows the current password:

class User < ActiveRecord::Base

user = "sekret", password_confirmation: "sekret")
#=> true

user.update(password: "HAHAHA", password_challenge: "")
#=> false, challenge doesn't authenticate

user.update(password: "updated*sekret", password_challenge: "sekret")
#=> true

ActiveRecord's attribute_in_database method

Wanted to share a method that I learned about today.

ActiveRecord has a handy utility for checking the value of an attribute in the database. These methods are actually on the Dirty module, and as such, are intended to be used when checking during validations or callbacks before saving.

These particular methods will read from the database (as their name implies), instead of using the value currently in memory.

There's 2 different methods to be aware of that you can use like below -

class Invoice < ActiveRecord::Base
  validates :amount, presence: true

invoice = Invoice.last
=> 10.0

invoice.amount = 80.0

=> 10.0

=> 10.0

ActiveRecord::AttributeMethods::Dirty Docs

Health endpoint added by default in Rails 7.1

A newly generated Rails 7.1 app will now add an endpoint to your routes file to act as a heartbeat. You can point to many services or monitoring tools to check for downtime.

get "up" => "rails/health#show", as: :rails_health_check

However, this is just an indicator of the application running. If you want to do anything more advanced, like checking if the database is up... feel free to write your own. Ultimately, this is a great addition and will work in most situations.

Lib folder now auto-loaded again in Rails 7.1

Many years ago, the lib folder was removed from being auto-loaded in Rails. However, in Rails 7.1 it's back.

You can add autoload_lib to your config, and the lib folder will be added to your application's autoload paths. It does accept an ignore argument, which allows you to, of course, ignore folders in lib. When your new project is generated, it will add the assets & tasks folder to be ignored.

config.autoload_lib(ignore: %w(assets tasks))

There is a caveat that autoloading of lib is unavailable for Rails engines.

Pre-defined ActiveStorage variants in Rails 7.1

Rails 7.1 adds the ability to use pre-defined variants when calling preview or representation on an attachment.

class User < ActiveRecord::Base
  has_one_attached :file do |attachable|
    attachable.variant :thumb, resize_to_limit: [100, 100]

<%= image_tag user.file.representation(:thumb) %>

Here, we declare a thumb variant that can later be referenced—no need to explicitly express the resize_to_limit directly at the image_tag call.

Column alias for ActiveRecord select in Rails 7.1

Previously, when writing an ActiveRecord select and having the need to add a column alias, you'd have to provide a SQL fragment string like so:" AS customer_name")

=> SELECT AS customer_name FROM "customers"

Rails 7.1 added a more robust hash syntax for selects that also allows for column aliasing.

Customer.joins(:orders).select(name: :customer_name)

=> SELECT "customers"."name" AS "customer_name", FROM "customers"

Shorthand syntax for ActiveRecord select Rails 7.1

Using the newly available select syntax in Rails 7.1 we can express a SQL select like so:

Customer.joins(:orders).select(customers: [:name], orders: [:total])

=> SELECT "customers"."name", "orders"."total" FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id"

There's also some shorthand you can utilize to make it even shorter. We can still add column names as Symbols, as we've always been able to. They will reference the Model the select is being performed on.

Customer.joins(:orders).select(:name, orders: [:total])

=> SELECT "customers"."name", "orders"."total" FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id"

We removed the need to declare the customers table as it is implied from being called on the Customer relation.

Caveat: since declaring other columns is done via a hash, that must be provided as the last argument. You couldn't say something like .select(:id, orders: [:total], :name).

Hash syntax for ActiveRecord select in Rails 7.1

Previously, when writing an ActiveRecord select and wanted to add columns from anything outside of the model you originated from, you'd have to provide a SQL fragment string like so:


=> SELECT, FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id"

Notice above how we declare columns from the customers table and the orders table.

As of Rails 7.1, you can now stay in the ActiveRecord DSL and provide hash key/values. The query example above can directly be expressed as:

Customer.joins(:orders).select(customers: [:name], orders: [:total])

=> SELECT "customers"."name", "orders"."total" FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id"

You provide the table names as keys and the column names you want to select as Symbols in an Array.

Also, notice that the selected columns in the query are appropriately quoted vs the SQL fragment above.

Expanded routing info in Rails

When using rails and wanting to know how one of your routes plays out, it's very easy to do a quick cli call to rails routes. This gives an overview of all routes in the app.

> rails routes
Prefix             Verb  URI Pattern             Controller#Action
rails_health_check GET   /up(.:format)           rails/health#show
restaurants        GET   /restaurants(.:format)  restaurants#index {:format=>:json}                      

However if we provide the --expanded flag, we get a more verbose output with also tells you exactly where in the routes file it was declared.

> rails routes --expanded
--[ Route 1 ]--------------------------------------------------------------------
Prefix            | rails_health_check
Verb              | GET
URI               | /up(.:format)
Controller#Action | rails/health#show
Source Location   | config/routes.rb:6
--[ Route 2 ]--------------------------------------------------------------------
Prefix            | restaurants
Verb              | GET
URI               | /restaurants(.:format)
Controller#Action | restaurants#index {:format=>:json}
Source Location   | config/routes.rb:11

This could be exceptionally helpful when breaking up large routing files in Rails

Add a Default Format to Routes in Rails

When routing in Rails

# config/routes.rb

resources :users

sure you can respond to say json by going to the format directly (/users.json). What if you want that to always be the format without requesting it in the url? Normally by default we would get HTML if we declare nothing.

Turns out you can pass a defaults option to your routes with format being one of the things that can be accepted.

# config/routes.rb

resources :users, defaults: {format: :json}

Now requesting /users will render the json response as well as /users.json.

Importing Multiline Strings in Rails Config

Say I have a multiline string for an environment variable


If I try to import that in a Rails config yml file, I'm going to have a bad time.

  my_value: <%= ENV.fetch("MY_VAR") %>

If this config file is autoloaded, rails is going to blow up on startup:

YAML syntax error occurred while parsing config.yml. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Error: (<unknown>): could not find expected ':' while scanning a simple key at line 15 column 1 (Psych::SyntaxError)

This is happening because the ERB output is not writing the line breaks in a way that Psych (ruby YAML parser) knows how to handle. We can use String#dump to return the quoted version of the string to make Psych happy.


=> "\"hi\\nbye\""

So the resulting config would look like so:

  my_value: <%= ENV.fetch("MY_VAR").dump %>

And then our app can start and in console:


=> "hi\nbye"

Conditional link_to

In your Rails view, you can conditionally render links thanks to ActionView::Helper using #link_to_if

<%= link_to_if(current_user.nil?, "Login", login_path) %>

When this condition is true it would output:

<a href="/sessions/new/">Login</a>

When it is false it just outputs the text:


If you want a custom rendering for false values, just pass that into a block:

<%= link_to_if(current_user.nil?, "Login", login_path) do
  link_to("Logout", logout_path)
end %>

NOTE: There is also an inverse link_to_unless method available

Here's the docs if you're interested.

ActiveRecord Query with `and`

ActiveRecord has an and method for querying.

Post.where(id: [1, 2]).and(Post.where(id: [2, 3]))
# SELECT "posts".* FROM "posts" WHERE "posts"."id" IN (1, 2) AND "posts"."id" IN (2, 3)

More likely you'd write that by chaining wheres:

Post.where(id: [1, 2]).where(id: [2, 3])

However, and really shines if you have more complicated querying with nesting of ANDs and ORs:

SELECT "posts".* 
FROM "posts" 
WHERE "posts"."id" IN (1, 2) 
  AND ("posts"."title" = 'title'
    OR "posts"."body" IS NOT NULL)

How would you write this in ActiveRecord? A first pass with where and or doesn't get us what we want:

Post.where(id: [1,2])
  .where(title: 'title')
  .or(Post.where.not(body: nil))

# SELECT "posts".* 
# FROM "posts" 
# WHERE ("posts"."id" IN (1, 2) 
# 	AND "posts"."title" = 'title' 
# 	OR "posts"."body" IS NOT NULL)

Instead of A AND (B OR C) we get A AND B OR C. We can use a well-placed and to get the right grouping of conditions:

Post.where(id: [1,2])
  .and(Post.where(title: 'title')
    .or(Post.where.not(body: nil)))

# SELECT "posts".*
# FROM "posts"
# WHERE "posts"."id" IN (1, 2)
#   AND ("posts"."title" = 'title'
#     OR "posts"."body" IS NOT NULL)


h/t Craig Hafer

Add partial lookup prefixes in Rails

Utilizing partials in Rails can be super powerful, primarily when you use them in other templates. However, using a partial outside of its intended resource when it contains additional partials can give you a bad day.

# app/views/foos/_foo.html.erb

Foobity doo
<%= render "bar" %>
# app/views/dews/show.html.erb

Dewbie doo
<%= render "foos/foo" %>

After requesting the "dews/show" template, you'll probably find yourself with a disappointing error page saying the "bar" partial cannot be found.

Missing partial dews/_bar, application/_bar

As you can gather, partial lookup is trying to find the "bar" partial in the context it was called from. We declared the "foo" partial explicitly, so it did not error there. Since the "foo" partial calls the "bar" partial internally without being fully qualified, Rails is trying to find where it lives but cannot find it.

We can use a trick to help the template finder out. In your controller, we can define a local_prefixes method and add the prefix where the partial lives.

class DewsController < ApplicationController
  def self.local_prefixes
    super << "foos"

Rails can now find the sub-partial once you've added "foos" to the lookup context.

Excluding view templates for Spina resources

When creating a view_template in a Spina CMS Rails app there is an option of exclude_from that will take an array of strings. For any resources included in this array, that view template will not be available to pages of that resource.

Here is an example:

theme.view_templates = [
    # The rest of your view template...
    exclude_from: ["main", "blog_resource"]

Note: If you want to exclude templates from being used on main spina pages, you can just exclude from the implicit "main" resource.

Quote a SQL Value in Rails

If you saw my last post about Geocoding, you'll notice that the value passed to the geocode sql function is an address. To properly pass that value, we need to make sure that we quote it for SQL land.

❌ Bad

  from geocode('#{address}', 1)

Passing a mundane address like 100 O'Connel Ave will cause the above to throw an error in the database

But if we use the quote function from ActiveRecord, we properly quote our strings for SQL:

✅ Better

quoted_address = ActiveRecord::Base.connection.quote(address)

  from geocode(#{quoted_address}, 1)

Doing this ensures that we mitigate SQL Injection attacks and we properly account for things like single quotes in our values.

Rails config hosts accepts regular expressions

Imagine you're running your Rails server on an ngrok instance. If you want to access your ngrok instance, you're going to need to add your specific ngrok subdomain to your config hosts.

If you ever restart your ngrok instance and get a new subdomain, you now have to change your config hosts again to match the new one.

Well, not anymore! Because now that you know you can use regular expressions, you can just do this:

Rails.application.config.hosts << /.*\.ngrok\.app/

Technically, if your use-case is just for subdomains, you can just do this:

Rails.application.config.hosts << ""

But it's still nice to know you can utilize regular expressions if needed!

Comparison Validator Also Validates Presence

In ActiveModel, a comparison validation on an attribute will first perform a presence validation, and return a :blank error if the attribute is missing. So you don't have to explicitly add a presence validation if you're also doing a comparison validation (unless you really really want to (I probably still will)).

class SomeClass
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :some_number

    comparison: {greater_than: 8}

If you don't provide some_number you'll get a :blank error, and the comparison validation will not run:

pry(main)> foo =
pry(main)> foo.valid?; foo.errors.full_messages
=> ["Some number can't be blank"]

But if you do provide some_number, then the comparison validation will run as expected:

pry(main)> bar = 3)
pry(main)> bar.valid?; bar.errors.full_messages
=> ["Some number must be greater than 8"]


Bulk Delete Records in Rails with `delete_by`

I'm sure you're familiar with the Active Record method delete_all . But I recently learned of another method that can used to shorthand this method-call on a where statement.

You see - delete_by(...) is the shorthand for Model.where(...).delete_all

Model.where(foo: true, bar: false).delete_all

# vs.

Model.delete_by(foo: true, bar: false)

Similarly to delete_all, you will need to be careful with delete_by as it creates a single delete statement for the database and callbacks are not called.

Bonus round -> destroy_by is a similar shorthand for Model.where(...).destroy

Upload Active Storage Attachments to Specific Path

If you've ever wanted to organize your bucket while using Active Storage, it looks like this is now possible as of Rails 6.1

By passing the key argument to #attach, you can specify the location of the file within the bucket -

class Invoice < ApplicationRecord
  has_one_attached :document

invoice = Invoice.create
  key: "invoices/invoice_1_20230505.pdf",

You can turn arrays into sentences!

There's a neat little method Array#to_sentence that turns your array into a string in sentence format.

It's pretty self-explanatory, so here are some examples:

superheroes = ["Spider-Man", "Batman", "Iron man", "Wonder woman"]

=> "Spider-Man, Batman, Iron man, and Wonder woman"

  words_connector: " or ", 
  last_word_connector: " or maybe even "
=> "Spider-Man or Batman or Iron man or maybe even Wonder woman"

With slight configuration, you can even have this work in different locales. Check it out in the docs!

Disable "Try it out" feature of Swagger UI

Swagger UI is a great way to view API documentation. It's even got a cool feature where you can excercise the api right from the documentation.

However that may not be wanted or necessary in every occasion. Turning it off wasn't as straightforward as I'd have expected.

The Swagger UI documentation shows a configuration option called tryItOutEnabled. This sounds promising and the description even states:

Controls whether the "Try it out" section should be enabled by default.

So using our knowledge on how to configure Swagger UI through rswag, we try:

Rswag::Ui.configure do |c|
  c.config_object["tryItOutEnabled"] = false

However we'll find that this doesn't do anything 😕

Later I found that this particular option sets whether or not the "Try it out" feature is open by default.

If we utilize the config option of supportedSubmitMethods and provide an empty array, we'll get something more usable.

Rswag::Ui.configure do |c|
  c.config_object["supportedSubmitMethods"] = []

Now the button will completely disappear!

Configure Swagger UI when using rubygem rswag

The swagger documentation gem rswag contains the library Swagger UI. This allows your generated documentation to be viewable from your webserver.

It's great out of the box but is also more configurable than the gem's documentation would lead you to believe.

You have direct access to things like authentication but anything deeper than that can be controlled via a configuation object.

Rswag::Ui.configure do |c|
  c.config_object["showExtensions"] = true

Utilize the configuration's config_object. It is just a hash that you can set with keys that match available options found in Swagger UI's configuration docs

Custom Flash Messages in Rails

Why Aren't My Flash Messages Working?

Turns out, there's 2 keys that Rails supports by default for flash messages. Those are alert and notice; you can use them like this in your controller:

redirect_to users_path, notice: "User created successfully"
# - or -
render :new, alert: "An error prevented the user from being saved"

But if your flash rendering code is generic enough, you might notice that explicitly setting a key/message on flash will work for values other than the defaults:

flash[:success] = "User created successfully"
redirect_to users_path

TIL Rails has a helper that will allow us to add our own custom flash messages - add_flash_type. You can use this an a controller (re: ApplicationController) to enable new flash message types. This will allow you to do the one liners in render and redirect calls:

class ApplicationController < ActionController::Base
  add_flash_type :my_flash_type

# ...

redirect_to users_path, my_flash_type: "User created successfully"
# - or -
render :new, my_flash_type: "An error prevented the user from being saved"

In addition, it will also add a variable in our views to retrieve this message:

<%= my_flash_type %>

Disable strong parameters in rails

⚠️ Don't do thisYou should never do this. This was used for a non-public internal active admin application that needed to be updated. This app was not intended for public use. Please don't do this.
# config/environments/development.rb
require "active_support/core_ext/integer/time"

Rails.application.configure do
  config.action_controller.permit_all_parameters = true

Setting the url_options hash in the controller

When grabbing the url of an ActiveStorage record through any of these methods: ActiveStorage::Blob#url,ActiveStorage::Variant#url,ActiveStorage::Preview#url you may find yourself running into this error:

Cannot generate URL for <FILENAME> using Disk service, 
please set ActiveStorage::Current.url_options.

This can be resolved in the controller by utilizing a concern that ActiveStorage provides for us; ActiveStorage::SetCurrent.

It would look like this in your controller:

class MyController < ApplicationController
  include ActiveStorage::SetCurrent
  # The rest of your controller...

And now whenever you go to call #url on your ActiveStorage record, it will know where to generate the url from!

Including ActiveStorage::SetCurrent sets the ActiveStorage::Current.url_options hash to match the current request.

Rails' .insert_all method is too naive

Rails requires a unique index in order to use the .insert_all methods. This requirement can make this method very brittle and unusable. If your conflict target is the table's primary key, this won't work unless you create a redundant index on the table for this method to match against. This creates an amazing amount of waste not only of storage space, but also performance. This method would allow so many more use cases if it simply let you describe the conflict you want to match against.

More advanced method:

class ApplicationRecord
  def self.bulk_insert(array_of_hashes, conflict_targets = Array(primary_key))
    columns = array_of_hashes.first.keys
    values = array_of_hashes.flat_map(&:values)
    rows = do |f|
      "(#{ { "?" }.join(", ")})"
    end.join(", ")

    sql = sanitize_sql_array([<<~SQL, *values])
      INSERT INTO "#{table_name}"
      (#{ { |c| "\"#{c}\"" }.join(",")})
      VALUES #{rows}
      ON CONFLICT (#{ { |c| "\"#{c}\"" }.join(", ")}) DO NOTHING


SQL it produces:

User.bulk_insert([{email: ""}, {email: ""}])
INSERT INTO "users" ("email") VALUES (''), ('') ON CONFLICT ("id") DO NOTHING

This would then allow you to reference any conflict you like:

alter table users add unique (email);
  [{email: ""}, {email: ""}],

Direct many-to-many ActiveRecord associations

By using has_and_belongs_to_many you can directly relate models with a many-to-many-association. For example, if you have a model Film and a model Producer, a film can have multiple :producers, and a Producer can have multiple :films; in this case, each of models could have a has_and_belongs_to_many association with the other.

class Film < ApplicationRecord
  has_and_belongs_to_many :producers

class Producer < ApplicationRecord
  has_and_belongs_to_many :films
# Now associative records can be stored and retrieved
film = Film.where(title: "The Irishman")
producer = Producer.where(name: "Martin Scorcese")
film.producers << producer

#=> This should return a collection including The Irishman
#=> This should return a collection including Martin Scorcese

Confirmation alert using Turbo in Rails

Using an confirmation browser alert is a common thing to do. This seemingly works fine while using Turbo, however, try to cancel the alert and your request still goes through.

Previously on rails:

<%= link_to "Go away", "/go-away", data: {confirm: "Are you sure?"} %>

Use data-turbo-confirm for correct functionality:

<%= link_to "Go away", "/go-away", data: {turbo_confirm: "Are you sure?"} %>

The Outlet of Rails Stimulus

Besides values and targets Rails Stimulus has now outlets. Now we can invoke functions from one controller to the other like that:

// result_controller.js
export default class extends Controller {
  markAsSelected(event) {
    // ...
// search_controller.js
export default class extends Controller {
  static outlets = [ "result" ]

  selectAll(event) {
    this.resultOutlets.forEach(result => result.markAsSelected(event))

Encrypting database columns with Rails 7

In Rails 7, Active Record includes the option to encrypt database columns. To start run bin/rails db:encryption:init, then copy the resulting keys to your app's credentials.yml. Now in your model, you can tell Active Record to encrypt a column by using the encrypts which takes a db column name as an argument. For example:

class User < ApplicationRecord
  encrypts :super_secret_data

Active record will automatically decrypt the data upon retrieval. See more here.

ActiveStorage direct upload subfolders

When using S3, Rails does not let you configure subfolders for active storage. Every attachment lives at the root of your bucket.

This is not recommended, but cannot be helped

The only way to store attachments into subfolders is to monkey patch the direct uploads controller:

# config/initializer/direct_uploads_monkey_path.rb
Rails.application.config.to_prepare do
  class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController
    def create
      key = "#{sub_dir}/#{}/#{ActiveStorage::Blob.generate_unique_secure_token}"
      upload_attrs = {key:}.merge(blob_args)
      blob = ActiveStorage::Blob.create_before_direct_upload!(**upload_attrs)
      render json: direct_upload_json(blob)


    def user
      @user ||= User.enabled.find(session[:current_user_id])

    def sub_dir
      if /cool-uploads/.match?(request.referer)
      elsif /company-settings/.match?(request.referer)

Select options with option groups

ActionView::Helpers::FormOptionsHelper#grouped_options_for_select lets you pass in a nested array of strings, and returns a string of option tags wrapped with optgroup tags:

The first value serves as the optgroup label while the second value must be an array of options.

grouped_options = [
  ["North America",
    ["United States", "Canada"]],
    ["Denmark", "Germany", "France"]]

Next, simply call it from a form helper in the view, passing in your grouped options.

<%= :location, grouped_options_for_select(grouped_options) %>

Voila! You should now have something like this: image

How to add hidden fields to button_to

The button_to helper take a params key to add hidden fields to the form it renders:

<%= button_to "Delete", users_path, method: :delete, params: {send_email: false} %>


<form class="button_to" method="post" action="/users">
<input type="hidden" name="_method" value="delete" autocomplete="off">
<button type="submit">Delete</button>
<input type="hidden" name="authenticity_token" value="8nOHRzvQIev3XHW6YrEJUIohVGLm0PKOPNly8ovPdDtF75eyBj5Rvz_1FzDmVybbJ3YyfC7YExtfRQC3_H5NNw" autocomplete="off">
<input type="hidden" name="send_email" value="false" autocomplete="off">

Truncate by Word Count in Rails

Rails has a convenient method for truncating strings based on word count.

my_string = "Hello World you are now reading a til post"
#=> "Hello World..."

The method automatically adds ... to the end of the string to indicate that the string has been shortened. You can customize this omission by passing an omission argument.

my_string.truncate_words(2, omission: "... (
#=> "Hello World... (read more)"

Add custom flash keys

ActionController::Base has a default flash types of [:notice] allowing you to pass a key to #redirect_to:

class Controller < ActionController::Base
  def create
    if true
      redirect_to root_path, status: :see_other, notice: "Created"
      flash[:error] = "Could not create"
      redirect_to root_path, status: :see_other

However, you can add :error as a custom type to get the convenience argument:

class Controller < ActionController::Base
  add_flash_types :error

  def create
    if true
      redirect_to root_path, status: :see_other, notice: "Created"
      redirect_to root_path, status: :see_other, error: "Could not create"

Attaching Fixture Files in Rails Model Tests

If you want to attach fixture files in a model test. Assuming your ActiveStorage association is already set up, if it's not set up, check this out, then follow these steps:

  1. Make sure your desired fixture file has been placed in your test/fixtures/files folder
  2. Attach the fixture to the model instance by providing the .attach method with a hash including an IO object and the name of the file you wish to attach.

It should look something like this:

@object.image.attach(io:'test/fixtures/files/filename'), filename: 'filename')

Enforce TLS... except for health checks

Many infrastructure stability platforms will need to check the health of a rails application directly, not through a load balancer. Because many applications don't terminate TLS directly (because it's delegated to the load balancer) a health check endpoint must adhere the the force_ssl = true config option, but without TLS, causing a failure.

Rails 7 has an option to work around this (config.ssl_options):

# config/environments/production.rb
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
config.ssl_options = {
  redirect: {exclude: ->(request) { /healthz/.match?(request.path) }}

Limiting object counts in rails associations

Let's say you have a model owner and a model pet. Every owner has_many pets, but you want to limit the number of pets an owner can have. You can use the following validation in your model to make sure these owners don't get greedy with their number of pets.

has_many :pets
validates :pets, length: { maximum: 5 } 

The length helper is telling rails to only allow an owner to have a maximum of 5 pets. This is a little awkward because the length helper usually pertains to enforcing a minimum or maximum length on a string attribute, but it still works on the ActiveRecord Association Collection of :pets in a similar way where it is basically validating the maximum size of the collection.