Today I Learned

A Hashrocket project

Ready to join Hashrocket? Find Openings here and apply today.

199 posts about #rails

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

Easily Format Phone Numbers in Rails

Included as part of ActiveSupport, Rails has a handy helper for formatting phone numbers. Passing a number/string to the helper and some optional args and it will take care of the rest.

Here’s a few examples of how it works:

number_to_phone(8858846) # 885-8846
number_to_phone(8778858846) # 877-885-8846
number_to_phone(8778858846, area_code: true) # (877) 885-8846

To see all the options for the method, check out the Rails docs https://api.rubyonrails.org/classes/ActiveSupport/NumberHelper.html#method-i-number_to_phone

Group Related ActiveRecord Validations

Today I learned about the with_options feature of ActiveRecord. It’s used to group validations together in a block. You could use it to transform code like this:

class User < ApplicationRecord
  validates :password, length: { minimum: 10 }, if: :is_admin?
  validates :email, presence: true, if: :is_admin?
end

Into this:

class User < ApplicationRecord
  with_options if: :is_admin? do |admin|
    admin.validates :password, length: { minimum: 10 }
    admin.validates :email, presence: true
  end
end

A use case for this might be a collection of validations that should only be checked when a boolean feature flag like enabled? is true.

accepts_nested_attributes_for in Rails 5.2

Getting accepts_nested_attributes_for to work can be a bit tricky, and most of the walkthroughs I found are a bit dated, so here’s a quick update.

Our models will be User and Address:

class User < ApplicationRecord
  has_many :addresses
  accepts_nested_attributes_for :addresses
end

class Address < ApplicationRecord
end

Now, assuming we have a UsersController, with all the standard resource actions omitted, we try to add addresses to our permitted params:

class UsersController < ApplicationController
  def user_params
    params.require(:user).permit(:name, :email, :addresses)
  end
end

Logically this seems like it would work, but if we look in the rails server logs, we will see:

Unpermitted parameter: :addresses_attributes

So we add it to our permitted params:

  def user_params
    params.require(:user).permit(:name, :email, :addresses_attributes)
  end

and refresh. Lo and behold, we get the same error!

Unpermitted parameter: :addresses_attributes

The secret is that address_attributes is a hash, and we need to communicate that to Rails:

  def user_params
    params.require(:user).permit(:name, :email, addresses_attributes: {})
  end

Et voilà!

Rails `travel_to` changes the value of `usec` to 0

https://github.com/rails/rails/blob/4dcc5435e9569e084f6f90fcea6e7c37d7bd2b4d/activesupport/lib/active_support/testing/time_helpers.rb#L145

In and of itself, not a huge deal. However, when you combine it with the way that active record quotes times:

https://github.com/rails/rails/blob/4dcc5435e9569e084f6f90fcea6e7c37d7bd2b4d/activesupport/lib/active_support/testing/time_helpers.rb#L145

It can lead to some incredibly subtle bugs in your tests that use queries in their assertions. Long story short, if your query relies on a time comparison, you may not return anything. :|

Rails has an Array#exclude? method

In my vendetta against the unless expression in ruby, I came across a use case where I wanted to execute code only if an item was missing from an array.

I could easily do:

unless ['a', 'b', 'c'].include?('d')
  # do a thing
end

But I wanted to replace the unless with an if, which led me to wonder if there was an exclude? method for arrays in ruby.

Turns out, ActiveSupport extended the Enumerable class to introduce exactly what I was looking for!

if ['a', 'b', 'c'].exclude?('d')
  # do a thing
end

Hooray!

Rails Collection#exists?

I’ve been using Rails for almost a decade, and find I still don’t know even close to all it has to offer. Today, I was browsing through an open source app, and stumbled across this code:

unless feed.posts.exists?(@post.id)
  feed.posts << @post
end

I’d never seen or used exists? on a collection before, so I looked it up. And of course, there it is, right in the Rails Guides Association Basics - methods added by has many!

Digging in a bit, what arguments can exists? take? Here’s the reference.

Turns out there a lot of options!. Integer, string, array, hash, false, or no args.

Well that’s kinda neat! So what does the no args do?

Imgur

It seems to behave similar to any?, and return true if there are any members of the collection, false if it’s empty.

TLDR: Read the Rails Guides. A decade in, you’ll probably still learn something!

Access a shadowDOM node from Capybara

Using selenium_headless you can access a node from the shadowDOM using evaluate_script. The trick is knowing where in the showdowRoot.childNodes it’s going to be. If you have a web component called my-dope-component then you could do something like this:

shadow_host = find('my-dope-component')
shadow_child = evaluate_script('arguments[0].shadowRoot.childNodes[<magic-index>]', shadow_host)

The value for <magic-index> depends on the structure of the template that you’re using, as well as the library (if any) that you’re using. You could also write a find style script that iterates the children and locates the correct one based on the tagName property or similar.

For example:

FIND_NODE = <<-SCRIPT
Array.from(arguments[0].shadowRoot.childNodes).map(element => {
  if (element.tagName === arguments[1].toUpperCase()) return element;
}).filter( value => value != undefined)[0]
SCRIPT

Swap between STI classes in Rails

Rails allows you to swap between single table inheritance classes with the becomes method. This method creates a new instance of the desired class, and passes the attributes from the original record to the new instance.

So if I have two classes, User and Admin, where Admin inherits from User:

class User
  # ...
end

class Admin < User
  # ...
end

I can change between the two classes using becomes:

user = User.first # returns an instance of the User class
user.class # => User
user.id # => 1
user.name # => Joe Hashrocket

admin = user.becomes(Admin) # returns an instance of the Admin class
admin.class # => Admin
admin.id # => 1
admin.name # => Joe Hashrocket

Disable Spring in Rails with `DISABLE_SPRING`

This is fairly self explanatory. If you don’t want spring running, because maybe you forget it’s on and then you’re confused when you’re making changes to configuration and you don’t see those changes manifest, then you can disable spring with:

DISABLE_SPRING=true

But be careful about where you put this. If you are using dotenv and you place it in your .env file it will not take effect. dotenv sets its environment after spring has started.

This is something that you should put in bashrc or zshrc.

export DISABLE_SPRING=true

Remove newlines from strings in Rails

Rails has a method called squish that will remove newlines from strings. It works very nicely with heredocs, where you may want readability but don’t really want the newlines.

Without squish:

<<~SQL
  update posts
  set status = 'public'
  where status is null;
SQL
# "update posts\nset status = 'public'\nwhere status is null;\n"

With squish:

<<~SQL.squish
  update posts
  set status = 'public'
  where status is null;
SQL
# "update posts set status = 'public' where status is null;"

Couldn't decrypt `config/credentials.yml.enc`

Rails 5.2 introduced the credentials system for managing secure credentials in your application. You can edit the file with rails credentials:edit, which is pretty dope. But if you do like I did and you start your project on one machine and go to try and edit that file on a different machine after you pull down the repository from Github, it will generate a new master.key file for you. This will not work to decrypt the file, and you’ll get the error above. You need to make sure that you grab the file from wherever you started your application from. In my case it was as simple as using scp to grab the file from a different machine.

Constrain a route to a specific host

If you have two hosts that point to one rails app, like apple.myapp.com and orange.myapp.com and you’d like a specific route to only be availabe on orange then you can constrain the route based on the host like this:

constaints(host: 'orange.myapp.com') do
  get '/thing/:id', to: 'thing#show'
end

And if you had additional host for banana that should also get this route:

constaints(host: ['orange.myapp.com', 'banana.myapp.com']) do
  get '/thing/:id', to: 'thing#show'
end

But in this case only the subdomain differs so we can do this:

constaints(subdomain: ['orange', 'banana']) do
  get '/thing/:id', to: 'thing#show'
end

Rails CollectionProxy #length vs #count

When testing a controller, it’s not unreasonable to right a test something like this:

expect(post_to_endpoint).to change { some.objects.length }.from(0).to(1)

Because of the details of CollectionProxy, this will not work as expected. Instead, you need to write:

expect(post_to_endpoint).to change { some.objects.count }.from(0).to(1)

The length method will evaluate to the same value, while the count method will reflect changes to the underlying data since the last call.

Migrating to form_with

Whilst upgrading a Rails application from 4.1 to 5.2, my feature specs started throwing a strange error:

Unable to find field "X" that is disabled (Capybara::ElementNotFound)

I reverted my changes… the tests passed. Step 1 in debugging: read the error message. Capybara is saying the element I’m trying to find is disabled. If I inspect the html generated by form_for:

= form_for book do |f|
  = f.label :title, 'Title'
  = f.text_field :title
  = f.button 'Save Changes', type: :submit
<form action="/books/14" accept-charset="UTF-8" method="post">
  <label for="book_title">Title</label>
  <input type="text" name="book[title]" id="book_title">
  <button name="button" type="submit">Save Changes</button>
</form>

and compare it to the html generated by form_with:

= form_with model: book, local: true do |f|
  = f.label :title, 'Title'
  = f.text_field :title
  = f.button 'Save Changes', type: :submit
<form action="/books/14" accept-charset="UTF-8" method="post">
  <label for="book_title">Title</label>
  <input type="text" name="book[title]">
  <button name="button" type="submit">Save Changes</button>
</form>

Notice what’s missing. form_with does not automatically generates ids for form elements, and the id is necessary to link the label’s for with the input’s id, otherwise fill_in('Title', with: "Pride and Prejudice") doesn’t work in Capybara. Either add the ids manually, or in Rails 5.2 use this setting:

Rails.application.config.action_view.form_with_generates_ids = true

PostgreSQL index with NO lock on Rails

Rails allow us to create a PostgreSQL index that it would not lock the table for writing meanwhile it’s being calculated.

That’s usually handful if a table has millions of rows and this operation could take hours to release the INSERT/UPDATE/DELETE lock. Maybe the lock downtime is critical to the application.

There are caveats to this approach so please read the PostgreSQL documentation.

In order to do that you’ll need to add { algorithm: :concurrently } to the add_index on a Rails migration. Additionally PostgreSQL uses DB transactions to manage this new index creation so we need to disable the Rails migration transaction. Take this example:

class AddIndexOnEventsName < ActiveRecord::Migration[5.0]
  disable_ddl_transaction!

  def change
    add_index(:events, :name, algorithm: :concurrently)
  end
end

In this case I’m using disable_ddl_transaction! to disable the DB transaction that wraps each Rails migrations and I am using algorithm: :concurrently.

This option it will generate an index like that:

CREATE INDEX CONCURRENTLY "index_events_on_name" ON "events" ("name"));

Rails will change the `not` behavior

Yay Rails will change the behavior of the ActiveRecord#not on rails 6.1. That’s great as the current behavior sometimes leads to confusion. There will be a deprecation message on the version 6.0 so let’s watch out and change our code accordingly.

As I already wrote on this TIL this function does not act as a legit boolean algebra negation. Let’s say that we have this query:

User.where.not(active: true, admin: true).to_sql

Prior to this change this will produce this query:

SELECT users.*
  FROM users
 WHERE users.active != 't'
   AND users.admin != 't'

The problem is that negating an AND clause is naturally a NAND operator, but Rails have implemented as a NOR. Some developers might wonder why regular active users are not returning.

On Rails 6.1 this will be fixed hence the new query will be:

SELECT users.*
  FROM users
 WHERE users.active != 't'
    OR users.admin != 't'

Here’s the rollout plan:

  • Rails 5.2.3 acts as NOR
  • Rails 6.0.0 acts as NOR with a deprecation message
  • Rails 6.1.0 act as NAND

This is the PR in case you want to know more.

Use `reset_column_information` to Migrate Data

If you’re generating a Rails migration, chances are you might need to facilitate migrating data to a new column. You can use reset_column_information in a migration file to pick up your changes and immediately do something with those new columns.

Assuming we have 2 models, DraftPost and Post -

  class AddColumnDraftToPosts < ActiveRecord::Migration[5.2]
    def change
      add_column :posts, :draft, :boolean, default: true
      Post.reset_column_information
      DraftPost.all.each do |draft_post|
        Post.create(content: draftPost.content)
        draft_post.delete
    end
  end

Rails 6 new ActiveRecord method pick

Rails 6 will be released with a new convenient method on ActiveRecord pick. This method picks the first result value(s) from a ActiveRecord relation.

Person.all.pick(:name)
# SELECT people.name FROM people LIMIT 1
# => 'John'

Person.all.pick(:name, :email)
# SELECT people.name, people.email FROM people LIMIT 1
# => ['John', 'john@mail.com']

So pick(:name) is basically equivalent to limit(1).pluck(:name).first.

Watch out for rails inconsistencies as pick(:name) returns a single string value and pick(:name, :email) returns an array of values.

Rails 6 Blocked Hosts

Rails 6 has a new feature where only whitelisted hosts are allowed to be accessed. By default only localhost is permitted.

When doing mobile development, you always need to test the app in a real device that connects to a backend. In order to automatically add the dev machine host to the list, just change your development.rb to:

# config/environments/development.rb 

config.hosts << "#{`hostname -s`.strip}.local"

Rails 6 upsert_all

Rails 6 and ActiveRecord introduces upsert_all 🎉

I used to have to jump into SQL and write this myself, but no more!


As a simplified use case, say you have a User and that user has many Tags. If an API wants to send you an array of strings (["Tag Name A", "Tag Name B"]), you no longer have to do any looping, find_or_create, or defining your own upsert in SQL nonsense.

@user.tags << Tag.upsert_all([
  { name: "Tag Name A"},
  { name: "Tag Name B"}
], unique_by: :name)

If the tags exist, they will be updated. If the tags are new, they will be created. And all this happens in 1 SQL Insert.

ActiveRecord not is not boolean algebra negation

Today I learned that rails ActiveRecord not query is not to be considered a boolean algebra negative. Let’s see by an example:

User.where(name: "Jon", role: "admin")

This will produce a simple sql query:

SELECT "admins".*
FROM "admins"
WHERE "admins"."name" = $1
  AND "admins"."role" = $2
[["name", "Jon"], ["role", "admin"]]

If we get the same where clause and negate it:

User.where.not(name: "Jon", role: "admin")

Then we get:

SELECT "admins".*
FROM "admins"
WHERE "admins"."name" != $1
  AND "admins"."role" != $2
[["name", "Jon"], ["role", "admin"]]

But I was expecting a query like:

SELECT "admins".*
FROM "admins"
WHERE ("admins"."name" != $1
   OR "admins"."role" != $2)
 [["name", "Jon"], ["role", "admin"]]

So if you want to produce a query like that you’ll have to build on your own:

User.where.not(name: "Jon").or(User.where.not(role: "admin"))

Just beaware of that when using not with multiple where clauses.