Today I Learned

hashrocket A Hashrocket project

300 posts about #rails surprise

Rails Environment Task

In a Rails task if you need to load in your rails application (to get access to your models, etc.), you have to call :environment in the task:

task task_name: :environment do 
  ...
end

I kind of always took this for granted, and never thought much about it. Then, in Xavier Noria's 2024 Rails World talk he mentioned that :environment itself is a task - and that syntax is actually saying your task depends upon :environment and run that task before your task runs. šŸ¤Æ

So I decided to look up what the environment task actually does:

task :environment do
  ActiveSupport.on_load(:before_initialize) { config.eager_load = config.rake_eager_load }

  require_environment!
end

require_environment! ... requires your environment, specifically it requires your config/environment.rb which runs Rails.application.initialize! - which is what actually starts your rails app.

Cool!

Generate Rails Models with Namespaces

You can use the rails model generator to generate namespaced models. I always have a hard time remembering the syntax for namespaces, but it's pretty straightforward.

Say I want to create a model Blog::Post. The generator for this is:

rails generate model blog/post title:string ...

This will generate the following files (assuming you're using rspec and FactoryBot)

invoke  active_record
create    db/migrate/20241028193321_create_blog_posts.rb
create    app/models/blog/post.rb
create    app/models/blog.rb
invoke    rspec
create      spec/models/blog/post_spec.rb
invoke      factory_bot
create        spec/factories/blog/posts.rb

If the namespace already exists, you'll be prompted to either overwrite or keep the existing app/models/blog.rb file (you'll probably want to keep the existing one).

Happy generating!

Create index with migration shorthand in Rails

Using the Ruby on Rails migration generator is great to get up and running but always seems to require just a little bit more effort once the migration file is created.

There is a shorthand syntax for the cli interface of the generator but I can never remember it past a column name and type... so I'm putting this here to remember it for all of us!

To create an index just continue adding a bit more information to your column declaration. Let's start with this as a base so we're on the same page:

rails generate add_name_to_widgets name

This will create this migration:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
  end
end

Now by making a minor adjustment to the original cli call we can generate the index too:

rails generate add_name_to_widgets name:string:index
class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
    add_index :widgets, :name
  end
end

And you know what? We can do unique indexes as well:

rails generate add_name_to_widgets name:string:uniq
class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
    add_index :widgets, :name, unique: true
  end
end

Generate non-null field migration in Rails 8

I'm sure many of us are familiar with the Ruby on Rails migration generator...

rails generate migration add_name_to_widgets name

which generates a migration that looks like so:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string
  end
end

Then to make the name column have a non-null constraint an addition of null: false would be needed:

class AddNameToWidgets < ActiveRecord::Migration[7.0]
  def change
    add_column :widgets, :name, :string, null: false
  end
end

However, now in Rails 8 an adjustment was made to allow this to be done from the generator shorthand

rails generate migration add_name_to_widgets name:string!

By adding the exclamation point after the column type a migration will now be generated as we wanted before but without the additional manual edit:

class AddNameToWidgets < ActiveRecord::Migration[8.0]
  def change
    add_column :widgets, :name, :string, null: false
  end
end

h/t Akshay Khot

Alias Method on a GraphQL Field

A lot of times I find I have to alias a method on a model when I'm exposing it on a GraphQL type. Most often I have to do this with ? methods:

class Post < ApplicationRecord
  def draft?
    ...
  end
end

class PostType < GraphQL::Schema::Object
  field :draft, Boolean, null: false

  def draft
    @object.draft?
  end
end

I want the field to be draft (without the ?), but on it's own the GraphQL Ruby gem can't resolve it to draft?, so I have to add an additional def in the Type class. This works, but it feels clunky.

Turns out you can use the method: option on the field to alias it inline, which I think streamlines things:

class PostType < GraphQL::Schema::Object
  field :draft, Boolean, null: false, method: :draft?
end

Docs

Change Text of Blank Option in a Rails Select Tag

The form helper select tag (ex - f.select) accepts a boolean option for include_blank, which informs the select tag to have an extra blank option. Setting include_blank: true will look like the below - image

But you can also pass a string to include_blank and this will change the text content of the blank option -

<%= f.select :state, options_for_select(state_options), {include_blank: 'Select a State'} %>

