Activerecord Arel
Arel
https://github.com/rails/arel/tree/7-1-stable https://github.com/rails/rails/blob/master/activerecord/lib/arel/predications.rb https://www.youtube.com/watch?v=ShPAxNcLm3o&feature=youtu.be
left_outer_joins
is available in recent
rails
but usually you do not need joins.
For example
# instead of joins
def self.with_approved_picture
left_outer_joins(:member_pictures)
.where.not("member_pictures.id": nil)
.where(member_pictures: { status: MemberPicture.statuses[:approved] })
.distinct # since there could be multiple pictures
end
# we can write
def self.with_approved_picture
where id: MemberPicture.approved.select(:member_profile_id)
end
Online SQL to Arel www.scuttle.io EXCELLENT Playground https://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html
bundle console
cheatsheet https://devhints.io/arel
arel_table = User.arel_table
arel_table = Arel::Table.new(:users)
arel_col = arel_table[:email]
# select fields
# NOTE that AS should use string instead of symbols
arel_table.project(arel_col, arel_col.as('custom_name')).to_sql
# aggregates
arel_table.project arel_table[:age].sum
arel_table.project arel_table[:age].count.as('user_count')
# limit offset
arel_table.take(3).skip(2)
# order
arel_table.order(arel_col, arel_col.desc)
# where restrictions
arel_table.where arel_col.eq '[email protected]'
arel_col.eq 'string'
arel_col.gteq Date.today.beginning_of_month
arel_col.matches "%#{query}%" # generate ILIKE
# AR use joins(:photos) but Arel use join
photos = Photo.arel_table
arel_table.join(photos, Arel::Nodes::OuterJoin).on(arel_col.eq photos[:user_id]).project(photos[:name].as('photo_name'))
# functions
SEPARATOR = Arel::Nodes.build_quoted(' ')
Arel::Nodes::NamedFunction.new(
'concat',
[arel_table[:first_name], SEPARATOR, arel_table[:last_name]]
)
True relation example is (found using online arel editor http://www.scuttle.io )
Arel::Nodes::SqlLiteral.new('1').eq(1)
# Arel::Nodes::SqlLiteral.new('true') is also true relation, but can not be
# first, ie Arel::Nodes::SqlLiteral.new('true').and raises error
# NoMethodError (undefined method `and' for "true":Arel::Nodes::SqlLiteral):
To execute you can use
User.select(Arel.star).to_sql # SELECT * FROM users
# note that if you use joins it will select all columns (id of joined table will
# override id of User) so better is to use with table
User.select(User.arel_table[Arel.star]).to_sql # SELECT users.* FROM users
User.select(User.arel_table[:email].as('m')).first.m
User.where(User.arel_table[:email].eq('Hi')) # it the same as User.where(email: 'Hi')
User.where(User.areal_table[:log_count].gt(200)) # SELECT users.* FORM users WHERE (users.log_count > 200)
User.where(
User.arel_table[:email].eq('hi').and(
User.arel_table[:id].eq(22).or(
User.arel_table[:id].in(23,34)
)
)
)
Tips
- in one request there should be only one query per table
- instead of
count
use.size
(which will use ruby.length
if loaded). To force loading (for example to show sieze before.each
is performed) you can useusers.load.size
. In this case there is no edditional query forusers.each
. Only way when.count
is used is when you actually do not load all records, for example showing some link for all items. Similarly do not useempty?
,any?
withoutload
. Do not usepresent?
orblank?
if you do not need to load all.exists?
will always executes sql query. - do not write
QueryMethods,
FinderMethods,
and Calculations
inside methods on ActiveRecord objects since someone will eventually use then
in index action and N+1 will be triggered. Use scopes instead if you need all
nested recources (you can have several associations based on one table)
class Post has_many :comments has_many :active_comments, -> { active }, class_name: "Comment" end class Comment belongs_to :post scope :active, -> { where(soft_deleted: false) } end class PostsController def index @posts = Post.includes(:active_comments) end end
- to fetch all indexes for given table name use
@connection ||= ActiveRecord::Base.connection @tables ||= if ActiveRecord::VERSION::MAJOR == 5 connection.data_sources else connection.tables end @models = ActiveRecord::Base.descendants # reject table_name == 'schema_migrations' @connection.indexes(@model.table_name)
- to find belongs_to associations
model.reflects_on_all_associations(:belongs_to).
- if you have composite index on column [a, b, c] than you can use
where a == 1
orwhere a == 1 and b == 2
orwhere a == 1 and b == 2 and c == 3
. you can not usewhere b == 2
alone since it will examine all rows, in aws rds console under database -> Logs & events -> Logs -> search for slowquery.log (with bigger size, created around the time the server was slow) you can see Rows_examinated: 123456 … Also you can find on left menu Performance Insights -> Database load -> Top SQL -> Rows examined - squash migrations, just generate new one and copy content from
db/schema.rb
You can use gem squasher https://gihttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html#finding-quick-start-amihttps://docs.aws.amazon.com/AWSEC2/latest/UserGuide/finding-an-ami.html#finding-quick-start-amithub.com/jalkoby/squasher or you can manually merge migrations - if you override setters, for example
class User def name=(name) self.name = "#{company.name} child" end end company.users.new(name: 'Hi') # this will raise error since company is is not defined so better is to use company.users.new(name: 'Hi', company: company)
- use OR or statement like
Book.where(category: "Programming").or(Book.where(category: "Ruby")) member_profile.member_pictures.where(status: 'pending').or(member_profile.member_pictures.where(status: 'approved')) scope :web, -> { where.not("last_seen_user_agent LIKE '%iOS_app%' OR last_seen_user_agent LIKE '%Android_app%'").or(where last_seen_user_agent: nil) }
users.page(page).size
when applied to ActiveRecord and pagination, it might raisePG::UndefinedColumn: ERROR: column "distance" does not exist LINE 1: ...g" = $3 AND "member_profiles"."id" != $4 ORDER BY distance A...
but it works if you convert to array
users.page(page).to_a.size
(this exception is raised even when there is a single page, for example one result) Solution to this is usingusers = users.distinct