Preloads on instance dependent associations for CI partitions
Rails associations accept a block in their definition to pass arguments from one record to the other:
has_one :metadata, -> (build) { where(partition_id: build.partition_id) },
class_name: 'Ci::BuildMetadata',
foreign_key: :build_id,
inverse_of: :build,
autosave: true
This works for creating records, accessing them one by one, but it doesn't work for preloading them:
ArgumentError: The association scope 'metadata' is instance dependent (the
scope block takes an argument). Preloading instance dependent scopes is not
supported.
In Rails 7 it is possible to preload associations that use scopes:
require "bundler/inline"
gemfile(true) do
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
gem 'rails', '~> 7.0', '>= 7.0.4'
gem "sqlite3"
end
require "active_record"
require "logger"
ActiveRecord::Base.establish_connection(adapter: "sqlite3", database: ":memory:")
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
create_table :ci_builds, force: true do |t|
t.integer :partition_id, default: 100
end
create_table :ci_builds_metadata, force: true do |t|
t.integer :build_id
t.integer :partition_id
end
end
class Build < ActiveRecord::Base
self.table_name = :ci_builds
has_one :metadata, -> (metadatable) { where(partition_id: metadatable.partition_id) },
class_name: 'BuildMetadata',
foreign_key: :build_id,
inverse_of: :build,
autosave: true
end
class BuildMetadata < ActiveRecord::Base
self.table_name = :ci_builds_metadata
belongs_to :build, class_name: 'Build'
end
[100, 200, 300].each do |partition_id|
5.times do
job = Build.create!(partition_id: partition_id)
metadata = BuildMetadata.create!(build: job, partition_id: partition_id)
end
end
Build.preload(:metadata).to_a
This ends up creating one query for each partition_id
value:
D, [2022-10-11T15:31:09.176755 #68489] DEBUG -- : Build Load (0.0ms) SELECT "ci_builds".* FROM "ci_builds"
D, [2022-10-11T15:31:09.182337 #68489] DEBUG -- : BuildMetadata Load (0.4ms) SELECT "ci_builds_metadata".* FROM "ci_builds_metadata" WHERE "ci_builds_metadata"."partition_id" = ? AND "ci_builds_metadata"."build_id" IN (?, ?, ?, ?, ?) [["partition_id", 100], ["build_id", 1], ["build_id", 2], ["build_id", 3], ["build_id", 4], ["build_id", 5]]
D, [2022-10-11T15:31:09.182846 #68489] DEBUG -- : BuildMetadata Load (0.1ms) SELECT "ci_builds_metadata".* FROM "ci_builds_metadata" WHERE "ci_builds_metadata"."partition_id" = ? AND "ci_builds_metadata"."build_id" IN (?, ?, ?, ?, ?) [["partition_id", 200], ["build_id", 6], ["build_id", 7], ["build_id", 8], ["build_id", 9], ["build_id", 10]]
D, [2022-10-11T15:31:09.183113 #68489] DEBUG -- : BuildMetadata Load (0.0ms) SELECT "ci_builds_metadata".* FROM "ci_builds_metadata" WHERE "ci_builds_metadata"."partition_id" = ? AND "ci_builds_metadata"."build_id" IN (?, ?, ?, ?, ?) [["partition_id", 300], ["build_id", 11], ["build_id", 12], ["build_id", 13], ["build_id", 14], ["build_id", 15]]
Should we backport this feature into Rails 6?
Edited by Marius Bobin