troelskn
troelskn

Reputation: 117615

Reuse has_many association in has_one

I have (simplified) a data model, that looks roughly like the following. E.g. a key-value table called "targets" that is used by "tasks".

class Target < ActiveRecord::Base
  belongs_to :task
end

class Task < ActiveRecord::Base
  has_one :name_target, -> { where(name: "name") },
    class_name: "Target"
  has_one :version_target, -> { where(name: "version") },
    class_name: "Target"
end

For performance reasons, I would like to avoid making individual requests for each relation. Currently, the following code will trigger 3 individual SQL-requests:

t = Task.find(1)       # SELECT * FROM tasks WHERE id = $1
t.name_target.value    # SELECT * FROM targets WHERE task_id = $1 AND name = "name"
t.version_target.value # SELECT * FROM targets WHERE task_id = $1 AND name = "version"

What I would like to do, is preload ALL targets and then have AR reuse this for the has_one relations. So, reducing the number of queries from 3 to 2.

Something like this:

class Task < ActiveRecord::Base
  has_many :targets
  has_one :name_target, -> { where(name: "name") },
    class_name: "Target"
  has_one :version_target, -> { where(name: "version") },
    class_name: "Target"
end

t = Task.includes(:targets).find(1) # SELECT * FROM tasks WHERE id = $1
                                    # SELECT * FROM targets WHERE task_id = $1
t.name_target.value                 # use preload
t.version_target.value              # use preload

Except that doesn't work.

Any ideas how I could achieve what I want?

Upvotes: 1

Views: 43

Answers (1)

max
max

Reputation: 102368

What I would like to do, is preload ALL targets and then have AR reuse this for the has_one relations. So, reducing the number of queries from 3 to 2.

You can't really do that with associations as ActiveRecord has no concept of different associations being linked to each other in that way. You could use enumerable to iterate through the loaded association:

Task.eager_load(:targets).find(1).targets.detect { |t| t.name == "name" }

But you're kind of approaching the problem wrong from the get go. To create one-to-one associations you want instead to add name_target_id and version_target_id foreign key columns to the tasks table.

class AddTargetsToTasks < ActiveRecord::Migration[5.2]
  def change
    add_reference :tasks, :name_target, foreign_key: { to_table: :targets }
    add_reference :tasks, :version_target, foreign_key: { to_table: :targets }
  end
end

And use belongs_to.

class Task < ActiveRecord::Base
  has_many :targets
  belongs_to :name_target, class_name: "Target"
  belongs_to :version_target, class_name: "Target"
end

You then need to load each association to avoid further queries:

Task.eager_load(:targets, :name_target, :version_target)

Upvotes: 1

Related Questions