# Rails 7 new features. Before and After - [Add ComparisonValidator](#add-comparisonvalidator) - [PostgreSQL generated columns](#postgresql-generated-columns) - [PostgreSQL custom enum types](#postgresql-custom-enum-types) - [Add tracking of belongs_to association](#add-tracking-of-belongs_to-association) - [association_previously_changed? method](#association_previously_changed-method) - [Add invert_where method](#add-invert_where-method) - [Add associated method](#add-associated-method) - [Add missing method](#add-missing-method) - [Active Record Encryption](#active-record-encryption) - [Disable partial_inserts as default](#disable-partial_inserts-as-default) - [Active storage pre-defined variants](#active-storage-pre-defined-variants) # Add ComparisonValidator ### Before Rails 7 ```ruby 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 ```ruby 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: ```ruby # == 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: ```ruby 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 ```ruby 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 ```ruby 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 ```ruby 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. ```ruby 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 ```ruby 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. ```ruby 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 ```ruby 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. ```ruby > event.organizer => # > event.organizer = Organizer.second => # > 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. ```ruby class User scope :active, -> { where(accepted: true, locked: false) } end ``` ### Before ```ruby active_users = User.active inactive_users = User.where.not(id: User.active.ids) ``` ### After ```ruby active_users = User.active inactive_users = User.active.invert_where ``` - More examples: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods.html#method-i-invert_where - Side effects of `invert_where`: https://blog.kiprosh.com/side-effects-of-activerecords-new-feature-invert_where-in-rails-7/ # 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 ```ruby > 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 ```ruby class Post < ApplicationRecord encrypts :title end ``` ```ruby > 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: ```ruby > 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. ```ruby class Post < ApplicationRecord encrypts :title, deterministic: true end ``` ```ruby > 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' # => ``` * Guide1: https://edgeguides.rubyonrails.org/active_record_encryption.html * Guide2: https://blog.kiprosh.com/activerecord-encryption-in-rails-7/ # Disable partial_inserts as default ```ruby # == 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 ```ruby > 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 ```ruby > 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 ```ruby class Puppy < ApplicationRecord has_one_attached :photo end ``` `<%= image_tag puppy.photo.variant(resize_to_fill: [250, 250]) %>` ### After ```ruby 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) %>`