Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@Alexander-Senko
Copy link
Collaborator

@Alexander-Senko Alexander-Senko commented Sep 4, 2024

ActiveRecord::Associations::CollectionProxy#decorate ignores target (be it already loaded or not) and loads the association again every time it's called.

That's why unsaved records get missing from the decorated collection.

Testing

  • it 'returns a decorated collection'

    1. Ensure a record and some records associated with it are persisted.
    2. Get the decorated association: record.associated_records.decorate.
    3. Ensure that it matches record.associated_records.
  • it 'uses cached records'

    1. Ensure a record and some records associated with it are persisted.
    2. Get the association: record.associated_records.
    3. Get the decorated association: record.associated_records.decorate.
    4. Ensure the association was loaded only once.
  • it 'caches records'

    1. Ensure a record and some records associated with it are persisted.
    2. Get the decorated association: record.associated_records.decorate (twice).
    3. Get the association: record.associated_records.
    4. Ensure the association was loaded only once.
  • it 'respects unsaved records'

    1. Ensure a record and some records associated with it are persisted.
    2. build new associated records.
    3. Get the decorated association: record.associated_records.decorate.
    4. Ensure that it matches both record.associated_records and the new ones.

To-Dos

  • tests

References

@Alexander-Senko
Copy link
Collaborator Author

I'm sorry, but the test environment looks like a mess for me 😕 I spent some time figuring out how it's supposed to test Active Record integration — and found nothing looking like a valid example. Was it ever tested? 🤔

So, I've tested it manually. Could we pull this in without a test coverage for now?

@y-yagi
Copy link
Collaborator

y-yagi commented Sep 6, 2024

I'm sorry, but the test environment looks like a mess for me 😕 I spent some time figuring out how it's supposed to test Active Record integration — and found nothing looking like a valid example. Was it ever tested? 🤔

I'm sorry I've misunderstood something, but I think we can add these kinds of specs under the spec/dummy you already did in #929. At least, the following spec works as expected.

diff --git a/spec/dummy/app/decorators/comment_decorator.rb b/spec/dummy/app/decorators/comment_decorator.rb
new file mode 100644
index 0000000..8257739
--- /dev/null
+++ b/spec/dummy/app/decorators/comment_decorator.rb
@@ -0,0 +1,2 @@
+class CommentDecorator < Draper::Decorator
+end
\ No newline at end of file
diff --git a/spec/dummy/app/models/comment.rb b/spec/dummy/app/models/comment.rb
new file mode 100644
index 0000000..8b86c56
--- /dev/null
+++ b/spec/dummy/app/models/comment.rb
@@ -0,0 +1,3 @@
+class Comment < ApplicationRecord
+  belongs_to :post
+end
diff --git a/spec/dummy/app/models/post.rb b/spec/dummy/app/models/post.rb
index 6352fdb..d143128 100644
--- a/spec/dummy/app/models/post.rb
+++ b/spec/dummy/app/models/post.rb
@@ -4,4 +4,6 @@ class Post < ApplicationRecord
   # attr_accessible :title, :body

   broadcasts if defined? Turbo::Broadcastable
+
+  has_many :comments
 end
diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb
index 9aebf2c..8213494 100644
--- a/spec/dummy/db/schema.rb
+++ b/spec/dummy/db/schema.rb
@@ -18,4 +18,9 @@ ActiveRecord::Schema.define(version: 20121019115657) do
     t.datetime "updated_at", null: false
   end

+  create_table "comments", force: true do |t|
+    t.integer "post_id", null: false
+    t.datetime "created_at", null: false
+    t.datetime "updated_at", null: false
+  end
 end
diff --git a/spec/dummy/spec/models/post_spec.rb b/spec/dummy/spec/models/post_spec.rb
index 8bc05a0..c8dc10e 100644
--- a/spec/dummy/spec/models/post_spec.rb
+++ b/spec/dummy/spec/models/post_spec.rb
@@ -18,4 +18,13 @@ RSpec.describe Post do
       }
     end
   end if defined? Turbo::Broadcastable
+
+  describe 'association' do
+    it 'respects unsaved records' do
+      post = Post.create!
+      Comment.create!(post: post)
+      post.comments.build
+      expect(post.comments.decorate.count).to eq 2
+    end
+  end
 end

@Alexander-Senko Alexander-Senko force-pushed the fix/associations/loading branch 2 times, most recently from a9c1da1 to f621594 Compare September 7, 2024 06:24
@Alexander-Senko Alexander-Senko force-pushed the fix/associations/loading branch from f621594 to 4fc079c Compare September 7, 2024 06:35
`ActiveRecord::Associations::CollectionProxy#decorate` ignores `target`
(be it already loaded or not) and loads the association again every time
it's called.