image

https://devdocs.io/rails~7.1/actionview/helpers/formtaghelper#method-i-select_tag

Ruby GraphQL Generators

Today I Learned the GraphQL ruby gem includes generators for types, mutations and other fun things.

rails g graphql:object
rails g graphql:input
rails g graphql:interface
rails g graphql:union
rails g graphql:enum
rails g graphql:scalar
rails g graphql:mutation

What's even neater is if the name of thing things you're generating matches an existing ActiveRecord model, it will scaffold all the database columns as fields!

If you have a model Book like:

class Book < ApplicationRecord
  attribute :id
  attribute :author_id
  attribute :title
  attribute :published_at
end

Running the object generator rails g graphql:object Book will produce:

module Types
  class BookType < Types::BaseObject
    field :id, ID
    field :author_id, ID
    field :title, String
    field :published_at, GraphQL::Types::ISO8601DateTime
  end
end

Easily Update ActiveRecord Enum Values

Today I learned about some nice helper methods for ActiveRecord Enums. You might already know the ? methods to check the value of the enum. There are also ! methods to succinctly set the value of the enum too!

class Post < ActiveRecord::Base
  enum status: {
    draft: 0,
    published: 1
  }
end

post = Post.new
post.published? # => false
post.published! # updates the status to `published`
post.published? # => true

Test for String Equality

In Rails, there is an nice and easy interface that can be used to check a string for equality.

To do this, you can use the ActiveSupport::StringInquirer object. Simply initialize it with a string value, like so:

string = ActiveSupport::StringInquirer.new("vanilla")

and now we can just call string#vanilla? to check for equality

string.vanilla?
=> true

string.chocolate?
=> false

Fun fact:
This is how you are able to do Rails.env.production?

H/T: Matt Polito

Add conditional class names

If you want to conditionally add class names in Rails, here is a clean way to do it.

<%= link_to("Some Page", some_path, 
  class: class_names("bg-black text-black", {"text-white": text_visible?})
%>

This takes advantage of the class_names method, which is just an alias for the token_list method.

h/t to Matt Polito.
I always find myself looking up his TIL and figured if I make it myself it will help me retain it. šŸ˜…

Handling exceptions with rescue_from

ActiveSupport has a handy tool for handling exceptions globally in your Rails app. You can use it to catch specific exceptions and provide a centralized way to manage errors across your application, making your code cleaner and more maintainable.

Here is the example from the docs, showcasing how to catch specific exceptions and what method to run when they are caught.
Note: You can also pass it a block as the handler instead.

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :deny_access
  rescue_from ActiveRecord::RecordInvalid, with: :show_record_errors

  rescue_from "MyApp::BaseError" do |exception|
    redirect_to root_url, alert: exception.message
  end

  private
    def deny_access
      head :forbidden
    end

    def show_record_errors(exception)
      redirect_back_or_to root_url, alert: exception.record.errors.full_messages.to_sentence
    end
end

ActiveRecord Reload only Associated Record

Today I learned about some focused reload methods for ActiveRecord Associations.

Let's say I have an Author class that can have one magnum opus.

class Author < ApplicationRecord
  has_one :magnum_opus
end

Now suppose I've instantiated an Author, and his magnum opus gets updated elsewhere. If I try to access that instance's magnum opus again (existing instance, no reloading), I'll get the old cached version:

author = Author.first
author.magnum_opus.title # => "The Dark Tower"

#elsewhere
mo = MagnumOpus.find_by(title: "The Dark Tower")
mo.update title: "The Shining"

author.magnum_opus.title # => "The Dark Tower"

Now to remedy this, we can reload the author:

author.reload.magnum_opus.title # => "The Shining"

But why reload the whole author when you can reload just the association? ActiveRecord has reload_* methods for each has_one and belongs_to association that does just that - it reloads only the associated record.

author.reload_magnum_opus.title # => "The Shining"

Docs for has_one and belongs_to.

h/t Matt Polito

Find Missing Relations Easily with .missing

It can be kind of clunky to query ActiveRecord for records where the has_manyassociation is empty.

Say I have an Author class that has many Books. One way to query for Authors without books is the following:

class Author < ApplicationRecord
  has_many :books
end

Author.left_joins(:books).where(books: {id: nil}) 

This totally works, but at least for me is difficult to reason what we're querying. However, Rails 6.1 added a .missing method that really helps clarify what the query is doing:

Author.where.missing(:books)

Much clearer - this will grab all authors that do not have any books.

Docs for further reading. Happy querying!

Set Attributes when Creating with .create_with

You can set attributes to be used when creating new records with .create_with:

Author.create_with(name: "Hashrocketeer")
Author.new.name # => "Hashrocketeer"

This can be particularly useful if you're doing a .find_or_create_by and want to set some values only when creating:

Author
  .create_with(post_count: 0)
  .find_or_create_by(name: "Hashrocketeer")

This will find an author with name "Hashrocketeer". If we find an existing Author, we won't change their post_count. If we don't find an existing Author, we'll create one with a post_count of 0.

Read more in the docs

Modify Tables with `change_table` in Rails

Ever since I used ecto in Elixir Phoenix, I've always thought about their alter table function while working on Rails projects. Turns out, I have been living under a rock and I could have been using change_table this whole time šŸ¤¦šŸ»ā€ā™‚ļø

For those new to the change_table function, it allows you to make bulk alterations to a single table with one block statement. You can adjust columns(and their types), references, indexes, indexes, etc. For a full list of transformations, see the official api documentation.

If you're using Postgres or MySQL, you can also add an optional argument bulk: true and it will tell the block to generate a bulk SQL alter statement.

class AddUserIdToPosts < ActiveRecord::Migration[7.1]
  def change
    change_table(:posts, bulk: true) do |t|
      t.remove_reference :user_post, :text, null: false
      t.references :user, null: false
    end
  end
end

API Ruby on Rails - change_table

Hooray Rails! šŸ„³

Convert objects into URL query strings

In Rails you can easily convert objects into URL query strings.

Here are a few examples:

string = "vanilla"
big_string = "extra chocolate"
integer = 5
array = ["vanilla", "chocolate"]
hash = {flavor: "vanilla", scoops: 2}

string.to_query("flavor")
=> "flavor=vanilla"

big_string.to_query("flavor")
=> "flavor=extra+chocolate"

integer.to_query("scoops")
=> "scoops=5"

array.to_query("flavors")
=> "flavors%5B%5D=vanilla&flavors%5B%5D=chocolate"

hash.to_query
=> "flavor=vanilla&scoops=2"

Note: Hashes don't require a key to be passed in, because they are inferred from the hash keys.

Change ActionCable's URL on the Client

ActionCable provides a handy escape hatch for changing the url path of the cable in your frontend. In my case, I wanted to specify a token on the cable url so that I could preserve some in place functionality while also being able to use the Turbo::StreamsChannel.

I did some source diving in turbo-rails and found that turbo calls out to createConsumer which comes from @rails/actioncable.

And that's where I found this (part of which is pasted below with a subtle change for readers) -

export function createConsumer(url = getConfig("url") || "/cable") {
  return new Consumer(url)
}

export function getConfig(name) {
  const element = document.head.querySelector(`meta[name='action-cable-${name}']`)
  if (element) {
    return element.getAttribute("content")
  }
}

You can place a meta tag for the cable url in your views; this will be picked up by Action Cable and Turbo -

<meta name="action-cable-url" content="/cable?foo=bar">

ActiveRecord allows you to view saved changes

Say you have an Activerecord object that you are making changes to.

user = User.last
user.update(first_name: "Joe", last_name: "Hashrocket")

With ActiveModel's Dirty module, you can view the changes made to the model even after it has been saved using the saved_changes method

user.saved_changes
# {"first_name"=>[nil, "Joe"], "last_name"=>[nil, "Hashrocket"]]}

Create Accessors for JSONB column data in Rails

I was recently reading about the ActiveRecord::Store API and just today, I got the chance to use it. It's a really great API for working with serialized data in Rails.

Let's consider the following model Geolocation, with a jsonb column data. The contents of the data column look something like this -

{
  "coordinates": {
    "latitude": "5.887692972524717",
    "longitude": "-162.08234016158394"
  }
}

The store_accessor creates getters and settings for coordinates.

class Geolocation < ApplicationRecord
  store_accessor :data, :coordinates
end

Geolocation.last.coordinates
# => {"latitude" => "5.887692972524717", "longitude" => "-162.08234016158394"}

In my case, I have nested data that I'd like to create accessors for - latitude and longitude. From what I could find, this API doesn't support nested data yet, so we have to bring in a helper from ActiveModel::Attributes. We declare the coordinates portion as a jsonb attribute.

class Geolocation < ApplicationRecord
  store_accessor :data, :coordinates
  store_accessor :coordinates, :latitude, :longitude
  attribute :coordinates, :jsonb
end

geo = Geolocation.new
geo.coordinates
# => nil
geo.latitude
# => nil
geo.longitude
# => nil

geo.latitude = 5.887692972524717
# => 5.887692972524717
geo.coordinates
# => {"latitude"=>5.887692972524717}
geo.longitude = -162.08234016158394
# => -162.08234016158394
geo.coordinates
# => {"latitude"=>5.887692972524717, "longitude"=>-162.08234016158394}

I_Ran_Out_Of_Words_Hopefully_You_Get_My_Point :)

