Logo

TODAY I LEARNED

20 posts about #rails

Using Gem::Dependency class manually to ensure version matching

I needed to add a mechanism to ensure that some actions of a controller were available only for specific versions, and I thought it was super similar of what Gemfile does, so I started to look how to use Gem::Dependency to solve this, and turns out it was super easy:

class Controller
  def feeds_one
    check_for_version_support('>= 1.0', '< 3')

    render json: SomeData.all
  end

  def feeds_two
    check_for_version_support('= 1.0')

    render json: SomeData.all
  end

  def feeds_three
    check_for_version_support('>= 2.1')

    render json: SomeData.all
  end

  private

  def check_for_version_support(*specification)
    checker = Gem::Dependency.new(action_name, specification)

    return if checker.match?(action_name, params[:version])

    raise VersionNotSupported, "Version not upported"
  end
end

Learned by Edwin Cruz on Apr 7, 2021

973983 adforcodereview v3 0211021 c02 021121

If you're having issues installing gems...

...try running rm -rf vendor/cache inside your app's root directory. Looks like sometimes cache can cause compilation issues while building gem extensions so getting rid of it fixes the issue. I can't guarantee this works 100% of the time, but it's worth giving a try if it can help you avoid a headache.

Learned by kevin-perez on Mar 29, 2021

973983 adforcodereview v3 0211021 c02 021121

Using Upsert with Rails

Some DBMS support natively something that behaves like update or insert, Rails recently added the method Upsert that can take advantage of this method.

It is useful to update information that do not need to run validations, meaning, in a super performant way, here's an example:

# We usually do this:
activity = current_user.activity
if activity 
  activity.update(last_seen_at: Time.current)
else
  current_user.activity.create(last_seen_at: Time.current)
end

But if you see, it does not need to run any validation, it just needs to update last_seen_at if exists or create a new one, it performs two queries: one to instanciate the activity object and a second one that performs the real update/insert statement.

That can be replaced with the following code and it will perform just a single query and it will take care to either create the record or update an existing one

Activity.upsert({ last_seen_at: Time.current, user_id: current_user.id}, unique_by: :user_id)

To make this really work, considering you use Postgresql, you have to add a unique index on user_id and modify default value on created_at and updated_at in a migration like this:

query = <<-SQL
  ALTER TABLE #{Activity.table_name}
  ALTER COLUMN created_at SET DEFAULT CURRENT_TIMESTAMP,
  ALTER COLUMN updated_at SET DEFAULT CURRENT_TIMESTAMP
SQL
execute(query)

Learned by Edwin Cruz on Mar 19, 2021

973983 adforcodereview v3 0211021 c02 021121

Better usage of Rails logger

Logging usefull data is a hard task, but there's one specific method that helps to improve the experience of logging actual useful information: tagged. It adds extra tags to the log message making it easy to debug:

Rails.logger.tagged('Super App') do
  Rails.logger.info('Log Message')
end

This will result in somethin like this:

[Super App] Log Message

If you can see, it prepends usefull information, you could add personalized data to trace logs, for example:

class ApplicationController << ActionController::Base
  around_action :add_logger_tags

  def add_logger_tags
    Rails.logger.tagged(logging_tags) do
      yield
    end
  end

  def logging_tags
    [
      "Request Id: #{request.id}",
      "Session Id: #{session.id}",
      current_user && "User id: #{current_user.id}"
    ]
end

And you will have super nice logs to read

Learned by Edwin Cruz on Mar 18, 2021

973983 adforcodereview v3 0211021 c02 021121

Printing queries executed by ActiveRercord

Starting of Rails 6, there's a handy way to specify the level of verbocity to ActiveRecord queries:

ActiveRecord::Base.verbose_query_logs = true

Full documentation here

No more adding a custom logger to ActiveRecord::Base.logger

Learned by Edwin Cruz on Mar 11, 2021

973983 adforcodereview v3 0211021 c02 021121

Is it possible to render a "sidecar" partial for a ViewComponent?

Let's say I have

class MyComponent < ViewComponent::Base
end

and 2 view files in

app/components/my_component/my_component.html.erb
app/components/my_component/_some_partial.html.erb

In my_component.html.erb, I want to be able to do:

render "some_partial"

But without a special configuration, that looks under the views directory for said partial. I don't want to extract that partial to its own component, nor do I want it floating by itself in the view directory.

The first step is to tell Rails it can look for templates in the view components directory

class ApplicationController < ActionController::Base
  append_view_path "#{Rails.root}/app/components"

Keep in mind that view contexts are based on the currently executing controller, so <%= render :some_partial %> in a PostsController (even within a ViewComponent class) will look for a partial in a subdirectory /posts or /application.

To make sure Rails finds your partial, use an absolute path when you render it:

<%= render "/my_component/some_partial" %>

Hat tip to Roli in the StimulusReflex discord

Learned by obie-fernandez on Mar 10, 2021

973983 adforcodereview v3 0211021 c02 021121

Updating ActiveRecord models without loading an instance using Postgresql

There're sometimes that we need to update an ActiveRecord Model but it is not necesary to load an instance, the normal flow would be the following:

profile = UserProfile.find_by(user_id: id)
profile.update(last_seen_at: Time.now)

The problem with this is that we load a useless instance of UserProfile, it is not needed, in a high traffic sites, an extra select query can count a lot, but luckly, Rails has addressed this with upsert command:

UserProfile.upsert({ last_seen_at: Time.now, user_id: id }, unique_by: :user_id)

This will use native Postgresql upsert command to update a record if it exists or insert a new one and no select will be performed, everything in a single query instead of two.

To make it really work, you need to modify your created_at and updated_at columns to have default current_timestamp

Learned by Edwin Cruz on Mar 10, 2021

973983 adforcodereview v3 0211021 c02 021121

Using Rails to migrate columns from JSON to JSONB in Postgresql

Postgres offers data type json to store any structure easily, but one dissadvantage is that filtering by properties stored in the json column are super slow, one simple fix before refactoring the whole implementation is to migrate the column to be jsonb, since it is stored in binary form, it supports indexes, a easy and safe way to do it is as follows:

class ModifyJSONDataDataType < ActiveRecord::Migration[6.0]
  def up
    add_column :table_name, :data_jsonb, :jsonb, default: '{}'

    # Copy data from old column to the new one
    TableName.update_all('data_jsonb = data::jsonb')

    # Rename columns instead of modify their type, it's way faster
    rename_column :table_name, :data, :data_json
    rename_column :table_name, :data_jsonb, :data
  end

  def down
    safety_assured do
      rename_column :table_name, :data, :data_jsonb
      rename_column :table_name, :data_json, :data
    end
  end
end

Then, using another migration(due the ability to disable transactions), add an index to it

class AddIndexToDataInTableName < ActiveRecord::Migration[6.0]
  disable_ddl_transaction!

  def change
    add_index :table_name, :data, name: "data_index", using: :gin, algorithm: :concurrently

    # You can even add indexes to virtual properties:
    # add_index :table_name, "((data->'country')::text)", :name => "data_country_index", using: 'gin', algorithm: :concurrently
  end
end

Learned by Edwin Cruz on Mar 8, 2021

973983 adforcodereview v3 0211021 c02 021121

Invoking ActiveRecord::Migration functions in command line

If you need for some weird reason, to run migration commands in your rails console, just do the following:

ActiveRecord::Migration.add_index :table, :col, name: 'col_index'

Learned by Edwin Cruz on Mar 2, 2021

973983 adforcodereview v3 0211021 c02 021121

Remove Rails model's fields without crashing on production

If you are experiencing problems after removing an attribute from a Rails model it is probably because ActiveRecord has cached that field, it is something normal is intended to help to improve performance on production environments but when removing a field may cause errors because Rails is training to read/writer a column that doesn't exist anymore, so for Rails 5 and newest we have the ignore_columns setting to fix this issue, simply add it to your model something like:

class MyModel < ApplicationRecord
  self.ignore_columns = %w[field_to_ignore]
end

And you are good to go! if you want to be warned or remind to your team you would like to consider using the strong migrations gem https://github.com/ankane/strong_migrations

Learned by ozmar-ugarte on Mar 1, 2021

973983 adforcodereview v3 0211021 c02 021121

Native Pub/Sub in Rails with ActiveSupport::Notifications

If you want to use pub/sub design pattern inside your rails app, there's no need to add extra dependencies, you can use ActiveSupport::Notifications to do the job

Example:

class Order
  # methods

  def complete
    update(complted_at: Time.zone.now, etc: 'some')
    ActiveSupport::Notifications.instrument("order_completed", { number: order.number })
  end
end

module Subscribers
  class SendConfirmationEmail
    def self.subscribe!
      ActiveSupport::Notifications.subscribe("order_completed") do |_name, _start, _finish, _id, params|
        order = Order.find_by number: params[:number]
        OrderMailer.confirm_order(order).deliver_later
      end
    end
  end
end

module Subscribers
  class UpdateCustomerCRM
    def self.subscribe!
      ActiveSupport::Notifications.subscribe("order_completed") do |_name, _start, _finish, _id, params|
        order = Order.find_by number: params[:number]
        CrmIntegration.update_customer(order.customer.email, order.total_amount)
      end
    end
  end
end

# etc

Learned by Edwin Cruz on Mar 1, 2021

973983 adforcodereview v3 0211021 c02 021121

Rails find_by using relationships

If you want to find records via a relationship you can do it easily:

product = Product.joins(:variants).find_by(variants: { sku: 'SKU' }

Instead of:

product = Variant.find_by(sku: 'SKU')&.product

Learned by Edwin Cruz on Mar 1, 2021

973983 adforcodereview v3 0211021 c02 021121

Use discard_on to discard the job with no attempts to retry

Discard the job with no attempts to retry, if the exception is raised. This is useful when the subject of the job, like an Active Record, is no longer available, and the job is thus no longer relevant.

You can also pass a block that'll be invoked. This block is yielded with the job instance as the first and the error instance as the second parameter.

Example: 1

class SearchIndexingJob < ActiveJob::Base
  discard_on ActiveJob::DeserializationError
  discard_on(CustomAppException) do |job, error|
    ExceptionNotifier.caught(error)
  end

  def perform(record)
    # Will raise ActiveJob::DeserializationError if the record can't be deserialized
    # Might raise CustomAppException for something domain specific
  end
end

Example: 2:

class UserNotFoundJob < ActiveJob::Base
  discard_on ActiveRecord::RecordNotFound



  def perform(user_id)
    @user = User.find(user_id)
    @user.do_some_thing
  end
end

Source

Learned by Victor Velazquez on Feb 20, 2021

973983 adforcodereview v3 0211021 c02 021121

When to eager load relationships in Rails and when it is not that good

Rails provides a way to eager load relationships when fetching objects, the main idea is to avoid queries N+1, but, when isn't a good idea when to do it?

Good

When rendering unique results that can not be cached, for example: table reports

Why? Most of the times you need to display related information

orders = Order.includes(:user, :line_items).completed

Try to avoid

When you use fragment cache

Why? Eager load is executed before the rendering, regardless the final result is already cached or not. If using eager loading, it will always be executed, but when allowing queries n+1 that query will be executed once to fill the cache, and that's it

products = Product.includes(:categories, variants: [:price]).search(keywords)

Use product.id & updated_at to fill a fragment cache and fetch the data from database only when needed, no extra info needed such as variants, categories, prices, etc

Learned by Edwin Cruz on Feb 19, 2021

973983 adforcodereview v3 0211021 c02 021121

Beware of calling #count on Active Record relations!

Given code like this:

records = Record.includes(:related).all # Eager-loads to prevent N+1 queries...
records.each do |record|
  puts record.related.count # => ... but this produces N+1 queries anyway!
end

If you run this, you'll notice you get an N+1 queries problem, even though we're using #includes. This happens because of record.related.count. Remember, records.related here is not an Array but an instance of CollectionProxy and its #count method always reaches out to the database. Use #length or #size instead to solve this issue.

records = Record.includes(:related).all
records.each do |record|
  puts record.related.length # Problem solved!
end

Learned by kevin-perez on Feb 18, 2021

973983 adforcodereview v3 0211021 c02 021121

Migration operation that should run only in one direction

Disclaimer: I know it's not recommended to do data mutation in schema migrations. But if you want to do it anyway, here's how you do a one-way operation, using the reversible method.

class AddAds < ActiveRecord::Migration[5.0]
  def change
    create_table :ads do |t|
      t.string :image_url, null: false
      t.string :link_url, null: false
      t.integer :clicks, null: false, default: 0
      t.timestamps
    end

    reversible do |change|
      change.up do
        Ad.create(image_url: "https://www.dropbox.com/s/9kevwegmvj53whd/973983_AdforCodeReview_v3_0211021_C02_021121.png?dl=1", link_url: "http://pages.magmalabs.io/on-demand-github-code-reviews-for-your-pull-requests")
      end
    end
  end
end

Learned by obie-fernandez on Feb 16, 2021

973983 adforcodereview v3 0211021 c02 021121

Lotties with Rails 6 and Webpacker

1) Install Lottie Player:

npm install --save @lottiefiles/lottie-player

2) Require it at app/javascript/packs/application.js

require('@lottiefiles/lottie-player');

3) Set webpacker to load jsons at config/webpacker.yml

static_asset_extensions:
  - .json

4) Put your lotties jsons wherever you want e.g. app/javascript/images/lotties

5) Render lottie-player tag in your htm

%lottie-player{ autoplay: true,
                loop: true,
                src: asset_pack_path('media/images/lotties/mylottie.json') }

6) Profit

Learned by carlos-muniz on Feb 15, 2021

973983 adforcodereview v3 0211021 c02 021121

Period of Time with Ruby on Rails and Integers | ActiveSupport::Duration

From the rails console try next:

irb(main):001:0> period_of_time = 10.minutes
=> 10 minutes
irb(main):002:0> period_of_time.class
=> ActiveSupport::Duration
irb(main):003:0> period_of_time = 10.hours
=> 10 hours
irb(main):004:0> period_of_time.class
=> ActiveSupport::Duration
irb(main):005:0> period_of_time.to_i
=> 36000

Useful when you work with devise authentication gem, e.g to expire the session in a certain period of time.

class User < ApplicationRecord
  devise :database_authenticatable, :timeoutable,
    timeout_in: ENV['EXPIRATION_TIME_IN_MINUTES'].to_i.minutes || 10.minutes
end

And so on...

User.where(created_at: 20.days.ago..10.minutes.ago)

Learned by emmanuel-oseguera on Feb 15, 2021

973983 adforcodereview v3 0211021 c02 021121

How to add timeouts to slow queries

Sometimes some of your queries are taking too long to execute; you can specify optimizer hints and define timeouts for those queries.

Employee.optimizer_hints("MAX_EXECUTION_TIME(5000)").all

It will raise a StatementTimeout exception if the query takes longer than usual to execute

Example (for PostgreSQL with pg_hint_plan):

Employee.optimizer_hints("SeqScan(employees)", "Parallel(employees 8)")

Example (for MySQL):

Employee.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(employees)")

There are many causes for sudden slow queries in many databases, such as missing index, wrong catching, and performance.

But this is a topic for another day!

Learned by Victor Velazquez on Feb 11, 2021

973983 adforcodereview v3 0211021 c02 021121

ActiveModel: Rails 6.1.0 - *_previously_changed? accepts :from and :to keyword arguments

*_previously_changed? accepts :from and :to keyword arguments like *_changed? since Rails 6.1.0

task.update!(status: :archived)
task.status_previously_changed?(from: "active", to: "archived")
# => true

Learned by Victor Velazquez on Feb 10, 2021

973983 adforcodereview v3 0211021 c02 021121