класс ActiveRecord::Relation
Активное соотношение записей
Константы
- CLAUSE_METHODS
- НЕДОПУСТИМЫЕ_МЕТОДЫ_ДЛЯ_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
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
@delegate_to_klass = false
end Общедоступные методы экземпляров
# File activerecord/lib/active_record/relation.rb, line 681
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 277 def any? return super if block_given? !empty? end
Возвращает true, если есть записи.
# File activerecord/lib/active_record/relation.rb, line 697 def blank? records.blank? end
Возвращает true, если отношение пустое.
# File activerecord/lib/active_record/relation.rb, line 311
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 338
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 = _deprecated_scope_block("create", &block)
scoping { klass.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 = _deprecated_scope_block("create!", &block)
scoping { klass.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 554
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
if eager_loading?
relation = apply_join_dependency
return relation.delete_all
end
stmt = Arel::DeleteManager.new
stmt.from(arel.join_sources.empty? ? table : arel.source)
stmt.key = arel_attribute(primary_key)
stmt.take(arel.limit)
stmt.offset(arel.offset)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
reset
affected
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 604 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 532
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 591 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 666
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 265 def empty? return @records.empty? if loaded? !exists? end
Возвращает true, если записей нет.
# File activerecord/lib/active_record/relation.rb, line 255 def encode_with(coder) coder.represent_seq(nil, records) end
Сериализует объекты связи Массив.
# 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 # different 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 37 def initialize_copy(other) @values = @values.dup reset end
# File activerecord/lib/active_record/relation.rb, line 705
def inspect
subject = loaded? ? records : self
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 676 def joined_includes_values includes_values & joins_values end
Соединения, которые также помечены для предварительной загрузки. В этом случае мы должны просто выполнить жадную загрузку. Обратите внимание, что это простое реализация, потому что у нас могут быть строки и символы, которые представляют одну и ту же ассоциацию, но не совпадают с этой реализацией. Также у нас могут быть вложенные массивы, которые частично совпадают, например, { a: :b } & { a: [:b, :c] }
# File activerecord/lib/active_record/relation.rb, line 614 def load(&block) exec_queries(&block) unless loaded? self end
Заставляет загрузить записи из базы данных, если они ещё не загружены. Вы можете использовать это, если по какой-то причине вам нужно явно загрузить некоторые записи перед фактическим использованием.
Значение, возвращаемое этой функцией, - это само отношение, а не сами записи.
Post.where(published: true).load # => #<ActiveRecord::Relation>
# File activerecord/lib/active_record/relation.rb, line 289 def many? return super if block_given? limit_value ? records.many? : size > 1 end
Возвращает true, если записей больше одной.
# File activerecord/lib/active_record/relation.rb, line 69
def new(attributes = nil, &block)
block = _deprecated_scope_block("new", &block)
scoping { klass.new(attributes, &block) }
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 271 def none? return super if block_given? empty? end
Возвращает true, если записей нет.
# File activerecord/lib/active_record/relation.rb, line 283 def one? return super if block_given? limit_value ? records.one? : size == 1 end
Возвращает true, если ровно одна запись.
# File activerecord/lib/active_record/relation.rb, line 692 def pretty_print(q) q.pp(records) end
# File activerecord/lib/active_record/relation.rb, line 621 def reload reset load end
Принудительно перезагружает отношение.
# File activerecord/lib/active_record/relation.rb, line 626
def reset
@delegate_to_klass = false
@_deprecated_scope_source = nil
@to_sql = @arel = @loaded = @should_eager_load = nil
@records = [].freeze
@offsets = {}
self
end # File activerecord/lib/active_record/relation.rb, line 661 def scope_for_create where_values_hash.merge!(create_with_value.stringify_keys) end
# File activerecord/lib/active_record/relation.rb, line 397
def scoping
already_in_scope? ? yield : _scoping(self) { yield }
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
Обратитесь к unscoped, если вы хотите удалить все предыдущие контексты (включая default_scope) во время выполнения блока.
# File activerecord/lib/active_record/relation.rb, line 260 def size loaded? ? @records.length : count(:all) end
Возвращает размер записей.
# File activerecord/lib/active_record/relation.rb, line 244 def to_ary records.dup end
Преобразует объекты отношения в Массив.
# File activerecord/lib/active_record/relation.rb, line 639
def to_sql
@to_sql ||= begin
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
end Возвращает SQL-запрос для отношения.
User.where(name: 'Oscar').to_sql # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
# File activerecord/lib/active_record/relation.rb, line 512 def touch_all(*names, time: nil) update_all klass.touch_attributes_with_time(*names, time: time) end
Обновляет все записи в текущем отношении без предварительного создания записей с атрибутами updated_at/updated_on, установленных на текущее время или указанное время. Этот метод может принимать имена атрибутов и необязательный аргумент времени. Если передаются имена атрибутов, они обновляются вместе с атрибутами 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 432
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
if eager_loading?
relation = apply_join_dependency
return relation.update_all(updates)
end
stmt = Arel::UpdateManager.new
stmt.table(arel.join_sources.empty? ? table : arel.source)
stmt.key = arel_attribute(primary_key)
stmt.take(arel.limit)
stmt.offset(arel.offset)
stmt.order(*arel.orders)
stmt.wheres = arel.constraints
if updates.is_a?(Hash)
if klass.locking_enabled? &&
!updates.key?(klass.locking_column) &&
!updates.key?(klass.locking_column.to_sym)
attr = arel_attribute(klass.locking_column)
updates[attr.name] = _increment_attribute(attr)
end
stmt.set _substitute_values(updates)
else
stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name))
end
@klass.connection.update stmt, "#{@klass} Update All"
end Обновляет все записи в текущем отношении с указанными данными. Этот метод строит один SQL-запрос UPDATE и отправляет его непосредственно в базу данных. Он не создаёт связанные модели и не вызывает обратные вызовы или валидации Active Record. Тем не менее, значения, переданные в update_all, все равно будут проходить через стандартную типизацию и сериализацию Active Record.
Примечание: Поскольку обратные вызовы Active Record не вызываются, этот метод не будет автоматически обновлять столбцы updated_at/updated_on.
Параметры
-
updates- Строка, массив или хеш, представляющие часть SET SQL-запроса.
Примеры
# 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 701 def values @values.dup end
# File activerecord/lib/active_record/relation.rb, line 657 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 742 def load_records(records) @records = records.freeze @loaded = true end
© 2004–2019 David Heinemeier Hansson
Licensed under the MIT License.