S/O Vlad & LayeredDesignForRailsApplications

https://devdocs.io/rails~7.1/activerecord/store

Scoped Uniqueness Validation

Let's say you have a blog, with Authors and BlogPosts. A BlogPost has an Author and a title. If we have a requirement that the post titles need to be unique, our model would look something like this:

class Blog < ApplicationRecord
  has_one :author

  validates :title, uniqueness: true
end

Let's say the requirement changes, and the post title needs to be unique to the author (yeah it's convoluted, but let's roll with it). So Ann can author a post titled 'Cool Post', and Bob can also author 'Cool Post', but Ann can't publish another post titled 'Cool Post'.

Conveniently, uniqueness validations can be scoped to a particular attributes. So to satisfy the above, we can update the validation to:

class Blog < ApplicationRecord
  has_one :author

  validates :title, uniqueness: {scope: :author_id}
end

Tell Rails Your Foreign Key is a UUID

Let's say you have a blog with an Author model, and you want to create a blog_posts table. Each post has an author, and you want a foreign key on blog_posts to the Author's id.

class CreateAuthors < ActiveRecord::Migration[7.0]
  def change
    create_table :authors do |t|
      t.string :name
    end
  end
end

class CreateBlogPosts < ActiveRecord::Migration[7.0]
  def change
    create_table :blog_posts do |t|
      t.string :title
      t.text :content
      t.references :author, null: false, foreign_key: true

    end
  end
end

Pretty straightforward, right? But if Author#id is a UUID, you'll probably run into some issues with this migration. Rails by default assumes your table's IDs will be BigInt and if your IDs aren't then you need to specify the type in t.references:

class CreateAuthors < ActiveRecord::Migration[7.0]
  def change
    create_table :authors, id: :uuid do |t|
      t.string :name
    end
  end
end

class CreateBlogPosts < ActiveRecord::Migration[7.0]
  def change
    create_table :blog_posts, id: :uuid do |t|
      t.string :title
      t.text :content
      t.references :author, null: false, foreign_key: true, type: :uuid

    end
  end
end

Disable database management tasks in Rails

If you wish to not have Rails manage migrations, seeds, schema changes, etc, you can add the database_tasks: false to your database.yml. This may be useful if you are "inheriting", sharing a database managed with some other system, or if you are connecting to a readonly database.

production:
  database: hashrocket
  adapter: postgresql
  encoding: unicode
  pool: 5
  database_tasks: false

https://guides.rubyonrails.org/active_record_multiple_databases.html#connecting-to-databases-without-managing-schema-and-migrations

Create Environment Specific Rails Credentials

If you've worked with the rails encrypted credentials system before, you're probably familiar with the command to edit them

bin/rails credentials:edit

