- Add ComparisonValidator
- PostgreSQL generated columns
- PostgreSQL custom enum types
- Add tracking of belongs_to association
- association_previously_changed? method
- Add invert_where method
- Add associated method
- Add missing method
- Active Record Encryption
- Disable partial_inserts as default
- Active storage pre-defined variants
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
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/
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
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/
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
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
class Event
belongs_to :organizer
end
class Organizer
has_many :events
end
The
association_changed?
method tells if a different associated object has been assigned and the foreign key will be updated in the next save.
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
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
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/
Allows you to invert an entire where clause instead of manually applying conditions.
class User
scope :active, -> { where(accepted: true, locked: false) }
end
active_users = User.active
inactive_users = User.where.not(id: User.active.ids)
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/
It returns the list of all records that have an association
User.where.not(contact_id: nil)
User.where.associated(:contact)
more examples: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-associated
It returns the list of all records that don't have an association. opposite of associated
User.where(contact_id: nil)
User.where.missing(:contact)
more examples: https://api.rubyonrails.org/classes/ActiveRecord/QueryMethods/WhereChain.html#method-i-missing
> Post.create(title: 'Rails 7')
INSERT INTO "posts" ("title") VALUES (?) [["title", "Rails 7"]]
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)
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"...>
- Guide1: https://edgeguides.rubyonrails.org/active_record_encryption.html
- Guide2: https://blog.kiprosh.com/activerecord-encryption-in-rails-7/
# == Schema Information
#
# Table name: posts
#
# id :bigint
# title :string
# description :text
# created_at :datetime
# updated_at :datetime
class Post < ApplicationRecord
end
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"]]
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/
class Puppy < ApplicationRecord
has_one_attached :photo
end
<%= image_tag puppy.photo.variant(resize_to_fill: [250, 250]) %>
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) %>