Today I Learned

A Hashrocket project

226 posts about #rails

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"
my_string.truncate_words(2) 
#=> "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: "... (read more)")
#=> "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"
    else
      flash[:error] = "Could not create"
      redirect_to root_path, status: :see_other
    end
  end

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"
    else
      redirect_to root_path, status: :see_other, error: "Could not create"
    end
  end

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: File.open('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.

Rails TimeHelpers

Rails ActiveSupport testing library includes some really helpful methods for manipulating time.

Here’s a cool one:

travel_to Time.new(2022, 9, 14) do  
    #everything inside this block is now happening as if it is 9-14-2022

end
# afterwards we return to the present

This way you can test all sorts of time-based features by jumping back and forth through time.

Trippy 🔮

📧 Change the delivery method of a mailer

You can change the delivery method of a mailer with the default method:

Rails.application.config.action_mailer.delivery_method
=> :smtp

class LocalMailer < ApplicationMailer
  default delivery_method: :sendmail

  def notify_root
    mail({to: "root@localhost", subject: "Alert"})
  end
end

LocalMailer.notify_root.deliver_now

Then check the inbox:

sudo su root
mail
Mail version 8.1 6/6/93.  Type ? for help.
"/var/mail/root": 1 message 1 new
>N  1 no-reply@rails-app.example.com     Fri Aug 19 17:29 494/19390 "Alert"
?

Batch Active Record Operations with `find_each`

Iterating over a collection of ActiveRecord models can incur a performance hit to your application, especially when the result set is large.

A basic query like all or where could return tens of thousand of records. When that query returns, it will instantiate your records all at once, loading them into memory, and potentially leading to instability. Luckily if we need to operate on a large result set, we can use the find_each method to work in batches on our query.

find_each works by using the find_in_batches method under the hood, with a default batch size of 1000.

# This is bad ❌
Customer.where(active: true).each do |customer|
  # ....
end

# This is good ☑️
Customer.where(active: true).find_each do |customer|
  # ....
end

If you need to, you can also adjust the batch size by passing the batch_size kwarg to find_each

Customer.where(active: true).find_each(batch_size: 2000) do |customer|
  # ...
end

https://devdocs.io/rails~6.1/activerecord/batches#method-i-find_each

H/T https://rails.rubystyle.guide/

Nested table querying with ActiveRecord

Sometimes you need to query a joined table in ActiveRecord. This can be easily done via the hash syntax:

User.where(greetings: {text: "HOWDY"}).to_sql

=> SELECT "users".* FROM "users" WHERE ("greetings"."text" = 'HOWDY')

Note: the key here (greetings) is pluralized because we’re referencing a table name and not a potential relationship that ActiveRecord knows about.

Note: I’ve ommited a join to the greetings table that would have this query make sense

Cool: so we can do that but there’s even an alternative way to handle this using dot notation

User.where("greetings.text": "HOWDY"}).to_sql

=> SELECT "users".* FROM "users" WHERE ("greetings"."text" = 'HOWDY')

Under the hood, ActiveRecord will convert dots from the string key and interpret them as table and column names.

Start multiple processes in dev env with bin/dev

Rails comes with bin/dev to start multiple processes from a Procfile.dev

# Procfile.dev
web: bin/rails server -p 3000
css: bin/rails tailwindcss:watch
jobs: bin/good_job
firebase: firebase emulators:start --import=./emulator-data

So that you can run everything at once:

$ bin/dev
08:36:10 web.1  | started with pid 40038
08:36:10 css.1  | started with pid 40039
08:36:10 firebase.1 | started with pid 40040
08:36:11 firebase.1 | i  emulators: Starting emulators: auth, firestore, storage
08:36:11 firebase.1 | i  firestore: Firestore Emulator logging to firestore-debug.log
08:36:11 web.1  | => Booting Puma
08:36:11 web.1  | => Rails 7.0.2.3 application starting in development
08:36:11 web.1  | => Run `bin/rails server --help` for more startup options
08:36:12 web.1  | Puma starting in single mode...
08:36:12 web.1  | * Puma version: 5.6.4 (ruby 3.1.2-p20) ("Birdie's Version")
08:36:12 web.1  | *  Min threads: 5
08:36:12 web.1  | *  Max threads: 5
08:36:12 web.1  | *  Environment: development
08:36:12 web.1  | *          PID: 40038
08:36:12 web.1  | * Listening on http://127.0.0.1:3000
08:36:12 web.1  | * Listening on http://[::1]:3000
08:36:12 web.1  | Use Ctrl-C to stop
08:36:12 css.1  |
08:36:12 css.1  | Rebuilding...
08:36:12 css.1  | Done in 300ms.