This will create(if it doesn't exist config/credentials.yml.enc by way of the RAILS_MASTER_KEY). If you only have this credentials file, the items in this file will be available to all environments of your application.

But you can also create/edit environment specific credentials by specifying an environment. This will similarly create 2 files - config/development.key and config/development.yml.enc.

bin/rails credentials:edit --environment development

Credentials specified in this file will only be available in the development environment

https://edgeguides.rubyonrails.org/security.html#custom-credentials

Use foreign-key like attributes with FactoryBot

An odd issue occurs when trying to use attributes similar to methods you'd expect with Rails ActiveRecord relationships in FactoryBot.

Take a relationship named provider. You would expect the ActiveRecord object to have methods for provider and provider_id.

However, some interesting behavior happens when you legitimately have a reason to have methods with those names, not during an ActiveRecord relationship.

FactoryBot.define do
  factory :widget do
    url { "URL" }
    provider { "PROVIDER_NAME" }
    provider_id { "PROVIDER_ID" }
  end
end

One will get cleared out when providing the attributes for provider and provider_id.

There is a long-running issue regarding this, but for now, this is how I could remedy the situation. Set the methods as transient attributes and merge the attributes during initialization.

FactoryBot.define do
  factory :widget do
    url { "URL" }

    transient do
      provider { "PROVIDER" }
      provider_id { "PROVIDER_ID" }
    end

    initialize_with do
      new(
        attributes.merge(
          provider: provider,
          provider_id: provider_id
        )
      )
    end
  end
end

ActiveRecord Strict Loading in Rails

When using ActiveRecord, lazy loading is the default behavior, where associated records are loaded when accessed. While convenient, this can lead to N+1 query problems, where an unanticipated number of database queries are triggered, potentially degrading performance.

Strict loading is a countermeasure to this issue. When enabled, it enforces the eager loading of associations, meaning all necessary data is loaded upfront in a single query. This approach mitigates the risk of N+1 queries and makes data fetching more efficient and predictable.

Strict loading can be set at various levels:

Globally: Set strict loading for all models by configuring it in the application:

config.active_record.strict_loading_by_default = true

Model Level: Enable strict loading for a specific model:

class Book < ApplicationRecordĀ 
Ā Ā has_many :chapters, strict_loading: true
end

Association Level: Apply it to specific associations:

has_many :comments, strict_loading: trueĀ 

Query Level: Use it on a per-query basis:

User.strict_loading.find(params[:id])

Optional routing segments in Rails

Optional routing segments in Ruby on Rails are a versatile feature in the framework's routing system, allowing for more flexible and dynamic URL structures. These segments are denoted by parentheses and can significantly streamline routing patterns.

For example, a route like:

get 'books(/:genre)', to: 'books#index' 

Here, :genre is an optional segment. This route will match /books and /books/fiction, routing them to the books#index action. The presence or absence of the genre parameter can be used within the controller to tailor the response.

Optional segments are handy for simplifying routes that cater to multiple URL patterns, reducing the need for numerous specific routes. They enhance the flexibility and readability of the code, making the application's URL structure more intuitive and user-friendly.

Touch ActiveRecord::Associations

ActiveRecord::Associations#belongs_to has an option of touch.

If passed true, the associated object will have its updated_at/updated_on attributes set to current time.

You can also pass a symbol, in that case that attribute will be updated with the current time in addition to the updated_at/updated_on

class Comment < ApplicationRecord
  # When a comment is saved/destroyed, its post will
  # update the updated_at/updated_on to the current time
  belongs_to :post, touch: true

  # OR

  # When a comment is saved/destroyed, its post will update both
  # the updated_at/updated_on as well as comment_last_updated_at
  belongs_to :post, touch: :comment_last_updated_at
end

Note: No validations are performed when touching.

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

user = User.new(password: "sekret", password_confirmation: "sekret")
user.save
#=> 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
end

invoice = Invoice.last
invoice.amount
=> 10.0

invoice.amount = 80.0

invoice.attribute_in_database("amount")
=> 10.0

invoice.amount_in_database
=> 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]
  end
end

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

Customer.select("customers.name AS customer_name")

=> SELECT customers.name 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:

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

=> SELECT customers.name, orders.total 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

MY_VAR="hi
bye"

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

config:
  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
bye".dump

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

So the resulting config would look like so:

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

And then our app can start and in console:

Rails.configuration.config.my_value

=> "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:

Login

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)

Docs

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

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

ActiveRecord::Base.connection.execute(<<~SQL)
  select
    rating
  from geocode('#{address}', 1)
SQL

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)

ActiveRecord::Base.connection.execute(<<~SQL)
  select
    rating
  from geocode(#{quoted_address}, 1)
SQL

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

https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/Quoting.html#method-i-quote

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 << ".ngrok.app"

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