Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
New Rails 7 features, Before and After

Rails 7 new features. Before and After

Add ComparisonValidator

Before Rails 7

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true

  validate :end_date_is_after_start_date

  private

  def end_date_is_after_start_date
    if end_date < start_date
      errors.add(:end_date, 'cannot be before the start date')
    end
  end
end

After Rails 7

class Event < ApplicationRecord
  validates :start_date, presence: true
  validates :end_date, presence: true
  
  validates_comparison_of :end_date, greater_than: :start_date
end

more details: https://blog.kiprosh.com/rails7-activerecord-comparison-validator/

PostgreSQL generated columns

Before

One of the options was using callbacks:

# == Schema Information
#
# Table name: prders
#
#  id                     :bigint
#  price                  :decimal, precision: 8, scale: 2
#  tax                    :decimal, precision: 8, scale: 2
#  total                  :decimal, precision: 8, scale: 2
#  created_at             :datetime
#  updated_at             :datetime

class Order < ApplicationRecord
  before_save :calculate_total
  
private

  def calculate_total
    self[:total] = price + tax
  end
end

Result:

order = Order.create!(price: 12, tax: 1)
order.total => 13

After

You just need to use virtual and all will be done automatically by postgres

create_table :orders, force: true do |t|
  t.decimal :price, precision: 8, scale: 2
  t.decimal :tax, precision: 8, scale: 2
  t.virtual :total, type: :decimal, as: 'price + tax', stored: true
end

Result: You need to reload data to get the calculated value form the DB

order = Order.create!(price: 12, tax: 1)
order.total => nil

order.reload
order.total => 13

More details: https://tejasbubane.github.io/posts/2021-12-18-rails-7-postgres-generated-columns/

PostgreSQL custom enum types

Before

  def up
    execute <<-SQL
      CREATE TYPE mood_status AS ENUM ('happy', 'sad');
    SQL
    add_column :cats, :current_mood, :mood_status
  end

And we had to set config.active_record.schema_format = :sql to use structure.sql instead of schema.rb

After

In migrations, use create_enum to add a new enum type, and t.enum to add a column.

def up
  create_enum :mood, ["happy", "sad"]

  change_table :cats do |t|
    t.enum :current_mood, enum_type: "mood", default: "happy", null: false
  end
end

Enums will be presented correctly in schema.rb, means no need to switch to structure.sql anymore :D

Tutorial for Rails < 7: https://medium.com/@diegocasmo/using-postgres-enum-type-in-rails-799db99117ff

Add tracking of belongs_to association

class Event
  belongs_to :organizer
end

class Organizer
  has_many :events
end

association_changed? method

The association_changed? method tells if a different associated object has been assigned and the foreign key will be updated in the next save.

Before

Tracking the target of a belongs_to association was able by checking its foreign key.

class Event
  belongs_to :organizer
  before_save :track_change
    
  private
    
  def track_change
    if organizer_id_changed?
      #track something
    end
  end
end

After

It's doable by using association_changed? method

class Event
  belongs_to :organizer
  before_save :track_change
    
  private
    
  def track_change
    if organizer_changed?
      #track something
    end
  end
end

association_previously_changed? method

The association_previously_changed? method tells if the previous save updated the association to reference a different associated object.

> event.organizer
=> #<Organizer id: 1, name: "Organization 1">

> event.organizer = Organizer.second
=> #<Organizer id: 2, name: "Organization 2">

> event.organizer_changed?
=> true

> event.organizer_previously_changed?
=> false

> event.save!
=> true

> event.organizer_changed?
=> false

> event.organizer_previously_changed?
=> true

More details: https://blog.kiprosh.com/rails-7-supports-tracking-of-belongs_to-association/

Add invert_where method

Allows you to invert an entire where clause instead of manually applying conditions.

class User
  scope :active, -> { where(accepted: true, locked: false) }
end

Before

active_users = User.active
inactive_users = User.where.not(id: User.active.ids)

After

active_users = User.active
inactive_users = User.active.invert_where

Add associated method

It returns the list of all records that have an association

Before

User.where.not(contact_id: nil)

After

User.where.associated(:contact)

more examples: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-associated

Add missing method

It returns the list of all records that don't have an association. opposite of associated

Before

User.where(contact_id: nil)

After

User.where.missing(:contact)

more examples: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing

Active Record Encryption

without encryption

> Post.create(title: 'Rails 7')

INSERT INTO "posts" ("title") VALUES (?)  [["title", "Rails 7"]]

Encryption Before

