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
countuse.size(which will use ruby.lengthif loaded). To force loading (for example to show sieze before.eachis performed) you can useusers.load.size. In this case there is no edditional query forusers.each. Only way when.countis 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 == 1orwhere a == 1 and b == 2orwhere a == 1 and b == 2 and c == 3. you can not usewhere b == 2alone 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.rbYou 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).sizewhen 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