Make numbers more readable in a Rails view

Rails has a number of handy helper methods.

Specifically, in ActionView::Helpers::NumberHelper there is the method number_with_delimiter

If you want to display the number 100000 and have people know right away whether it is 1 million or 100 thousand, just pass it to number_with_delimiter and it will return "100,000".

Some useful optional keyword parameters:
delimiter:"🍔" replaces the default , delimiter. (100000 becomes 100🍔000)
separator:"🍕" replaces the default . separator. (100.56 becomes 100🍕56)

Prevent rails' file server from serving index.html

You can prevent rails’ file server from serving index.html, while continuing to serve other files from the public directory by changing the index_name:

class Application
  config.public_file_server.index_name = "other-index.html"
end

Rails.application.routes.draw do
  get :dashboard, to: "application#dashboard"
  root to: redirect("/dashboard")
end

Visiting the root path / will no longer serve public/index.html if you define another route for /

Implicit order column

ActiveRecord has a class method implicit_order_column= that allows you to override the behavior of the .first and .last methods.

class User < ApplicationRecord
  self.implicit_order_column = "email"
end

User.first
User Load (3.2ms)  SELECT "users".* FROM "users" ORDER BY "users"."email" ASC, "users"."id" ASC LIMIT $1  [["LIMIT", 1]]

Get database value of model instance

AR instances have a method #attribute_in_database. Sometimes things don’t make sense, someone has created a method with the same name as the column:

create table users (id int, email citext);
class User < ApplicationRecord
  def email
    Time.current.strftime("%A, %B %C")
  end
end

User.first.email
=> "Thursday, March 20"

But you actually wan the email:

User.first.attribute_in_database("email")
=> "admin@example.com"

Rails composed_of

I learned that Rails ActiveRecord has composed_of method which allow us to group model attributes under a common PORO like object. Let’s say we have an User with address_street and address_city fields, stored in the same table, so we can still group the address attributes:

class User < ActiveRecord::Base
  composed_of :address, mapping: [%w(address_street street), %w(address_city city)]
end

This way, besides the user.address_street and user.address_city, we can also access the same values as:

address = user.address
puts address.street
puts address.city

Rails AR readonly!

ActiveRecord has some readonly features that marks a model (or relation) not to be updated. This way if we try:

irb> user = User.last
=> #<User id: 123, ...>

irb> user.readonly!
=> true

irb> user.update(updated_at: Time.current)
   (0.3ms)  SAVEPOINT active_record_1
   (0.3ms)  ROLLBACK TO SAVEPOINT active_record_1
ActiveRecord::ReadOnlyRecord: User is marked as readonly

Then we get a rollback with the readonly error.

An interesting thing is that the touch AR method does not respect this readonly feature (I tested on rails 6.0.4.1), check this out:

irb> user = User.last
=> #<User id: 123, ...>

irb> user.readonly!
=> true

irb> user.touch
   (0.3ms)  SAVEPOINT active_record_1
  User Update (1.4ms)  UPDATE "users" SET "updated_at" = $1 WHERE "users"."id" = $2  [["updated_at", "2022-03-08 20:39:40.750728"], ["id", 123]]
   (0.2ms)  RELEASE SAVEPOINT active_record_1
=> true

Easy conditional style class names in Rails

How many times do you see something like this?

<%= 
  link_to("Somewhere", "#",
    class: "class-1 class-2#{" class-3" if true}"
  )
%>

# <a href="#" class="class-1 class-2 class-3">Somewhere</a>

Gross right!?

Take advantage of a token list instead.

It’s also got a great alias in class_names

<%= 
  link_to("Somewhere", "#", 
    class: class_names("class-1 class-2", {"class-3": true})
  )
%>

# <a href="#" class="class-1 class-2 class-3">Somewhere</a>

As you see it can be passed different types and still generates down to a string list.

It works great with the current_page helper.

<%= 
  link_to("Home", root_path, 
    class: class_names({"active": current_page?(root_path)})
  )
%>

# <a href="/" class="active">Home</a>

Rails ActiveRecord count on groups

Rails ActiveRecord can count queries with GROUP BY clauses, and in this case the result is not just an integer, but a Hash with the grouped values and the count for each group. Check this out:

Project.group(:status, :city).count
# SELECT COUNT(*) AS count_all, "projects"."status" AS projects_status, "projects"."city" AS projects_city
# FROM "projects"
# GROUP BY "projects"."status", "projects"."city"
=> {
  [:pending, "Jacksonville Beach"] => 21,
  [:finished, "Jacksonville Beach"] => 1061
  [:pending, "Chicago"] => 10,
  [:finished, "Chicago"] => 980,
}

So rails manipulates the select statement to have all grouped by fields and the count(*) as well, which is pretty neat.

Rails scopes might just return all resources

ActiveRecord’s scopes are meant to be composable and intended to only ever return an ActiveRecord relation.

If you make a mistake with your scope and have it return something like a nil or false, Rails will return all records for that class in order to maintain composability.

If you are intentionally writing something that might return an empty value, use a class method rather than adding a scope in order to prevent bugs

Comparison Validate more than Numbers in Rails

As of Rails 7 we can now use comparisions in validations for more than just numbers!

Previously we could do this but only against an integer

class Procedure
  validates :appointment_count, numericality: { less_than: 5 }
end

Now we can compare against many other types. Here is a quick example validating that an end date is further in the future than a start date.

