ruby on rails

Unique ID for DOM in Rails

Rails has many great view helpers, one of which is dom_id:
dom_id(Post.find(12))       # => "post_23"
dom_id(Post.new)            # => "new_post"

You can also provide a prefix:
dom_id(Post.find(23), :edit) # => "edit_post_23"
dom_id(Post.new, :custom)    # => "custom_post"
Const

Fix input on Heroku's rails console

If you end up pasting lots of lines into a rails console on Heroku (or else where), you might get issues with the lines "bleeding" into each other.

Next time this happens, try disabling multiline:

$ heroku run 'bundle exec rails console -- --nomultiline'
Const

PostgreSQL UPDATE Join

This query shows how to use the PostgreSQL UPDATE join syntax to update data in a table based on values in another table. for example;

updating subscription_missions.giftbox_type with rewards.gift_box_type through mission_rewards table (tested on 300k records, crazy level fast)

  UPDATE
        subscription_missions sm
  SET
        giftbox_type = r.gift_box_type
  FROM
        mission_rewards mr,
        rewards r
  WHERE
        sm.giftbox_type IS NULL
        AND sm.id = mr.subscription_mission_id
        AND mr.reward_id = r.id;



Ali

List your model names

$ RUBYOPT='-W0' rails runner 'Rails.application.eager_load!; puts ApplicationRecord.descendants.collect { |type| type.name }'


NOTE: RUBYOPT=-W0 avoids verbosity when running rails command which suppresses warnnings.

Ali

Postgres Functions & Non-sargable Queries

Using postgres functions inside a where clause can make a query non-sargable.

Database Structure

tracks has_many artists

Non-Sargable Query

Using a LOWER function prevents DBMS engine from using indexes.

Track.joins(:artists).where('LOWER(tracks.display_name) LIKE ?', "eric clapton%").explain

Gather  (cost=1000.85..56714.32 width=4061)
   Workers Planned: 2
   ->  Nested Loop  (cost=0.85..55713.62 rows=3 width=4061)
         ->  Nested Loop  (cost=0.42..55708.37 rows=3 width=4069)
               ->  Parallel Seq Scan on tracks  (cost=0.00..55365.17 rows=41 width=4061)
                     Filter: (lower((display_name)::text) ~~* 'eric clapton%'::text)
               ->  Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations  (cost=0.42..8.36 rows=1 width=16)
                     Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
         ->  Index Only Scan using idx_35952_primary on artists  (cost=0.43..1.75 rows=1 width=8)
               Index Cond: (id = artist_relations.artist_id)


Sargable Query

Removing LOWER function allows DBMS engine to use indexes, resulting in faster execution.

Track.joins(:artists).where('tracks.display_name ILIKE ?', "eric clapton%").explain

Nested Loop  (cost=1497.60..2695.43 width=4061)
   ->  Nested Loop  (cost=1497.17..2684.94 rows=6 width=4069)
         ->  Bitmap Heap Scan on tracks  (cost=1496.75..1873.05 rows=97 width=4061)
               Recheck Cond: ((display_name)::text ~~* 'eric clapton%'::text)
               ->  Bitmap Index Scan on index_tracks_on_display_name  (cost=0.00..1496.73 rows=97 width=0)
                     Index Cond: ((display_name)::text ~~* 'eric clapton%'::text)
         ->  Index Scan using index_artist_relations_on_artist_item_type_and_artist_item_id on artist_relations  (cost=0.42..8.36 rows=1 width=16)
               Index Cond: (((artist_item_type)::text = 'Track'::text) AND (artist_item_id = tracks.id))
   ->  Index Only Scan using idx_35952_primary on artists  (cost=0.43..1.75 rows=1 width=8)
         Index Cond: (id = artist_relations.artist_id)


Abhi

How to find god objects in your project (Rails)

Go to your project root and cd into app/models (using CLI obviously) then run wc -lw * | sort -u

$ wc -lw * | sort -u
103     186 deal_broker.rb
116     313 physical_sim_report.rb
126     229 coupon_code.rb
126     260 subscription_mission.rb
129     284 payment.rb
145     290 deal_value.rb
147     255 single_use_coupon.rb
166     344 sim_card.rb
166     406 coupon_campaign.rb
225     451 order_summary.rb
260     534 deal.rb
308     783 package.rb
443    1031 user.rb
728    1744 subscription.rb
7262   15841 total


So now you can see Rocky is all about User ,Subscription and Package.

Note: First column number of lines and the second column number of words.

Ali

Tag rails logs with useful information

Just learned about tagged logging in rails. Did you know you can tag a log by using

logger.tagged('Your Tag') { logger.info('Message') }


You can also do it globally with

# config/application.rb
config.log_tags = [:method_on_request_object, lambda { |request| request.method.modifier_method }]


Add client Device OS and OS Version information to logs (requires clients to send X-OS and X-OS-Version headers)

# config/application.rb
config.log_tags = [ :request_id, lambda { |request| "#{request.headers['HTTP_X_OS']} #{request.headers['HTTP_X_OS_VERSION']}" } ]


Joe

Limited has_many associations

# app/models/user.rb
has_one :four_car_garage
has_many :cars
validates :cars, length: { maximum: 4 }


Joe

Create idempotent migrations

When you want to write destructive migrations please use if_exists: true when you’re trying to remove a table and check for a table existence when you want to add or remove a column ActiveRecord::Base.connection.table_exists?

Case 1:

def up
  drop_table :kittens, if_exists: true
end


Case 2:

def up
  return unless ActiveRecord::Base.connection.table_exists?(:kittens)
  add_column :kittens, :kind, :integer
end


Ali

If you want to override previously set order

If you want to override previously set order (even through default_scope), use reorder() instead.

E.g.

User.order('id ASC').reorder('name DESC')


would ignore ordering by id completely

Ali

Attaching file to ActiveStorage without HTTP

If you want to attach a file you generated on disk or downloaded from a user-submitted URL, you can do it like this.

@model.image.attach(
    io: File.open('/path/to/file'),
  filename: 'file.pdf',
  # content type is optional
  content_type: 'application/pdf'
)


Tino

Add new types to Rails 5 attributes API

The Rails 5 attributes API allows us to build form objects very easily.

I had previously been using an after_initialize callback to downcase emails, with the following code I can define an :email date type and values are automatically cast when set.

This has the advantage of casting subsequent calls to set the email value.

class EmailType < ActiveModel::Type::String
  def cast(value)
    return super unless value.is_a?(String)
    super(value.downcase)
  end
end

ActiveModel::Type.register(:email, EmailType)


class Foo
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :email, :email
end


> Foo.new(email: 'EMAIL@MAIL.com').email
=> 'email@mail.com'


Joe

Scope or scope in Rails

# app/models/user.rb
...
enum role: { user: 0, author: 1, admin: 2, robot: 3 }

scope :human, -> { user.or(author) }
scope :terminator, -> { admin.or(robot) }


Produces

User.human.to_sql
#=> "SELECT \"users\".* FROM \"users\" WHERE (\"users\".\"role\" = 0 OR \"users\".\"role\" = 1)"


Joe

HTML forms that submit to another action

I've known for a long time that you can submit values with submit buttons so you can track which button was used to submit the form e.g.

language-html

However, today I needed to actually submit the form to a completely differnt action. Turns out you can do this with formaction attributes

language-html

Joe

Show TODO and other notes in your Rails app

Did you know you can show all TODOs/OPTIMIZE/FIXME in your rails app with rails notes?

$ rails notes
app/helpers/users_helper.rb:
  * [10] [TODO] Use ActiveSupport extension methods for Date/Time

app/services/slack.rb:
  * [20] [OPTIMIZE] Replace library with core Net::HTTP



You can even focus with notes:todo etc.:

$ rails notes:todo
app/helpers/users_helper.rb:
  * [10] [TODO] Use ActiveSupport extension methods for Date/Time


Const

Create a Hash from an Enumerable (Rails 6.0)

New method allowing you to create a Hash from an Enumerable:

%w(driver owner drivy).index_with(nil)
# => { 'driver' => nil, 'owner' => nil, 'drivy' => nil }

%w(driver owner drivy).index_with { |role| delta_amount_for(role) }
# => { 'driver' => '...', 'owner' => '...', 'drivy' => '...' }


Ali

Use minItems/maxItems in collection JSON schemas

To make your collection JSON schemas more reilable use minItems and maxItems so that you can trust your API.

{
  "type": "array",
  "minItems": 1,
  "items": {
    "$ref": "subscription_notification.json"
  }
}


instead of

{
  "type": "array",
  "items": {
    "$ref": "subscription_notification.json"
  }
}


If your API returns [] empty array that last one would pass if you make an assertion in your specs.

expect(response).to match_response_schema(:subscription_notifications)


But not the first one

The property '#/' did not contain a minimum number of items 1 in schema


https://json-schema.org/understanding-json-schema/reference/array.html

Ali