Today I Learned

hashrocket A Hashrocket project

248 posts about #rails surprise

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

https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-delete_by

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
end

invoice = Invoice.create
invoice.document.attach(
  key: "invoices/invoice_1_20230505.pdf",
  io: File.read("invoice_1.pdf")
)

https://github.com/rails/rails/commit/4dba136c83cc808282625c0d5b195ce5e0bbaa68 https://github.com/rails/rails/issues/32790

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"]

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

superheroes.to_sentence(
  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
end

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"] = []
end

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
end

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
end

# ...

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 %>

https://api.rubyonrails.org/classes/ActionController/Flash/ClassMethods.html

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
end

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...
end

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 = array_of_hashes.map do |f|
      "(#{columns.size.times.map { "?" }.join(", ")})"
    end.join(", ")

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

    connection.execute(sql.squish)
  end
end

SQL it produces:

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

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

alter table users add unique (email);
User.bulk_insert(
  [{email: "a@example.com"}, {email: "b@example.com"}],
  %i[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.

#film.rb
class Film < ApplicationRecord
  has_and_belongs_to_many :producers
end

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

producer.films
#=> This should return a collection including The Irishman
films.producers
#=> 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
end

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}/#{user.id}/#{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)
    end

    private

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

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

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"]],
  ["Europe",
    ["Denmark", "Germany", "France"]]
]

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

<%= f.select :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} %>

Produces:

<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">
</form>

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.