class Procedure
  validates :start_date, comparison: { greater_than: ->(_) { Date.current }
  validates :end_date, comparison: { greater_than: :start_date }
end

Cool thing is you can pass a Proc or Symbol. A symbol can represent a method on the class. The Proc can represent anything. Its argument is the instance of the class.

For more info, check out this PR.

Always declare columns for SQL query in Rails

When ignoring a column in an ActiveRecord query you’ll receive a query that declares the column names of the table explicitly versus using *.

What if you do not want to ignore a column to get this functionality? Rails 7 will introduce an ActiveRecord class attribute to do just this.

class Procedure < ApplicationRecord
  self.enumerate_columns_in_select_statements = true
end
Procedure.all

=> SELECT "procedures"."id", "procedures"."name", "procedures"."created_at", "procedures"."updated_at" FROM "procedures"

When enumerate_columns_in_select_statements is set to true, ActiveRecord SELECT queries will always include column names explicitly over using a wildcard. This change was introduced to provide consistency in query generation and avoid prepared statment issues.

Note: it can be declared at the app configuration level as well.

module MyApp
  class Application < Rails::Application
    config.active_record.enumerate_columns_in_select_statements = true
  end
end

With that change, all ActiveRecord queries will avoid the wildcard.

Ignore columns on ActiveRecord queries in Rails

ActiveRecord models have ignored_columns defined as a class attribute. This allows us to, just like it says, ignore a particular column from our queries and returning ActiveRecords.

class Procedure < ApplicationRecord
    self.ignored_columns = [:created_at, :updated_at]
end

If we perform a lookup, this is what we’ll now see.

Procedure.all

=> SELECT "procedures"."id", "procedures"."name" FROM "procedures"

Notice the subtle difference?

Without the ignored_columns declaration, we’d see a query like this:

Procedure.all

=> SELECT "procedures".* FROM "procedures"

Using NULLS FIRST / NULLS LAST ordering in Rails

Methods nulls_first & nulls_last were added to Arel in Rails 6.1 for PostgreSQL and in most other databases in Rails 7.

In an earlier TIL, I showed how to order using Arel.

Procedure.order(Procedure.arel_table[:name].desc.nulls_first)

=> SELECT "procedures".* FROM "procedures" ORDER BY "procedures"."name" DESC NULLS FIRST
Procedure.order(Procedure.arel_table[:name].desc.nulls_last)

=> SELECT "procedures".* FROM "procedures" ORDER BY "procedures"."name" DESC NULLS LAST

Here the desc method is wrapping the Arel attribute in a node so that it can be utilized in order. nulls_first/nulls_last is just wrapping the previous ordering node.

Procedure.arel_table[:name].desc.nulls_first

=> 
#<Arel::Nodes::NullsFirst:0x00007fb73ebf18f8
 @expr=
  #<Arel::Nodes::Descending:0x00007fb73ebf1920
   @expr=
    #<struct Arel::Attributes::Attribute
     relation=
      #<Arel::Table:0x00007fb735a02dc0
       @klass=Procedure(id: integer, name: string, created_at: datetime, updated_at: datetime),
       @name="procedures",
       @table_alias=nil,
       @type_caster=
        #<ActiveRecord::TypeCaster::Map:0x00007fb735a02cd0
         @klass=Procedure(id: integer, name: string, created_at: datetime, updated_at: datetime)>>,
     name="name">>>

I mention this as you need to define the order before utilizing the nulls methods.

Using Arel in ActiveRecord ORDER Queries

I utilize Arel in ActiveRecord where all the time but it never occured to me that order can also take an Arel node.

Procedure.order(Procedure.arel_table[:name].desc)

=> SELECT "procedures".* FROM "procedures" ORDER BY "procedures"."name" DESC

For such a trivial example I’d say that it’s much clearer to just use ActiveRecord in this case.

Procedure.order(name: :desc)

=> SELECT "procedures".* FROM "procedures" ORDER BY "procedures"."name" DESC

Cool piece of knowledge for possible use in a more complex ordering case.

You MUST use a ws protocol in rails💩actioncable

Rails’ actioncable library is a bit immature compared to other implementations, so there are a lot of rough edges to work around. One of those is the basic createConsumer function.

If your app is running without a DOM (nodejs), the node_module @rails/actioncable is going to fight you.

The rails guides recommend this:

createConsumer('https://ws.example.com/cable')

But that function relies on having a global document that can create <a> tags, which you won’t have in many contexts (node, react-native, etc.)

Also, why does a websocket library depend on HTML anchor tags?

You can work around this limitation by explicitly using a ws or wss protocol:

createConsumer('ws://localhost:3000/cable')

Custom URL helpers in Rails

Ever want a named route for something that’s not necessarily a resource in your Rails app?

In your any of your route files you can utilize direct.

# config/routes.rb

Rails.application.routes.draw do
  get 'foo', to: 'foo#bar'

  direct :get_to_the_goog do
    "https://google.com"
  end
end

This gives a nice named route of get_to_the_goog_url!

This can be even more useful as the return value of the block has to be valid arguments that you would pass to url_for. So you can use pretty much anything you’d normally use to build a url helper.

Let’s modify this just a bit to be more useful.

# config/routes.rb

Rails.application.routes.draw do
  get 'foo', to: 'foo#bar'

  direct :get_to_the_goog, search: nil do |options|
    "https://google.com/search?q=#{options[:search]}"
  end
end

Now we can call get_to_the_goog_url(search: "TIL").

Pretty cool… just take note of a few caveats. You get access to the _path version of the named helper but even if the helper is using a hard coded string (like our example above), it will remove the domain info (rightfully so). Also you are not able to use the direct functionality inside of a namespace or scope, however it will raise an error if you do… so that’s nice.

Break up large routing files in Rails

Breaking apart a routing file could be useful if you have large a different concepts in your app with their own larger route sets.

To do this you can utilize draw

# config/routes.rb

Rails.application.routes.draw do
  get 'foo', to: 'foo#bar'

  draw(:admin)
end

This will try to load another route file at config/routes/admin.rb. That file is just another normal routing file.

# config/routes/admin.rb

namespace :admin do
  resources :users
end

How to use ActiveRecored.pluck without Arel.sql

It is too cumbersome to remember to wrap every string in a class method, so this is a shortcut:

# app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  module ::ActiveRecord::Sanitization::ClassMethods
    define_method(:original_disallow_raw_sql!, instance_method(:disallow_raw_sql!))

    def disallow_raw_sql!(args, permit: connection.column_name_matcher) # :nodoc:
      original_disallow_raw_sql!(args.map { |a| a.is_a?(String) ? Arel.sql(a) : a }, permit: permit)
    end
  end
end

Then you can write sql without the class methods

This can be easier to write:

User.where(payment_due: true)
  .pluck(Arel.sql("coalesce('last_billed_date', 'start_date')"))

Can now just be written as:

User.where(payment_due: true)
  .pluck("coalesce('last_billed_date', 'start_date')")

This can also be useful for .order as well

Subtlety between Rails' #try & Ruby's #&.

Many would say that the safe navigation operator (&.) in Ruby is a drop in replacment for Rails’ #try. However I came across an interesting case where they are not the same.

thing = nil
thing&.something
> nil
thing = nil
thing.try(:something)
> nil

Seems pretty much the same right?

However what happens when the object receiving the message is not nil but does not respond to the message?

thing = Thing.new
thing&.something
> NoMethodError: undefined method 'something' for <Thing:0x0000>
thing = Thing.new
thing.try(:something)
> nil

Hopefully not something you’ll run into often, but definitely be aware of it. It makes the safe navigation operator not necessarily a drop in replacment.

Rails has helpers for uploading spec fixture files

Instead of writing your own helper method in specs:

module HelpMePlease
  def uploaded_file(path)
    Rack::Test::UploadedFile.new(Rails.root.join("spec/fixtures", path))
  end
end

Rails has a built in helper method:

require "rails_helper"

RSpec.describe HelpNeeded do
  describe "something" do
    it "sends the file" do
      post :change_avatar, params: { avatar: fixture_file_upload("spongebob.png", "image/png") }
    end
  end
end

Finding Rails ActiveModel Errors

Rails allows to filter ActiveModel erros by field name and type with the where method. Check this out:

user = User.new(email: "user@test.com")
user.valid?

user.errors.where(:email).map(&:full_message)
# => [
#      "Email has already been taken",
#      "Email is too short (minimum is 20 characters)"
#    ]

user.errors.where(:email, :taken).map(&:full_message)
# => ["Email has already been taken"]

Rails delegated_type

Rails 6.1 added delegated_type for the ones who like polymorphic relations. So in addition to set a regular polymorphic relation in rails you can also call delegated_type method to inject a bunch of useful methods and scopes for you. Check this out:

class Content < ApplicationRecord
  # belongs_to :contentable, polymorphic: true
  delegated_type :contentable, types: %w[ TilPost BlogPost ]
end

class TilPost < ApplicationRecord
  has_one :content, as: :contentable
end

class BlogPost < ApplicationRecord
  has_one :content, as: :contentable
end

And this will produce helper methods like:

content.contentable_class
# => +TilPost+ or +BlogPost+
content.contentable_name
# => "til_post" or "blog_post"

Content.til_posts
# => Content.where(contentable_type: "TilPost")
content.til_post?
# => true when contentable_type == "TilPost"
content.til_post
# => returns the til_post record, when contentable_type == "TilPost", otherwise nil
content.til_post_id
# => returns contentable_id, when contentable_type == "TilPost", otherwise nil

Rails 6 `create_or_find_by`

Rails 6 comes with a new ActiveRecord method create_or_find_by.

User.create_or_find_by(first_name: 'Vinny') do |user|
  user.status = 'pending'
end
# => #<User id: 1, first_name: "Vinny", status: pending>

This method is similar to the already existing find_or_create_by method by it tries to create the model in the database first, then if an unique constraint is raised by the DB then rails rescue the error, it rollbacks the transaction and it finally finds the model.

The idea behind to use this new method instead is to avoid race condition between a find and a create calls to the db.

There’s a few downsides to this process, but one that caught my eyes was that now we’re seeing a lot of ROLLBACK messages in the rails logs, which is the expected behavior now.

When to set `inverse_of` in Rails AR

Rails ActiveRecord does not auto infer bi-directional associations if some of the associations contains a scope or any of the following through or foreign_key options.

class Author < ApplicationRecord
  has_many :books
end

class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
irb> a = Author.first
irb> b = a.books.first
irb> a.first_name = 'David'
irb> a.id = b.writer.id
=> true
irb> a.object_id = b.writer.object_id
=> false
irb> a.first_name == b.writer.first_name
=> false

To solve this issue we should use inverse_of option, check this out:

class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end

class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
irb> a = Author.first
irb> b = a.books.first
irb> a.first_name = 'David'
irb> a.id = b.writer.id
=> true
irb> a.object_id = b.writer.object_id
=> true
irb> a.first_name == b.writer.first_name
=> true

Conditionally Render Rails Links with `link_to_if`

ActionView::Helpers contains some really handy helpers for conditionally rendering links. For example, the link_to_if method will render the link if the given condition is met; otherwise it renders just the text.

<%= link_to_if(current_user.present?, "Admin Panel Tools", admin_tools_path) %>

When current_user.present? is true, yields the following HTML:

<a href="/admin_tools">Admin Panel Tools</a>

But when current_user.present? is false, yields the following:

Admin Panel Tools

https://api.rubyonrails.org/v6.0.2.1/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to_if

Note, there are also methods for link_to_unless and link_to_unless_current