класс ActiveRecord::Relation
Отношение Active Record
Константы
- CLAUSE_METHODS
- INVALID_METHODS_FOR_DELETE_ALL
- MULTI_VALUE_METHODS
- SINGLE_VALUE_METHODS
- VALUE_METHODS
Атрибуты
Публичные методы класса
# File activerecord/lib/active_record/relation.rb, line 27
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
@klass = klass
@table = table
@values = values
@loaded = false
@predicate_builder = predicate_builder
@delegate_to_klass = false
@future_result = nil
@records = nil
end Общедоступные методы экземпляров
# File activerecord/lib/active_record/relation.rb, line 765
def ==(other)
case other
when Associations::CollectionProxy, AssociationRelation
self == other.records
when Relation
other.to_sql == to_sql
when Array
records == other
end
end Сравнивает два отношения на равенство.
# File activerecord/lib/active_record/relation.rb, line 284 def any? return super if block_given? !empty? end
Возвращает true, если есть какие-либо записи.
# File activerecord/lib/active_record/relation.rb, line 781 def blank? records.blank? end
Возвращает true, если отношение пустое.
# File activerecord/lib/active_record/relation.rb, line 320
def cache_key(timestamp_column = "updated_at")
@cache_keys ||= {}
@cache_keys[timestamp_column] ||= klass.collection_cache_key(self, timestamp_column)
end Возвращает стабильный ключ кэша, который можно использовать для идентификации этого запроса. Ключ кэша создается с отпечатком SQL-запроса.
Product.where("name like ?", "%Cosmic Encounter%").cache_key
# => "products/query-1850ab3d302391b85b8693e941286659"
Если ActiveRecord::Base.collection_cache_versioning выключен, как это было в Rails 6.0 и ранее, ключ кэша также будет содержать версию.
ActiveRecord::Base.collection_cache_versioning = false
Product.where("name like ?", "%Cosmic Encounter%").cache_key
# => "products/query-1850ab3d302391b85b8693e941286659-1-20150714212553907087000"
Также можно передать пользовательский столбец со временем, чтобы получить метку времени последней обновлённой записи.
Product.where("name like ?", "%Game%").cache_key(:last_reviewed_at)
# File activerecord/lib/active_record/relation.rb, line 399
def cache_key_with_version
if version = cache_version
"#{cache_key}-#{version}"
else
cache_key
end
end Возвращает ключ кэша вместе с версией.
# File activerecord/lib/active_record/relation.rb, line 347
def cache_version(timestamp_column = :updated_at)
if collection_cache_versioning
@cache_versions ||= {}
@cache_versions[timestamp_column] ||= compute_cache_version(timestamp_column)
end
end Возвращает версию кэша, которая может быть использована вместе с ключом кэша для формирования схемы рециркуляции кэша. Версия кэша создаётся с числом записей, соответствующих запросу, и меткой времени последней обновлённой записи. Когда новая запись соответствует запросу, или любая из существующих записей обновляется или удаляется, версия кэша меняется.
Если коллекция загружена, метод будет перебирать записи для генерации метки времени, в противном случае он выполнит один SQL-запрос, например:
SELECT COUNT(*), MAX("products"."updated_at") FROM "products" WHERE (name like '%Cosmic Encounter%') # File activerecord/lib/active_record/relation.rb, line 95
def create(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create(attr, &block) }
else
block = current_scope_restoring_block(&block)
scoping { _create(attributes, &block) }
end
end Пытается создать новую запись с теми же атрибутами области, которые определены в отношении. Возвращает инициализированный объект, если проверка валидации завершается ошибкой.
Ожидает аргументы в том же формате, что и ActiveRecord::Base.create.
Примеры
users = User.where(name: 'Oscar')
users.create # => #<User id: 3, name: "Oscar", ...>
users.create(name: 'fxn')
users.create # => #<User id: 4, name: "fxn", ...>
users.create { |user| user.name = 'tenderlove' }
# => #<User id: 5, name: "tenderlove", ...>
users.create(name: nil) # validation on name
# => #<User id: nil, name: nil, ...>
# File activerecord/lib/active_record/relation.rb, line 110
def create!(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| create!(attr, &block) }
else
block = current_scope_restoring_block(&block)
scoping { _create!(attributes, &block) }
end
end Аналогично create, но вызывает create! в базовом классе. Вызывает исключение, если возникает ошибка валидации.
Ожидает аргументы в том же формате, что и ActiveRecord::Base.create!.
# File activerecord/lib/active_record/relation.rb, line 209
def create_or_find_by(attributes, &block)
transaction(requires_new: true) { create(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
find_by!(attributes)
end Пытается создать запись с заданными атрибутами в таблице, которая имеет уникальное ограничение базы данных на одном или нескольких столбцах. Если строка уже существует с одним или несколькими из этих уникальных ограничений, исключение, которое обычно вызывает такая вставка, перехватывается, и существующая запись с этими атрибутами находится с помощью find_by!.
Это аналогично find_or_create_by, но избегает проблемы устаревших чтений между SELECT и INSERT, поскольку этому методу необходимо сначала запросить таблицу, а затем попытаться вставить строку, если она не найдена.
Однако существует несколько недостатков для create_or_find_by:
-
Базовая таблица должна иметь соответствующие столбцы, определённые с уникальными ограничениями базы данных.
-
Нарушение уникального ограничения может быть вызвано только одним или хотя бы меньшим числом заданных атрибутов. Это означает, что последующий find_by! может не найти соответствующую запись, что затем вызовет исключение
ActiveRecord::RecordNotFoundвместо записи с заданными атрибутами. -
Хотя мы избегаем гонки между SELECT -> INSERT из
find_or_create_by, на самом деле у нас есть другая гонка между INSERT -> SELECT, которая может быть вызвана, если другой клиент выполняет DELETE между этими двумя операторами. Но для большинства приложений это значительно менее вероятный сценарий. -
Он полагается на обработку исключений для управления потоком управления, что может быть немного медленнее.
-
Первичный ключ может автоматически инкрементироваться при каждом создании, даже если он завершается неудачей. Это может ускорить проблему исчерпания целых чисел, если базовая таблица всё ещё использует первичный ключ типа int (примечание: все приложения Rails начиная с 5.1+ по умолчанию используют bigint, что не подвержено этой проблеме).
Этот метод вернёт запись, если все заданные атрибуты покрыты уникальными ограничениями (если не вызвана гонка INSERT -> DELETE -> SELECT), но если попытка создания завершилась неудачей из-за ошибок валидации, она не будет сохранена, вы получите то, что create возвращает в такой ситуации.
# File activerecord/lib/active_record/relation.rb, line 218
def create_or_find_by!(attributes, &block)
transaction(requires_new: true) { create!(attributes, &block) }
rescue ActiveRecord::RecordNotUnique
find_by!(attributes)
end Как create_or_find_by, но вызывает create!, поэтому при ошибке созданной записи выбрасывается исключение.
# File activerecord/lib/active_record/relation.rb, line 601
def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
value = @values[method]
method == :distinct ? value : value&.any?
end
if invalid_methods.any?
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
arel = eager_loading? ? apply_join_dependency.arel : build_arel
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
stmt = arel.compile_delete(table[primary_key], having_clause_ast, group_values_arel_columns)
klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
end Удаляет записи, не инициализируя записи предварительно, и, следовательно, не вызывая метод #destroy и не вызывая обратные вызовы. Это единственное SQL-утверждение DELETE, которое отправляется непосредственно в базу данных, намного эффективнее, чем destroy_all. Однако будьте осторожны с отношениями, в частности :dependent правила, определённые для ассоциаций, не соблюдаются. Возвращает количество изменённых строк.
Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
Оба вызова удаляют затронутые посты сразу с помощью одного утверждения DELETE. Если вам нужно уничтожить зависимые ассоциации или вызвать ваши before_* или after_destroy обратные вызовы, используйте метод destroy_all вместо этого.
Если предоставлен неверный метод, delete_all вызывает ActiveRecordError:
Post.distinct.delete_all # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
# File activerecord/lib/active_record/relation.rb, line 642 def delete_by(*args) where(*args).delete_all end
Находит и удаляет все записи, соответствующие указанным условиям. Это сокращённая форма relation.where(condition).delete_all. Возвращает количество изменённых строк.
Если запись не найдена, возвращает 0 , так как строк не было изменено.
Person.delete_by(id: 13)
Person.delete_by(name: 'Spartacus', rating: 4)
Person.delete_by("published_at < ?", 2.weeks.ago)
# File activerecord/lib/active_record/relation.rb, line 579
def destroy_all
records.each(&:destroy).tap { reset }
end Уничтожает записи, создавая экземпляр каждой записи и вызывая её метод #destroy. Выполняются обратные вызовы каждого объекта (включая параметры ассоциации :dependent). Возвращает коллекцию уничтоженных объектов; каждый будет заморожен, чтобы отразить, что изменения не должны быть внесены (так как они не могут быть сохранены).
Примечание: Создание экземпляра, выполнение обратных вызовов и удаление каждой записи может занимать много времени, когда вы удаляете много записей одновременно. Это генерирует как минимум один запрос SQL DELETE на запись (или, возможно, больше, чтобы обеспечить выполнение ваших обратных вызовов). Если вы хотите быстро удалить много строк, не беспокоясь об их ассоциациях или обратных вызовах, используйте delete_all вместо этого.
Примеры
Person.where(age: 0..18).destroy_all
# File activerecord/lib/active_record/relation.rb, line 629 def destroy_by(*args) where(*args).destroy_all end
Находит и уничтожает все записи, соответствующие указанным условиям. Это сокращение для relation.where(condition).destroy_all. Возвращает коллекцию уничтоженных объектов.
Если запись не найдена, возвращает пустой массив.
Person.destroy_by(id: 13)
Person.destroy_by(name: 'Spartacus', rating: 4)
Person.destroy_by("published_at < ?", 2.weeks.ago)
# File activerecord/lib/active_record/relation.rb, line 750
def eager_loading?
@should_eager_load ||=
eager_load_values.any? ||
includes_values.any? && (joined_includes_values.any? || references_eager_loaded_tables?)
end Возвращает true, если отношение нуждается в нетерпеливой загрузке.
# File activerecord/lib/active_record/relation.rb, line 269
def empty?
if loaded?
records.empty?
else
!exists?
end
end Возвращает true, если записей нет.
# File activerecord/lib/active_record/relation.rb, line 255 def encode_with(coder) coder.represent_seq(nil, records) end
Сериализует объекты отношения Array.
# File activerecord/lib/active_record/relation.rb, line 239
def explain
exec_explain(collecting_queries_for_explain { exec_queries })
end Выполняет EXPLAIN для запроса или запросов, запущенных этим отношением, и возвращает результат в виде строки. Строка отформатирована, имитируя те, которые выводятся оболочкой базы данных.
Обратите внимание, что этот метод фактически выполняет запросы, поскольку результаты некоторых из них необходимы для следующих, когда происходит нетерпеливая загрузка.
Дополнительные сведения см. в руководстве по интерфейсу запросов Active Record.
# File activerecord/lib/active_record/relation.rb, line 168 def find_or_create_by(attributes, &block) find_by(attributes) || create(attributes, &block) end
Находит первую запись с заданными атрибутами или создает запись с атрибутами, если запись не найдена:
# Find the first user named "Penélope" or create a new one. User.find_or_create_by(first_name: 'Penélope') # => #<User id: 1, first_name: "Penélope", last_name: nil> # Find the first user named "Penélope" or create a new one. # We already have one so the existing record will be returned. User.find_or_create_by(first_name: 'Penélope') # => #<User id: 1, first_name: "Penélope", last_name: nil> # Find the first user named "Scarlett" or create a new one with # a particular last name. User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett') # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
Этот метод принимает блок, который передается в create. Последний пример выше можно написать по-другому:
# Find the first user named "Scarlett" or create a new one with a # particular last name. User.find_or_create_by(first_name: 'Scarlett') do |user| user.last_name = 'Johansson' end # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
Этот метод всегда возвращает запись, но если создание было предпринято и завершилось неудачей из-за ошибок проверки, оно не будет сохранено, вы получите то, что возвращает create в такой ситуации.
Обратите внимание, что этот метод не является атомарным, он сначала выполняет SELECT, а если результатов нет, предпринимается попытка INSERT. Если есть другие потоки или процессы, возникает условие гонки между обоими вызовами, и может случиться так, что вы получите две похожие записи.
Если это может быть проблемой для вашего приложения, см. create_or_find_by.
# File activerecord/lib/active_record/relation.rb, line 175 def find_or_create_by!(attributes, &block) find_by(attributes) || create!(attributes, &block) end
Как find_or_create_by, но вызывает create!, поэтому исключение возникает, если созданная запись недействительна.
# File activerecord/lib/active_record/relation.rb, line 226 def find_or_initialize_by(attributes, &block) find_by(attributes) || new(attributes, &block) end
Как find_or_create_by, но вызывает new вместо create.
# File activerecord/lib/active_record/relation.rb, line 38 def initialize_copy(other) @values = @values.dup reset end
# File activerecord/lib/active_record/relation.rb, line 793
def inspect
subject = loaded? ? records : annotate("loading for inspect")
entries = subject.take([limit_value, 11].compact.min).map!(&:inspect)
entries[10] = "..." if entries.size == 11
"#<#{self.class.name} [#{entries.join(', ')}]>"
end # File activerecord/lib/active_record/relation.rb, line 760 def joined_includes_values includes_values & joins_values end
Объединения, которые также помечены для предварительной загрузки. В этом случае мы должны просто выполнить нетерпеливую загрузку. Обратите внимание, что это наивная реализация, потому что у нас могут быть строки и символы, которые представляют одну и ту же ассоциацию, но которые не соответствуют этому. Кроме того, у нас могут быть вложенные хэши, которые частично совпадают, например, { a: :b } & { a: [:b, :c] }
# File activerecord/lib/active_record/relation.rb, line 693
def load(&block)
if !loaded? || scheduled?
@records = exec_queries(&block)
@loaded = true
end
self
end Заставляет записи загружаться из базы данных, если они еще не были загружены. Вы можете использовать это, если по какой-либо причине вам нужно явно загрузить некоторые записи, прежде чем фактически использовать их. Возвращаемое значение — само отношение, а не записи.
Post.where(published: true).load # => #<ActiveRecord::Relation>
# File activerecord/lib/active_record/relation.rb, line 664
def load_async
return load if !connection.async_enabled?
unless loaded?
result = exec_main_query(async: connection.current_transaction.closed?)
if result.is_a?(Array)
@records = result
else
@future_result = result
end
@loaded = true
end
self
end Планирует выполнение запроса из пула фоновых потоков.
Post.where(published: true).load_async # => #<ActiveRecord::Relation>
Когда Relation итерируется, если фоновый запрос еще не выполнен, он будет выполнен фоновым потоком.
Обратите внимание, что config.active_record.async_query_executor должен быть настроен для того, чтобы запросы фактически выполнялись одновременно. В противном случае по умолчанию они выполняются на переднем плане.
load_async также вернется к выполнению на переднем плане в тестовой среде, когда включены транзакционные заглушки.
Если запрос был фактически выполнен в фоновом режиме, журналы Active Record покажут это, добавив префикс к строке журнала с помощью ASYNC:
ASYNC Post Load (0.0ms) (db time 2ms) SELECT "posts".* FROM "posts" LIMIT 100
# File activerecord/lib/active_record/relation.rb, line 297 def many? return super if block_given? return records.many? if loaded? limited_count > 1 end
Возвращает true, если записей больше одной.
Enumerable#many? # File activerecord/lib/active_record/relation.rb, line 66
def new(attributes = nil, &block)
if attributes.is_a?(Array)
attributes.collect { |attr| new(attr, &block) }
else
block = current_scope_restoring_block(&block)
scoping { _new(attributes, &block) }
end
end Инициализирует новую запись из отношения, сохраняя текущую область видимости.
Ожидает аргументы в том же формате, что и ActiveRecord::Base.new.
users = User.where(name: 'DHH') user = users.new # => #<User id: nil, name: "DHH", created_at: nil, updated_at: nil>
Вы также можете передать блок в new с новой записью в качестве аргумента:
user = users.new { |user| user.name = 'Oscar' }
user.name # => Oscar
# File activerecord/lib/active_record/relation.rb, line 278 def none? return super if block_given? empty? end
Возвращает true, если записей нет.
# File activerecord/lib/active_record/relation.rb, line 290 def one? return super if block_given? return records.one? if loaded? limited_count == 1 end
Возвращает true, если существует ровно одна запись.
# File activerecord/lib/active_record/relation.rb, line 776 def pretty_print(q) q.pp(records) end
# File activerecord/lib/active_record/relation.rb, line 703 def reload reset load end
Принудительно перезагружает отношение.
# File activerecord/lib/active_record/relation.rb, line 708 def reset @future_result&.cancel @future_result = nil @delegate_to_klass = false @to_sql = @arel = @loaded = @should_eager_load = nil @offsets = @take = nil @cache_keys = nil @records = nil self end
# File activerecord/lib/active_record/relation.rb, line 683 def scheduled? !!@future_result end
Возвращает true , если отношение было запланировано в пуле потоков фонового выполнения.
# File activerecord/lib/active_record/relation.rb, line 743
def scope_for_create
hash = where_clause.to_h(klass.table_name, equality_only: true)
create_with_value.each { |k, v| hash[k.to_s] = v } unless create_with_value.empty?
hash
end # File activerecord/lib/active_record/relation.rb, line 421
def scoping(all_queries: nil, &block)
registry = klass.scope_registry
if global_scope?(registry) && all_queries == false
raise ArgumentError, "Scoping is set to apply to all queries and cannot be unset in a nested block."
elsif already_in_scope?(registry)
yield
else
_scoping(self, registry, all_queries, &block)
end
end Ограничивает все запросы текущим ограничением.
Comment.where(post_id: 1).scoping do Comment.first end # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
Если передано all_queries: true, ограничение будет применено ко всем запросам для отношения, включая update и delete для экземпляров. После того, как all_queries установлено в true, его нельзя установить в false вложенном блоке.
Проверьте unscoped, если вы хотите удалить все предыдущие ограничения (включая default_scope) во время выполнения блока.
# File activerecord/lib/active_record/relation.rb, line 260
def size
if loaded?
records.length
else
count(:all)
end
end Возвращает размер записей.
# File activerecord/lib/active_record/relation.rb, line 244 def to_ary records.dup end
Преобразует объекты отношения в Array.
# File activerecord/lib/active_record/relation.rb, line 723
def to_sql
@to_sql ||= if eager_loading?
apply_join_dependency do |relation, join_dependency|
relation = join_dependency.apply_column_aliases(relation)
relation.to_sql
end
else
conn = klass.connection
conn.unprepared_statement { conn.to_sql(arel) }
end
end Возвращает SQL запрос для отношения.
User.where(name: 'Oscar').to_sql # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
# File activerecord/lib/active_record/relation.rb, line 559 def touch_all(*names, time: nil) update_all klass.touch_attributes_with_time(*names, time: time) end
Обновляет все записи в текущем отношении, устанавливая атрибуты updated_at/updated_on на текущее время или указанное время. Не создаёт связанные модели, не запускает обратные вызовы или валидацию Active Record. Данный метод может принимать имена атрибутов и необязательное время. Если переданы имена атрибутов, они обновляются вместе с атрибутами updated_at/updated_on. Если время не указано, используется текущее время.
Примеры
# Touch all records Person.all.touch_all # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'" # Touch multiple records with a custom attribute Person.all.touch_all(:created_at) # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'" # Touch multiple records with a specified time Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0)) # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'" # Touch records with scope Person.where(name: 'David').touch_all # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
# File activerecord/lib/active_record/relation.rb, line 464
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
if updates.is_a?(Hash)
if klass.locking_enabled? &&
!updates.key?(klass.locking_column) &&
!updates.key?(klass.locking_column.to_sym)
attr = table[klass.locking_column]
updates[attr.name] = _increment_attribute(attr)
end
values = _substitute_values(updates)
else
values = Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
end
arel = eager_loading? ? apply_join_dependency.arel : build_arel
arel.source.left = table
group_values_arel_columns = arel_columns(group_values.uniq)
having_clause_ast = having_clause.ast unless having_clause.empty?
stmt = arel.compile_update(values, table[primary_key], having_clause_ast, group_values_arel_columns)
klass.connection.update(stmt, "#{klass} Update All").tap { reset }
end Обновляет все записи в текущем отношении с использованием предоставленных данных. Этот метод создаёт один SQL запрос UPDATE и отправляет его напрямую в базу данных. Не создаёт связанные модели и не вызывает обратные вызовы или валидацию Active Record. Однако значения, переданные в update_all, всё ещё будут проходить стандартное преобразование и сериализацию Active Record. Возвращает количество изменённых строк.
Примечание: Поскольку обратные вызовы Active Record не вызываются, этот метод не будет автоматически обновлять столбцы updated_at/updated_on.
Параметры
-
updates- Строка, массив или хеш, представляющие часть SQL-запроса SET.
Примеры
# Update all customers with the given attributes
Customer.update_all wants_email: true
# Update all books with 'Rails' in their title
Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
# Update all books that match conditions, but limit it to 5 ordered by date
Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
# Update all invoices and set the number column to its id value.
Invoice.update_all('number = id')
# File activerecord/lib/active_record/relation.rb, line 516
def update_counters(counters)
touch = counters.delete(:touch)
updates = {}
counters.each do |counter_name, value|
attr = table[counter_name]
updates[attr.name] = _increment_attribute(attr, value)
end
if touch
names = touch if touch != true
names = Array.wrap(names)
options = names.extract_options!
touch_updates = klass.touch_attributes_with_time(*names, **options)
updates.merge!(touch_updates) unless touch_updates.empty?
end
update_all updates
end Обновляет счётчики записей в текущем отношении.
Параметры
-
counter-Hashсодержащий имена полей для обновления в качестве ключей и величины обновления в качестве значений. -
:touch- параметр - обновлять временные метки столбцов при обновлении. -
Если переданы имена атрибутов, они обновляются вместе с атрибутами update_at/on.
Примеры
# For Posts by a given author increment the comment_count by 1. Post.where(author_id: author.id).update_counters(comment_count: 1)
# File activerecord/lib/active_record/relation.rb, line 785 def values @values.dup end
# File activerecord/lib/active_record/relation.rb, line 739 def where_values_hash(relation_table_name = klass.table_name) where_clause.to_h(relation_table_name) end
Возвращает хеш с условиями where.
User.where(name: 'Oscar').where_values_hash
# => {name: "Oscar"}
Защищённые методы экземпляра
# File activerecord/lib/active_record/relation.rb, line 834 def load_records(records) @records = records.freeze @loaded = true end
© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.