0

Say I have 3 models:

ModelA has many ModelB ModelB has many ModelC

I'm querying ModelA, but in ModelC I have multiple ones of the same type, let's say I have 3 but I only need the most recently one.

I tried to do something like this...

records = ModelA.where(some query).includes ModelB includes ModelC

// convert activerecord collection to array
records = records.to_a

records.each do |record|

  record.modelBs.each do |modelB|
    filter the modelCs i don't need

    modelB.modelCs = filteredModelCs

  end

end

return records

but instead of merely returning the array of records, an UPDATE sql query is run and the db records are modified. this is a surprise because i never used the .save method and i thought i had converted the collection from an active record collection to an array

How can I filter deeply nested records without modifying the db records? then i can return the filtered result

leonel
  • 10,106
  • 21
  • 85
  • 129
  • You are doing some sort of assignment with that `=` operator. Can you show what is getting updated? The question is very thin on details because of `filter....` we have no idea what you are actually doing. Also why are you bothering to turn it into an array? – Beartech Dec 30 '22 at 03:48
  • It looks like you should rethink your query. Why not filter in your original query? – Beartech Dec 30 '22 at 03:49
  • When you do `modelB.modelCs` you are getting back an `ActiveRecord_Associations_CollectionProxy` and then you are trying to assign something to that. At a minimum you should be putting those into some other array if you want to store them. – Beartech Dec 30 '22 at 04:12
  • 1
    Assigning a list of instances to a `has_many` collection with `=` will immediately [persist the changes to the database](https://guides.rubyonrails.org/association_basics.html#methods-added-by-has-many-collection-objects). – spickermann Dec 30 '22 at 06:10

2 Answers2

1

Assigning a list of instances to a has_many collection with = will immediately persist the changes to the database.

Instead, I would try to solve this with more specific associations like this:

class A
  has_many :bs
  has_many(:cs, through: :bs)
  has_one :recent_c, -> { order(created_at: :desc).limit(1) }, source: :cs

class B
  has_many :cs

With those associations, I would expect the following to work:

as = A.where(some query).includes(:recent_c)

as.each do |a|
  a.recent_c # returns the most recent c for this a
end
spickermann
  • 100,941
  • 9
  • 101
  • 131
  • Great answer. Can you give some reference on `has_many(:cs, through: :bs) do`? Can you really have the `do` with no `end`? Are you passing it the `has_one :recent_c...`? – Beartech Dec 30 '22 at 18:19
  • @spickermann this might not actually work the way you want to. If you do for example `A.eager_load(:recent_c).all` you might be suprised when you only get one A. Thats because limit applies to the entire query and not just the assocation. – max Dec 30 '22 at 18:44
  • There is no easy answer if you have to be able to load only the newest record - the closest I have gotten is adding a extra `belongs_to :latest_c, class_name: 'C'` assocation which is updated with a callback or DB trigger. Otherwise you have to do a window query, lateral join or something else to just get the latest record and then push that into a model. – max Dec 30 '22 at 18:48
  • See https://stackoverflow.com/a/60554931/544825 and https://stackoverflow.com/a/60568914/544825 for examples. – max Dec 30 '22 at 18:50
0

If I got you right, you want to get a collection of latest Cs, which are connected to Bs, which are connected to certain A-relation? If so, you can do something like that (considering you have tables as, bs and cs):

class A < ApplicationRecord
  has_many :bs
end

class B < ApplicationRecord
  belongs_to :a

  has_many :cs
end

class C < ApplicationRecord
  belongs_to :b

  scope :recent_for_bs, -> { joins(
    <<-sql
      INNER JOIN (SELECT b_id, MAX(id) AS max_id FROM cs GROUP BY b_id) recent_cs
      ON cs.b_id = recent_cs.b_id AND cs.id = recent_cs.max_id
    sql
  ) }
end

And then you would query Cs like that:

C.recent_for_bs.joins(b: :a).merge(A.where(some_query))

You get recent Cs, inner join them with Bs and As and then get records connected to your A-relation by merging it.

metacorn
  • 52
  • 3