We had to write a lot of extra codes, and use a gem (e.g. https://github.com/attr-encrypted/attr_encrypted) or play with ActiveSupport::MessageEncryptor (tutorial here: https://pawelurbanek.com/rails-secure-encrypt-decrypt)

After

class Post < ApplicationRecord
  encrypts :title
end
> Post.create(title: 'Rails 7')

INSERT INTO `posts` (`title`) VALUES ('{\"p\":\"n7J0/ol+a7DRMeaE\",\"h\":{\"iv\":\"DXZMDWUKfp3bg/Yu\",\"at\":\"X1/YjMHbHD4talgF9dt61A==\"}}')

Querying non-deterministically encrypted data is impossible:

> Post.find_by title: 'Rails 7'
# => nil

If you want to directly query an encrypted column attribute, you'd need to use the deterministic approach. For this, simply use the deterministic: true option during declaration.

class Post < ApplicationRecord
  encrypts :title, deterministic: true
end
> Post.create(title: 'Rails 7')
INSERT INTO `posts` (`title`) VALUES ('{\"p\":\"n7J0/ol+a7DRMeaE\",\"h\":{\"iv\":\"DXZMDWUKfp3bg/Yu\",\"at\":\"X1/YjMHbHD4talgF9dt61A==\"}}')

> Post.find_by title: 'Rails 7'
# => <Post:0x00 id: 1, title: "Rails 7"...>

Disable partial_inserts as default

# == Schema Information
#
# Table name: posts
#
#  id                     :bigint
#  title                  :string
#  description            :text
#  created_at             :datetime
#  updated_at             :datetime

class Post < ApplicationRecord
end

Before

It's enabled as default

Rails.configuration.active_record.partial_inserts => true

The INSERT command does not include description as we are just passing title to the Post.new command

> Post.new(title: 'Rails 7').save

Post Create (1.7ms)  INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES (?, ?, ?)  [["title", "Rails 7"], ["created_at", "2021-12-25 20:31:01.420712"], ["updated_at", "2021-12-25 20:31:01.420712"]]

After

It's disabled as default

Rails.configuration.active_record.partial_inserts => false

The INSERT command includes description too, even when we don't pass description to the Post.new command

> Post.new(title: 'Rails 7').save

Post Create (1.7ms)  INSERT INTO "posts" ("title", "description", "created_at", "updated_at") VALUES (?, ?, ?)  [["title", "Rails 7"], ["description", ""], ["created_at", "2021-12-25 20:31:01.420712"], ["updated_at", "2021-12-25 20:31:01.420712"]]

More details: https://blog.kiprosh.com/rails-7-introduces-partial-inserts-config-for-activerecord/

Active storage pre-defined variants

Before

class Puppy < ApplicationRecord
  has_one_attached :photo
end

<%= image_tag puppy.photo.variant(resize_to_fill: [250, 250]) %>

After

class Puppy < ApplicationRecord
  has_one_attached :photo do |attachable|
    attachable.variant :thumb, resize: "100x100"
    attachable.variant :medium, resize: "300x300", monochrome: true
  end
end

<%= image_tag puppy.photo.variant(:thumb) %>

@harshwardhan-punchh
Copy link

harshwardhan-punchh commented Dec 27, 2021

Great work!

@yfkhar
Copy link

yfkhar commented Dec 27, 2021

Thanks you πŸ‘

@SebSwan
Copy link

SebSwan commented Dec 27, 2021

Thanks!

@aliii95
Copy link

aliii95 commented Dec 28, 2021

Thanks for compiling snippets πŸ”₯

@heo001997
Copy link

heo001997 commented Dec 28, 2021

wonderful snippets, you rock πŸ‘

@emanuelhfarias
Copy link

emanuelhfarias commented Dec 28, 2021

Thank you πŸ‘

@viralmistry43
Copy link

viralmistry43 commented Dec 29, 2021

Nice work, Thank you.

@kentarofujiy
Copy link

kentarofujiy commented Dec 29, 2021

cheers mate!

@ydaniju
Copy link

ydaniju commented Dec 29, 2021

This is great! Thank you

@kreopelle
Copy link

kreopelle commented Dec 29, 2021

Love this! Thanks! πŸš€

@Naokimi
Copy link

Naokimi commented Dec 30, 2021

πŸ‘ thank you so much for this clear post!

@PaulineTW
Copy link

PaulineTW commented Dec 31, 2021

Thank you so much for sharing

@rodrigotoledo
Copy link

rodrigotoledo commented Jan 1, 2022

Hey, I'm having problems with the action cable in my Heroku deployment. The consumer isn't founded. Can you help me?

@rodrigotoledo
Copy link

rodrigotoledo commented Jan 1, 2022

My English isn't so good so I will record my screen and just type, ok?

@rodrigotoledo
Copy link

rodrigotoledo commented Jan 1, 2022

Video with description with the error

https://www.loom.com/share/98904e2327954b55b864f3fdb6a32161

@rodrigotoledo
Copy link

rodrigotoledo commented Jan 1, 2022

Solved... I add config.assets.compile = true on production environment

@mervesahan142
Copy link

mervesahan142 commented Jan 3, 2022

This is great!! Thank you πŸ‘

@dmilon
Copy link

dmilon commented Jan 3, 2022

Thank you for compiling this nice cheatsheet πŸ™ πŸ’ͺ

@ps-ruby
Copy link

ps-ruby commented Jan 3, 2022

Thanks for the nice overview of changes in Rails 7 πŸ‘Œ

@jotolo
Copy link

jotolo commented Jan 3, 2022

πŸ‘

@alexventuraio
Copy link

alexventuraio commented Jan 3, 2022

Really useful, thank you!

@sebaherrera07
Copy link

sebaherrera07 commented Jan 3, 2022

Nice! Thanks!

@luisAzcuaga
Copy link

luisAzcuaga commented Jan 4, 2022

Amazing! πŸ”₯

@odinsride
Copy link

odinsride commented Jan 18, 2022

This is great, thank you!

@igoracportela
Copy link

igoracportela commented Jan 18, 2022

Amazing! Thanks!

@eqaderi
Copy link

eqaderi commented Jan 19, 2022

Amazing πŸ”₯

@baonguyen1320
Copy link

baonguyen1320 commented Jan 20, 2022

Useful! Thanks!

@paxer
Copy link

paxer commented Jan 21, 2022

Nice, thanks! πŸ‘

I think though that this example

t.virtual :total, type: :decimal, as: 'price + tax', stored: true

is a really bad example how to use this feature and it probably never should be done with that kind of data, or I can only imagine the debugging hell it may introduce by splitting business logic between a model and the database

@dvodvo
Copy link

dvodvo commented Mar 30, 2022

Very helpful.

Last item did raise a question (more than a 'really?'). So does ActiveStorage save a copy of each variant.
Until now I found AS to be less convenient that carrierwave gem...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment