модуль ActiveRecord::QueryMethods
Константы
- FROZEN_EMPTY_ARRAY
- FROZEN_EMPTY_HASH
- VALID_UNSCOPING_VALUES
Открытые методы экземпляра
# File activerecord/lib/active_record/relation/query_methods.rb, line 1135
def and(other)
if other.is_a?(Relation)
spawn.and!(other)
else
raise ArgumentError, "You have passed #{other.class.name} object to #and. Pass an ActiveRecord::Relation object instead."
end
end Возвращает новое отношение, представляющее собой логическое пересечение этого отношения и отношения, переданного в качестве аргумента.
Эти два отношения должны быть структурно совместимы: они должны относиться к одной и той же модели и различаться только по where (если group не задан) или по having (если задан group).
Post.where(id: [1, 2]).and(Post.where(id: [2, 3])) # SELECT `posts`.* FROM `posts` WHERE `posts`.`id` IN (1, 2) AND `posts`.`id` IN (2, 3)
# File activerecord/lib/active_record/relation/query_methods.rb, line 1530 def annotate(*args) check_if_method_has_arguments!(__callee__, args) spawn.annotate!(*args) end
Добавляет комментарий SQL к запросам, сгенерированным из этого отношения. Например:
User.annotate("selecting user names").select(:name)
# SELECT "users"."name" FROM "users" /* selecting user names */
User.annotate("selecting", "user", "names").select(:name)
# SELECT "users"."name" FROM "users" /* selecting */ /* user */ /* names */
Разделители блочного комментария SQL «/*» и «*/» будут добавлены автоматически.
Выполняется экранирование, однако не следует использовать недоверенные пользовательские данные.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1347 def create_with(value) spawn.create_with!(value) end
Задает атрибуты, которые будут использоваться при создании новых записей из объекта отношения.
users = User.where(name: 'Oscar') users.new.name # => 'Oscar' users = users.create_with(name: 'DHH') users.new.name # => 'DHH'
Чтобы сбросить атрибуты, можно передать nil в create_with:
users = users.create_with(nil) users.new.name # => 'Oscar'
# File activerecord/lib/active_record/relation/query_methods.rb, line 1411 def distinct(value = true) spawn.distinct!(value) end
Указывает, должны ли записи быть уникальными. Например:
User.select(:name) # Might return two records with the same name User.select(:name).distinct # Returns 1 record per distinct name User.select(:name).distinct.distinct(false) # You can also remove the uniqueness
# File activerecord/lib/active_record/relation/query_methods.rb, line 290 def eager_load(*args) check_if_method_has_arguments!(__callee__, args) spawn.eager_load!(*args) end
Указывает ассоциации args, которые нужно загрузить заранее с помощью LEFT OUTER JOIN. Выполняет один запрос с объединением всех указанных ассоциаций. Например:
users = User.eager_load(:address).limit(5) users.each do |user| user.address.city end # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users" # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id" # LIMIT 5
Вместо загрузки 5 адресов с помощью 5 отдельных запросов все адреса загружаются одним запросом с объединением.
Можно загружать несколько вложенных ассоциаций с помощью хешей и массивов, как и в случае с includes:
User.eager_load(:address, friends: [:address, :followers]) # SELECT "users"."id" AS t0_r0, "users"."name" AS t0_r1, ... FROM "users" # LEFT OUTER JOIN "addresses" ON "addresses"."id" = "users"."address_id" # LEFT OUTER JOIN "friends" ON "friends"."user_id" = "users"."id" # ...
ПРИМЕЧАНИЕ: Загрузка ассоциаций с помощью объединения может привести к появлению множества строк с избыточными данными и плохо масштабируется.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1575
def excluding(*records)
relations = records.extract! { |element| element.is_a?(Relation) }
records.flatten!(1)
records.compact!
unless records.all?(model) && relations.all? { |relation| relation.model == model }
raise ArgumentError, "You must only pass a single or collection of #{model.name} objects to ##{__callee__}."
end
spawn.excluding!(records + relations.flat_map(&:ids))
end Исключает указанную запись (или коллекцию записей) из результирующего отношения. Например:
Post.excluding(post) # SELECT "posts".* FROM "posts" WHERE "posts"."id" != 1 Post.excluding(post_one, post_two) # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (1, 2) Post.excluding(Post.drafts) # SELECT "posts".* FROM "posts" WHERE "posts"."id" NOT IN (3, 4, 5)
Этот метод также можно вызывать для ассоциаций. Как и в примере выше, можно указать одну запись или коллекцию записей:
post = Post.find(1) comment = Comment.find(2) post.comments.excluding(comment) # SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 AND "comments"."id" != 2
Это сокращенная запись для .where.not(id: post.id) и .where.not(id: [post_one.id, post_two.id]).
Будет вызвано исключение ArgumentError, если записи не указаны или если какая-либо запись в коллекции (если передана коллекция) не является экземпляром той же модели, к которой относится отношение.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1457
def extending(*modules, &block)
if modules.any? || block
spawn.extending!(*modules, &block)
else
self
end
end Используется для расширения области видимости дополнительными методами — через модуль или переданный блок.
Возвращаемый объект является отношением, которое можно расширять дальше.
Использование модуля
module Pagination
def page(number)
# pagination code goes here
end
end
scope = Model.all.extending(Pagination)
scope.page(params[:page])
Также можно передать список модулей:
scope = Model.all.extending(Pagination, SomethingElse)
Использование блока
scope = Model.all.extending do
def page(number)
# pagination code goes here
end
end
scope.page(params[:page])
Также можно использовать блок и список модулей:
scope = Model.all.extending(Pagination) do
def per_page(number)
# pagination code goes here
end
end
# File activerecord/lib/active_record/relation/query_methods.rb, line 341 def extract_associated(association) preload(association).collect(&association) end
Извлекает именованную association из отношения. Сначала именованная ассоциация предварительно загружается, затем из отношения собираются отдельные записи ассоциации. Например:
account.memberships.extract_associated(:user) # => Returns collection of User records
Это сокращенная запись для:
account.memberships.preload(:user).collect(&:user)
# File activerecord/lib/active_record/relation/query_methods.rb, line 1392 def from(value, subquery_name = nil) spawn.from!(value, subquery_name) end
Указывает таблицу, из которой будут извлекаться записи. Например:
Topic.select('title').from('posts')
# SELECT title FROM posts
Можно передать другие объекты отношений. Например:
Topic.select('title').from(Topic.approved)
# SELECT title FROM (SELECT * FROM topics WHERE approved = 't') subquery
Если передать второй аргумент (строку или символ), он задаст псевдоним для выражения FROM в SQL. В противном случае используется псевдоним «subquery»:
Topic.select('a.title').from(Topic.approved, :a)
# SELECT a.title FROM (SELECT * FROM topics WHERE approved = 't') a
Несколько аргументов в выражение FROM SQL не добавляются. Используется последний добавленный вызов from:
Topic.select('title').from(Topic.approved).from(Topic.inactive)
# SELECT title FROM (SELECT topics.* FROM topics WHERE topics.active = 'f') subquery
Чтобы передать несколько аргументов для выражения FROM SQL, можно указать строку с точными элементами списка FROM в SQL:
color = "red"
Color
.from("colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue)")
.where("colorvalue->>'color' = ?", color)
.select("c.*").to_a
# SELECT c.*
# FROM colors c, JSONB_ARRAY_ELEMENTS(colored_things) AS colorvalues(colorvalue)
# WHERE (colorvalue->>'color' = 'red')
# File activerecord/lib/active_record/relation/query_methods.rb, line 573 def group(*args) check_if_method_has_arguments!(__callee__, args) spawn.group!(*args) end
Позволяет указать атрибут группировки:
User.group(:name) # SELECT "users".* FROM "users" GROUP BY name
Возвращает массив с уникальными записями, сгруппированными по атрибуту group:
User.select([:id, :name])
# => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
User.group('name AS grouped_name, age')
# => [#<User id: 3, name: "Foo", age: 21, ...>, #<User id: 2, name: "Oscar", age: 21, ...>, #<User id: 5, name: "Foo", age: 23, ...>]
Также можно передать массив атрибутов для группировки.
User.select([:id, :first_name]).group(:id, :first_name).first(3) # => [#<User id: 1, first_name: "Bill">, #<User id: 2, first_name: "Earl">, #<User id: 3, first_name: "Beto">]
# File activerecord/lib/active_record/relation/query_methods.rb, line 1197 def having(opts, *rest) opts.blank? ? self : spawn.having!(opts, *rest) end
Позволяет указать условие HAVING. Обратите внимание: HAVING нельзя использовать без указания условия GROUP.
Order.having('SUM(price) > 30').group('user_id')
# File activerecord/lib/active_record/relation/query_methods.rb, line 717
def in_order_of(column, values, filter: true)
model.disallow_raw_sql!([column], permit: model.adapter_class.column_name_with_order_matcher)
return spawn.none! if values.empty?
references = column_references([column])
self.references_values |= references unless references.empty?
values = values.map { |value| model.type_caster.type_cast_for_database(column, value) }
arel_column = column.is_a?(Arel::Nodes::SqlLiteral) ? column : order_column(column.to_s)
scope = spawn.order!(build_case_for_value_position(arel_column, values, filter: filter))
if filter
where_clause =
if values.include?(nil)
arel_column.in(values.compact).or(arel_column.eq(nil))
else
arel_column.in(values)
end
scope = scope.where!(where_clause)
end
scope
end Применяет условие ORDER BY на основе заданного column, сортируя и фильтруя результаты по определенному набору values.
User.in_order_of(:id, [1, 5, 3]) # SELECT "users".* FROM "users" # WHERE "users"."id" IN (1, 5, 3) # ORDER BY CASE # WHEN "users"."id" = 1 THEN 1 # WHEN "users"."id" = 5 THEN 2 # WHEN "users"."id" = 3 THEN 3 # END ASC
column может ссылаться на столбец enum; фактически сгенерированный запрос может различаться в зависимости от адаптера базы данных и определения столбца.
class Conversation < ActiveRecord::Base enum :status, [ :active, :archived ] end Conversation.in_order_of(:status, [:archived, :active]) # SELECT "conversations".* FROM "conversations" # WHERE "conversations"."status" IN (1, 0) # ORDER BY CASE # WHEN "conversations"."status" = 1 THEN 1 # WHEN "conversations"."status" = 0 THEN 2 # END ASC
values также может содержать nil.
Conversation.in_order_of(:status, [nil, :archived, :active])
# SELECT "conversations".* FROM "conversations"
# WHERE ("conversations"."status" IN (1, 0) OR "conversations"."status" IS NULL)
# ORDER BY CASE
# WHEN "conversations"."status" IS NULL THEN 1
# WHEN "conversations"."status" = 1 THEN 2
# WHEN "conversations"."status" = 0 THEN 3
# END ASC
Для filter можно задать значение false, чтобы включить все результаты, а не только указанные в values.
Conversation.in_order_of(:status, [:archived, :active], filter: false) # SELECT "conversations".* FROM "conversations" # ORDER BY CASE # WHEN "conversations"."status" = 1 THEN 1 # WHEN "conversations"."status" = 0 THEN 2 # ELSE 3 # END ASC
# File activerecord/lib/active_record/relation/query_methods.rb, line 250 def includes(*args) check_if_method_has_arguments!(__callee__, args) spawn.includes!(*args) end
Указывает ассоциации args, которые нужно загрузить заранее, чтобы избежать запросов N + 1. Для каждой ассоциации выполняется отдельный запрос, если только условия не требуют объединения таблиц.
Например:
users = User.includes(:address).limit(5) users.each do |user| user.address.city end # SELECT "users".* FROM "users" LIMIT 5 # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
Вместо загрузки 5 адресов с помощью 5 отдельных запросов все адреса загружаются одним запросом.
Загрузка ассоциаций отдельным запросом часто повышает производительность по сравнению с простым объединением, поскольку объединение может привести к появлению множества строк с избыточными данными и плохо масштабируется.
Можно также указать несколько ассоциаций. Для каждой ассоциации будет выполнен дополнительный запрос:
User.includes(:address, :friends).to_a # SELECT "users".* FROM "users" # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5)
Вложенные ассоциации можно загружать с помощью хеша:
User.includes(:address, friends: [:address, :followers])
Условия
Чтобы добавить строковые условия для включенных моделей, необходимо явно указать ссылки на них. Например:
User.includes(:posts).where('posts.name = ?', 'example').to_a
Это приведет к ошибке, а следующий вариант будет работать:
User.includes(:posts).where('posts.name = ?', 'example').references(:posts).to_a
# SELECT "users"."id" AS t0_r0, ... FROM "users"
# LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# WHERE "posts"."name" = ? [["name", "example"]]
Поскольку LEFT OUTER JOIN уже содержит записи posts, второй запрос для posts больше не выполняется.
Обратите внимание: includes использует имена ассоциаций, тогда как references требует фактическое имя таблицы.
Если передать условия через Hash, вызывать references явно не нужно, поскольку where самостоятельно ссылается на таблицы. Например, следующий вариант будет работать правильно:
User.includes(:posts).where(posts: { name: 'example' })
ПРИМЕЧАНИЕ: Условия влияют на обе стороны ассоциации. Например, приведенный выше код вернет только пользователей, у которых есть запись post с именем «example», и включит только записи posts с именем «example», даже если у соответствующего пользователя есть другие записи posts.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1101 def invert_where spawn.invert_where! end
Позволяет инвертировать все условие where вместо того, чтобы применять условия вручную.
class User
scope :active, -> { where(accepted: true, locked: false) }
end
User.where(accepted: true)
# WHERE `accepted` = 1
User.where(accepted: true).invert_where
# WHERE `accepted` != 1
User.active
# WHERE `accepted` = 1 AND `locked` = 0
User.active.invert_where
# WHERE NOT (`accepted` = 1 AND `locked` = 0)
Будьте осторожны: при вызове invert_where инвертируются все предшествующие условия.
class User
scope :active, -> { where(accepted: true, locked: false) }
scope :inactive, -> { active.invert_where } # Do not attempt it
end
# It also inverts `where(role: 'admin')` unexpectedly.
User.where(role: 'admin').inactive
# WHERE NOT (`role` = 'admin' AND `accepted` = 1 AND `locked` = 0)
# File activerecord/lib/active_record/relation/query_methods.rb, line 868 def joins(*args) check_if_method_has_arguments!(__callee__, args) spawn.joins!(*args) end
Выполняет JOIN для args. Переданный символ или символы должны соответствовать именам ассоциаций.
User.joins(:posts) # SELECT "users".* # FROM "users" # INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
Несколько объединений:
User.joins(:posts, :account) # SELECT "users".* # FROM "users" # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id"
Вложенные объединения:
User.joins(posts: [:comments]) # SELECT "users".* # FROM "users" # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" # INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
Для настройки объединений можно использовать строки:
User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
# SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
# File activerecord/lib/active_record/relation/query_methods.rb, line 883 def left_outer_joins(*args) check_if_method_has_arguments!(__callee__, args) spawn.left_outer_joins!(*args) end
Выполняет LEFT OUTER JOIN для args:
User.left_outer_joins(:posts) # SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
# File activerecord/lib/active_record/relation/query_methods.rb, line 1211 def limit(value) spawn.limit!(value) end
Задает ограничение на количество извлекаемых записей.
User.limit(10) # generated SQL has 'LIMIT 10' User.limit(10).limit(20) # generated SQL has 'LIMIT 20'
# File activerecord/lib/active_record/relation/query_methods.rb, line 1239 def lock(locks = true) spawn.lock!(locks) end
Задает параметры блокировки (по умолчанию true). Дополнительные сведения о блокировках см. в разделе ActiveRecord::Locking.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1282 def none spawn.none! end
Возвращает цепочечное отношение, не содержащее записей.
Возвращаемое отношение реализует шаблон Null Object. Это объект с определенным поведением для пустого значения, который всегда возвращает пустой массив записей, не обращаясь к базе данных.
Любое последующее условие, добавленное к возвращенному отношению, продолжит формировать пустое отношение и не приведет к выполнению запросов к базе данных.
Используется в случаях, когда метод или область видимости может вернуть ноль записей, но результат должен поддерживать цепочку вызовов.
Например:
@posts = current_user.visible_posts.where(name: params[:name])
# the visible_posts method is expected to return a chainable Relation
def visible_posts
case role
when 'Country Manager'
Post.where(country: country)
when 'Reviewer'
Post.published
when 'Bad User'
Post.none # It can't be chained if [] is returned.
end
end
# File activerecord/lib/active_record/relation/query_methods.rb, line 1228 def offset(value) spawn.offset!(value) end
Задает количество строк, которые нужно пропустить перед возвратом результатов.
User.offset(10) # generated SQL has "OFFSET 10"
Следует использовать вместе с order.
User.offset(10).order("name ASC")
# File activerecord/lib/active_record/relation/query_methods.rb, line 1486 def optimizer_hints(*args) check_if_method_has_arguments!(__callee__, args) spawn.optimizer_hints!(*args) end
Указывает подсказки оптимизатору, которые будут использоваться в инструкции SELECT.
Пример (для MySQL):
Topic.optimizer_hints("MAX_EXECUTION_TIME(50000)", "NO_INDEX_MERGE(topics)")
# SELECT /*+ MAX_EXECUTION_TIME(50000) NO_INDEX_MERGE(topics) */ `topics`.* FROM `topics`
Пример (для PostgreSQL с pg_hint_plan):
Topic.optimizer_hints("SeqScan(topics)", "Parallel(topics 8)")
# SELECT /*+ SeqScan(topics) Parallel(topics 8) */ "topics".* FROM "topics"
# File activerecord/lib/active_record/relation/query_methods.rb, line 1167
def or(other)
if other.is_a?(Relation)
if @none
other.spawn
else
spawn.or!(other)
end
else
raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead."
end
end Возвращает новое отношение, представляющее собой логическое объединение этого отношения и отношения, переданного в качестве аргумента.
Эти два отношения должны быть структурно совместимы: они должны относиться к одной и той же модели и различаться только по where (если group не задан) или по having (если задан group).
Post.where("id = 1").or(Post.where("author_id = 3"))
# SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3))
# File activerecord/lib/active_record/relation/query_methods.rb, line 656
def order(*args)
check_if_method_has_arguments!(__callee__, args) do
sanitize_order_arguments(args)
end
spawn.order!(*args)
end Добавляет к запросу условие ORDER BY.
order принимает аргументы в одном из нескольких форматов.
Символы
Символ обозначает имя столбца, по которому нужно сортировать результаты.
User.order(:name) # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC
По умолчанию используется сортировка по возрастанию. Чтобы отсортировать по убыванию, сопоставьте символ имени столбца со значением :desc.
User.order(email: :desc) # SELECT "users".* FROM "users" ORDER BY "users"."email" DESC
Таким образом можно передать несколько столбцов; они будут применены в указанном порядке.
User.order(:name, email: :desc) # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
Строки
Строки передаются напрямую в базу данных, что позволяет указывать простые выражения SQL.
Это может стать источником SQL-инъекции, поэтому разрешены только строки, состоящие из обычных имен столбцов и простых выражений function(column_name) с необязательными модификаторами ASC/DESC.
User.order('name')
# SELECT "users".* FROM "users" ORDER BY name
User.order('name DESC')
# SELECT "users".* FROM "users" ORDER BY name DESC
User.order('name DESC, email')
# SELECT "users".* FROM "users" ORDER BY name DESC, email
Arel
Если нужно передать сложные выражения, безопасность которых для базы данных проверена, можно использовать Arel.
User.order(Arel.sql('end_date - start_date'))
# SELECT "users".* FROM "users" ORDER BY end_date - start_date
Таким образом поддерживается пользовательский синтаксис запросов, например работа с JSON-столбцами в PostgreSQL.
User.order(Arel.sql("payload->>'kind'"))
# SELECT "users".* FROM "users" ORDER BY payload->>'kind'
# File activerecord/lib/active_record/relation/query_methods.rb, line 322 def preload(*args) check_if_method_has_arguments!(__callee__, args) spawn.preload!(*args) end
Указывает ассоциации args, которые нужно загрузить заранее с помощью отдельных запросов. Для каждой ассоциации выполняется отдельный запрос.
users = User.preload(:address).limit(5) users.each do |user| user.address.city end # SELECT "users".* FROM "users" LIMIT 5 # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5)
Вместо загрузки 5 адресов с помощью 5 отдельных запросов все адреса загружаются одним отдельным запросом.
Можно загружать несколько вложенных ассоциаций с помощью хешей и массивов, как и в случае с includes:
User.preload(:address, friends: [:address, :followers]) # SELECT "users".* FROM "users" # SELECT "addresses".* FROM "addresses" WHERE "addresses"."id" IN (1,2,3,4,5) # SELECT "friends".* FROM "friends" WHERE "friends"."user_id" IN (1,2,3,4,5) # SELECT ...
# File activerecord/lib/active_record/relation/query_methods.rb, line 1310 def readonly(value = true) spawn.readonly!(value) end
Помечает отношение как доступное только для чтения. Попытка обновить запись приведет к ошибке.
users = User.readonly users.first.save # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
Чтобы сделать отношение, доступное только для чтения, доступным для записи, передайте false.
users.readonly(false) users.first.save # => true
# File activerecord/lib/active_record/relation/query_methods.rb, line 355 def references(*table_names) check_if_method_has_arguments!(__callee__, table_names) spawn.references!(*table_names) end
Используется, чтобы указать, что на заданные table_names ссылается строка SQL, поэтому их следует объединить с помощью +JOIN+ в любом запросе, а не загружать отдельно. Этот метод работает только вместе с includes. Дополнительные сведения см. в разделе includes.
User.includes(:posts).where("posts.name = 'foo'")
# Doesn't JOIN the posts table, resulting in an error.
User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# Query now knows the string references posts, so adds a JOIN
# File activerecord/lib/active_record/relation/query_methods.rb, line 593 def regroup(*args) check_if_method_has_arguments!(__callee__, args) spawn.regroup!(*args) end
Позволяет изменить ранее заданное условие группировки.
Post.group(:title, :body) # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`, `posts`.`body` Post.group(:title, :body).regroup(:title) # SELECT `posts`.`*` FROM `posts` GROUP BY `posts`.`title`
Это сокращенная запись для unscope(:group).group(fields). Обратите внимание, что область видимости очищается от всего условия группировки.
# File activerecord/lib/active_record/relation/query_methods.rb, line 752
def reorder(*args)
check_if_method_has_arguments!(__callee__, args) do
sanitize_order_arguments(args)
end
spawn.reorder!(*args)
end Заменяет все существующие условия сортировки отношения на указанные.
User.order('email DESC').reorder('id ASC') # generated SQL has 'ORDER BY id ASC'
Последующие вызовы order для того же отношения будут добавлены к текущему условию. Например:
User.order('email DESC').reorder('id ASC').order('name ASC')
создает запрос с ORDER BY id ASC, name ASC.
# File activerecord/lib/active_record/relation/query_methods.rb, line 541 def reselect(*args) check_if_method_has_arguments!(__callee__, args) args = process_select_args(args) spawn.reselect!(*args) end
Позволяет изменить ранее заданную инструкцию select.
Post.select(:title, :body) # SELECT `posts`.`title`, `posts`.`body` FROM `posts` Post.select(:title, :body).reselect(:created_at) # SELECT `posts`.`created_at` FROM `posts`
Это сокращенная запись для unscope(:select).select(fields). Обратите внимание, что область видимости очищается от всей инструкции select.
# File activerecord/lib/active_record/relation/query_methods.rb, line 1499 def reverse_order spawn.reverse_order! end
Обращает порядок сортировки, заданный для отношения.
User.order('name ASC').reverse_order # generated SQL has 'ORDER BY name DESC'
# File activerecord/lib/active_record/relation/query_methods.rb, line 1061 def rewhere(conditions) return unscope(:where) if conditions.nil? scope = spawn where_clause = scope.build_where_clause(conditions) scope.unscope!(where: where_clause.extract_attributes) scope.where_clause += where_clause scope end
Позволяет изменить ранее заданное условие where для указанного атрибута, а не добавлять новое условие.
Post.where(trashed: true).where(trashed: false) # WHERE `trashed` = 1 AND `trashed` = 0 Post.where(trashed: true).rewhere(trashed: false) # WHERE `trashed` = 0 Post.where(active: true).where(trashed: true).rewhere(trashed: false) # WHERE `active` = 1 AND `trashed` = 0
Это сокращенная запись для unscope(where: conditions.keys).where(conditions). Обратите внимание: в отличие от reorder, область видимости очищается только от указанных условий, а не от всего условия where.
# File activerecord/lib/active_record/relation/query_methods.rb, line 413
def select(*fields)
if block_given?
if fields.any?
raise ArgumentError, "`select' with block doesn't take arguments."
end
return super()
end
check_if_method_has_arguments!(__callee__, fields, "Call `select' with at least one field.")
fields = process_select_args(fields)
spawn._select!(*fields)
end Работает двумя способами.
Во-первых, принимает блок и может использоваться так же, как Array#select.
Model.all.select { |m| m.field == value }
Этот метод создает массив объектов из базы данных для данной области видимости, преобразует их в массив и перебирает с помощью Array#select.
Во-вторых, изменяет инструкцию SELECT запроса, чтобы извлекались только определенные поля:
Model.select(:field) # => [#<Model id: nil, field: "value">]
Хотя в приведенном выше примере может показаться, что этот метод возвращает массив, на самом деле он возвращает объект отношения, к которому можно добавлять другие методы запросов, например другие методы из ActiveRecord::QueryMethods.
Аргументом метода также может быть массив полей.
Model.select(:field, :other_field, :and_one_more) # => [#<Model id: nil, field: "value", other_field: "value", and_one_more: "value">]
Аргументом также может быть хеш полей и псевдонимов.
Model.select(models: { field: :alias, other_field: :other_alias })
# => [#<Model id: nil, alias: "value", other_alias: "value">]
Model.select(models: [:field, :other_field])
# => [#<Model id: nil, field: "value", other_field: "value">]
Также можно использовать одну или несколько строк, которые будут без изменений использованы в качестве полей SELECT.
Model.select('field AS field_one', 'other_field AS field_two')
# => [#<Model id: nil, field_one: "value", field_two: "value">]
Если указан псевдоним, он будет доступен в результирующих объектах:
Model.select('field AS field_one').first.field_one
# => "value"
Обращение к атрибутам объекта, поля которых не были извлечены с помощью select, за исключением id, вызовет исключение ActiveModel::MissingAttributeError:
Model.select(:field).first.other_field # => ActiveModel::MissingAttributeError: missing attribute 'other_field' for Model
# File activerecord/lib/active_record/relation/query_methods.rb, line 1325 def strict_loading(value = true) spawn.strict_loading!(value) end
Переводит возвращаемое отношение в режим strict_loading. Если запись попытается лениво загрузить ассоциацию, будет вызвана ошибка.
user = User.strict_loading.first user.comments.to_a # => ActiveRecord::StrictLoadingViolationError
# File activerecord/lib/active_record/relation/query_methods.rb, line 1121 def structurally_compatible?(other) structurally_incompatible_values_for(other).empty? end
Проверяет, структурно ли совместимо указанное отношение с этим отношением, чтобы определить, можно ли использовать методы and и or без возникновения ошибки. Структурная совместимость определяется следующим образом: они должны относиться к одной и той же модели и различаться только по where (если не определён group) или по having (если присутствует group).
Post.where("id = 1").structurally_compatible?(Post.where("author_id = 3"))
# => true
Post.joins(:comments).structurally_compatible?(Post.where("id = 1"))
# => false
# File activerecord/lib/active_record/relation/query_methods.rb, line 1542
def uniq!(name)
if values = @values[name]
values.uniq! if values.is_a?(Array) && !values.empty?
end
self
end Удаляет повторяющиеся значения.
# File activerecord/lib/active_record/relation/query_methods.rb, line 806 def unscope(*args) check_if_method_has_arguments!(__callee__, args) spawn.unscope!(*args) end
Удаляет нежелательное отношение, уже определённое в цепочке отношений. Это полезно, когда нужно передать цепочки отношений и изменить отношения, не создавая всю цепочку заново.
User.order('email DESC').unscope(:order) == User.all
Аргументы метода — это символы, соответствующие именам методов, для которых следует отменить область действия. Допустимые аргументы перечислены в VALID_UNSCOPING_VALUES. Метод также можно вызвать с несколькими аргументами. Например:
User.order('email DESC').select('id').where(name: "John")
.unscope(:order, :select, :where) == User.all
Кроме того, в качестве аргумента можно передать хеш, чтобы отменить область действия определённых значений :where. Для этого передайте хеш с одной парой «ключ-значение». Ключом должно быть :where, а значением — значение where, для которого нужно отменить область действия. Например:
User.where(name: "John", active: true).unscope(where: :name)
== User.where(active: true) Этот метод похож на except, но, в отличие от except, сохраняется при объединении отношений:
User.order('email').merge(User.except(:order))
== User.order('email')
User.order('email').merge(User.unscope(:order))
== User.all Это означает, что его можно использовать в определениях ассоциаций:
has_many :comments, -> { unscope(where: :trashed) }
# File activerecord/lib/active_record/relation/query_methods.rb, line 1033
def where(*args)
if args.empty?
WhereChain.new(spawn)
elsif args.length == 1 && args.first.blank?
self
else
spawn.where!(*args)
end
end Возвращает новое отношение, полученное фильтрацией текущего отношения по условиям, указанным в аргументах.
where принимает условия в одном из нескольких форматов. В примерах ниже приведён результирующий SQL для иллюстрации; фактически сгенерированный запрос может отличаться в зависимости от адаптера базы данных.
Строка
Одна строка без дополнительных аргументов передаётся конструктору запроса как фрагмент SQL и используется в предложении where запроса.
Client.where("orders_count = '2'")
# SELECT * from clients where orders_count = '2';
Обратите внимание: если неправильно формировать строку из пользовательского ввода, приложение может стать уязвимым для атак с внедрением кода. В качестве альтернативы рекомендуется использовать один из следующих методов.
Массив
Если передан массив, его первый элемент рассматривается как шаблон, а остальные элементы подставляются в шаблон для создания условия. Active Record формирует запрос, предотвращая атаки с внедрением кода, и при необходимости преобразует значения из типов Ruby в типы базы данных. Элементы подставляются в строку в том порядке, в котором они указаны.
User.where(["name = ? and email = ?", "Joe", "joe@example.com"]) # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
Кроме того, в шаблоне можно использовать именованные заполнители и передать хеш вторым элементом массива. Имена в шаблоне заменяются соответствующими значениями из хеша.
User.where(["name = :name and email = :email", { name: "Joe", email: "joe@example.com" }])
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
Это может сделать код более читаемым в сложных запросах.
Наконец, в шаблоне можно использовать подстановки % в стиле sprintf. Это работает немного иначе, чем предыдущие методы: вы отвечаете за правильное экранирование значений в шаблоне. Значения передаются коннектору для экранирования, но вызывающий код отвечает за то, чтобы в результирующем SQL они были заключены в кавычки. После экранирования значения подставляются с использованием тех же спецификаторов, что и в основном методе Ruby Kernel::sprintf.
User.where(["name = '%s' and email = '%s'", "Joe", "joe@example.com"]) # SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
Если where вызывается с несколькими аргументами, они обрабатываются так, как если бы были переданы в качестве элементов одного массива.
User.where("name = :name and email = :email", { name: "Joe", email: "joe@example.com" })
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com';
При использовании строк для указания условий можно применять любой оператор, доступный в базе данных. Это обеспечивает максимальную гибкость, но может также непреднамеренно привести к зависимости от используемой базы данных. Если ваш код предназначен для общего использования, протестируйте его с несколькими СУБД.
Хеш
where также принимает условие в виде хеша, где ключи — это поля, а значения — значения, которые нужно найти.
Поля могут быть символами или строками. Значения могут быть отдельными значениями, массивами или диапазонами.
User.where(name: "Joe", email: "joe@example.com")
# SELECT * FROM users WHERE name = 'Joe' AND email = 'joe@example.com'
User.where(name: ["Alice", "Bob"])
# SELECT * FROM users WHERE name IN ('Alice', 'Bob')
User.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
# SELECT * FROM users WHERE (created_at BETWEEN '2012-06-09 07:00:00.000000' AND '2012-06-10 07:00:00.000000')
В случае отношения belongs_to для указания модели можно использовать ключ ассоциации, если в качестве значения передан объект ActiveRecord.
author = Author.find(1) # The following queries will be equivalent: Post.where(author: author) Post.where(author_id: author)
Это также работает с полиморфными отношениями belongs_to:
treasure = Treasure.create(name: 'gold coins') treasure.price_estimates << PriceEstimate.create(price: 125) # The following queries will be equivalent: PriceEstimate.where(estimate_of: treasure) PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
Условия Hash также можно задавать в виде кортежей. Ключи Hash могут представлять собой массив столбцов, а значения — массив кортежей.
Article.where([:author_id, :id] => [[15, 1], [15, 2]]) # SELECT * FROM articles WHERE author_id = 15 AND id = 1 OR author_id = 15 AND id = 2
Соединения
Если отношение получено в результате соединения, можно создать условие, использующее любую из таблиц в соединении. Для условий в виде строк и массивов укажите имя таблицы в условии.
User.joins(:posts).where("posts.created_at < ?", Time.now)
Для условий в виде хеша можно указать имя таблицы в ключе или использовать вложенный хеш.
User.joins(:posts).where("posts.published" => true)
User.joins(:posts).where(posts: { published: true })
Без аргументов
Если аргумент не передан, where возвращает новый экземпляр WhereChain, к которому можно добавить вызовы WhereChain#not, WhereChain#missing или WhereChain#associated.
Цепочка вызовов с WhereChain#not:
User.where.not(name: "Jon") # SELECT * FROM users WHERE name != 'Jon'
Цепочка вызовов с WhereChain#associated:
Post.where.associated(:author) # SELECT "posts".* FROM "posts" # INNER JOIN "authors" ON "authors"."id" = "posts"."author_id" # WHERE "authors"."id" IS NOT NULL
Цепочка вызовов с WhereChain#missing:
Post.where.missing(:author) # SELECT "posts".* FROM "posts" # LEFT OUTER JOIN "authors" ON "authors"."id" = "posts"."author_id" # WHERE "authors"."id" IS NULL
Пустое условие
Если условие является пустым или условно пустым объектом, where ничего не делает и возвращает текущее отношение.
# File activerecord/lib/active_record/relation/query_methods.rb, line 493 def with(*args) raise ArgumentError, "ActiveRecord::Relation#with does not accept a block" if block_given? check_if_method_has_arguments!(__callee__, args) spawn.with!(*args) end
Добавляет общее табличное выражение (CTE), на которое затем можно ссылаться в другом операторе SELECT.
Примечание: CTE поддерживаются в MySQL только начиная с версии 8.0. Использовать CTE с MySQL 5.7 нельзя.
Post.with(posts_with_tags: Post.where("tags_count > ?", 0))
# => ActiveRecord::Relation
# WITH posts_with_tags AS (
# SELECT * FROM posts WHERE (tags_count > 0)
# )
# SELECT * FROM posts
Также можно передать массив подзапросов для объединения с помощью +UNION ALL+.
Post.with(posts_with_tags_or_comments: [Post.where("tags_count > ?", 0), Post.where("comments_count > ?", 0)])
# => ActiveRecord::Relation
# WITH posts_with_tags_or_comments AS (
# (SELECT * FROM posts WHERE (tags_count > 0))
# UNION ALL
# (SELECT * FROM posts WHERE (comments_count > 0))
# )
# SELECT * FROM posts
Определив общее табличное выражение, вы можете использовать пользовательское значение FROM или JOIN для обращения к нему.
Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).from("posts_with_tags AS posts")
# => ActiveRecord::Relation
# WITH posts_with_tags AS (
# SELECT * FROM posts WHERE (tags_count > 0)
# )
# SELECT * FROM posts_with_tags AS posts
Post.with(posts_with_tags: Post.where("tags_count > ?", 0)).joins("JOIN posts_with_tags ON posts_with_tags.id = posts.id")
# => ActiveRecord::Relation
# WITH posts_with_tags AS (
# SELECT * FROM posts WHERE (tags_count > 0)
# )
# SELECT * FROM posts JOIN posts_with_tags ON posts_with_tags.id = posts.id
Рекомендуется передавать запрос в виде ActiveRecord::Relation. Если это невозможно и вы убедились в безопасности запроса для базы данных, его можно передать как SQL-литерал с помощью Arel.
Post.with(popular_posts: Arel.sql("... complex sql to calculate posts popularity ..."))
Необходимо соблюдать особую осторожность, чтобы избежать уязвимостей, связанных с внедрением SQL-кода. Этот метод нельзя использовать с небезопасными значениями, содержащими неочищенный пользовательский ввод.
Чтобы добавить несколько CTE, передайте несколько пар «ключ-значение»
Post.with(
posts_with_comments: Post.where("comments_count > ?", 0),
posts_with_tags: Post.where("tags_count > ?", 0)
)
или объедините в цепочку несколько вызовов .with
Post
.with(posts_with_comments: Post.where("comments_count > ?", 0))
.with(posts_with_tags: Post.where("tags_count > ?", 0))
# File activerecord/lib/active_record/relation/query_methods.rb, line 518 def with_recursive(*args) check_if_method_has_arguments!(__callee__, args) spawn.with_recursive!(*args) end
Добавляет рекурсивное общее табличное выражение (CTE), на которое затем можно ссылаться в другом операторе SELECT.
Post.with_recursive(post_and_replies: [Post.where(id: 42), Post.joins('JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id')])
# => ActiveRecord::Relation
# WITH RECURSIVE post_and_replies AS (
# (SELECT * FROM posts WHERE id = 42)
# UNION ALL
# (SELECT * FROM posts JOIN post_and_replies ON posts.in_reply_to_id = post_and_replies.id)
# )
# SELECT * FROM posts
Дополнительные сведения см. в разделе «#with».
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.