That's why unsaved records get missing from the decorated collection.

Resolves drapergem#646,
         drapergem#706,
         drapergem#827.
Copy link
Collaborator

@y-yagi y-yagi left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@Alexander-Senko Alexander-Senko merged commit c7e37c1 into drapergem:master Sep 13, 2024
@Alexander-Senko Alexander-Senko deleted the fix/associations/loading branch September 13, 2024 05:48
Alexander-Senko added a commit that referenced this pull request Oct 4, 2024
Added support for latest Ruby (upto 3.4) and Rails (upto 8) versions.

## Fixes

* `CollectionDecorator#respond_to?` for non-ORM collections
  (#920)
* Using Draper outside of controller scope
  (#927)
* Decoration of AR associations
  (#932)

## Performance

* Sped up delegation via `delegate_all`
  (#911)
cgunther added a commit to cgunther/draper that referenced this pull request Feb 12, 2025
Similar to drapergem#932 for associations, calling `#decorate` on a relation
loads the records every time, regardless of whether the underlying
relation has already been loaded.

This is because the default of passing `all` as the collection to
decorate ends up being a new relation, so even if it were previously
loaded, the new relation including `all` needs to be loaded.

`ActiveRecord::Associations::CollectionProxy` actually inherits from
`ActiveRecord::Relation`, so by pushing this up to the relation, we can
cover both scenarios.
https://github.com/rails/rails/blob/57c24948eb5cc9e5f9a4cecb6f2060f53e2246e1/activerecord/lib/active_record/associations/collection_proxy.rb#L31

The documentation included in drapergem#932 (`company.products.popular.decorate`)
wasn't correct as you're decorating a relation there (`popular`), not
just the association (`products`).

This now covers the association itself (`company.products.decorate`), as
well as a relation from that association (`company.products.popular.decorate`),
as well as a relation right from the class without associations
(`Product.popular.decorate`).

Decorating the class itself (`Product.decorate`) still calls `all` since
the model class is not a relation itself.
cgunther added a commit to cgunther/draper that referenced this pull request Feb 12, 2025
Similar to drapergem#932 for associations, calling `#decorate` on a relation
loads the records every time, regardless of whether the underlying
relation has already been loaded.

This is because the default of passing `all` as the collection to
decorate ends up being a new relation, so even if it were previously
loaded, the new relation including `all` needs to be loaded.

`ActiveRecord::Associations::CollectionProxy` actually inherits from
`ActiveRecord::Relation`, so by pushing this up to the relation, we can
cover both scenarios.
https://github.com/rails/rails/blob/57c24948eb5cc9e5f9a4cecb6f2060f53e2246e1/activerecord/lib/active_record/associations/collection_proxy.rb#L31

The documentation included in drapergem#932 (`company.products.popular.decorate`)
wasn't correct as you're decorating a relation there (`popular`), not
just the association (`products`).

This now covers the association itself (`company.products.decorate`), as
well as a relation from that association (`company.products.popular.decorate`),
as well as a relation right from the class without associations
(`Product.popular.decorate`).

Decorating the class itself (`Product.decorate`) still calls `all` since
the model class is not a relation itself.
Alexander-Senko pushed a commit to cgunther/draper that referenced this pull request Mar 6, 2025
Similar to drapergem#932 for associations, calling `#decorate` on a relation
loads the records every time, regardless of whether the underlying
relation has already been loaded.

This is because the default of passing `all` as the collection to
decorate ends up being a new relation, so even if it were previously
loaded, the new relation including `all` needs to be loaded.

`ActiveRecord::Associations::CollectionProxy` actually inherits from
`ActiveRecord::Relation`, so by pushing this up to the relation, we can
cover both scenarios.
https://github.com/rails/rails/blob/57c24948eb5cc9e5f9a4cecb6f2060f53e2246e1/activerecord/lib/active_record/associations/collection_proxy.rb#L31

The documentation included in drapergem#932 (`company.products.popular.decorate`)
wasn't correct as you're decorating a relation there (`popular`), not
just the association (`products`).

This now covers the association itself (`company.products.decorate`), as
well as a relation from that association (`company.products.popular.decorate`),
as well as a relation right from the class without associations
(`Product.popular.decorate`).

Decorating the class itself (`Product.decorate`) still calls `all` since
the model class is not a relation itself.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment