aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/CHANGELOG.md26
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb15
-rw-r--r--activerecord/lib/active_record/querying.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb13
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb18
-rw-r--r--activerecord/test/cases/boolean_test.rb9
-rw-r--r--activerecord/test/cases/relations_test.rb7
7 files changed, 73 insertions, 17 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index df781b4f1e..37f31462b3 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,3 +1,19 @@
+* Assign all attributes before calling `build` to ensure the child record is visible in
+ `before_add` and `after_add` callbacks for `has_many :through` associations.
+
+ Fixes #33249.
+
+ *Ryan H. Kerr*
+
+* Add `ActiveRecord::Relation#extract_associated` for extracting associated records from a relation.
+
+ ```
+ account.memberships.extract_associated(:user)
+ # => Returns collection of User records
+ ```
+
+ *DHH*
+
* Add `ActiveRecord::Relation#annotate` for adding SQL comments to its queries.
For example:
@@ -332,7 +348,7 @@
*Gannon McGibbon*
-* Cached columns_hash fields should be excluded from ResultSet#column_types
+* Cached `columns_hash` fields should be excluded from `ResultSet#column_types`.
PR #34528 addresses the inconsistent behaviour when attribute is defined for an ignored column. The following test
was passing for SQLite and MySQL, but failed for PostgreSQL:
@@ -363,12 +379,12 @@
* Make the implicit order column configurable.
- When calling ordered finder methods such as +first+ or +last+ without an
+ When calling ordered finder methods such as `first` or `last` without an
explicit order clause, ActiveRecord sorts records by primary key. This can
result in unpredictable and surprising behaviour when the primary key is
not an auto-incrementing integer, for example when it's a UUID. This change
makes it possible to override the column used for implicit ordering such
- that +first+ and +last+ will return more predictable results.
+ that `first` and `last` will return more predictable results.
Example:
@@ -514,10 +530,10 @@
* Enum raises on invalid definition values
- When defining a Hash enum it can be easy to use [] instead of {}. This
+ When defining a Hash enum it can be easy to use `[]` instead of `{}`. This
commit checks that only valid definition values are provided, those can
be a Hash, an array of Symbols or an array of Strings. Otherwise it
- raises an ArgumentError.
+ raises an `ArgumentError`.
Fixes #33961
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index 84a9797aa5..0d384950fe 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -57,21 +57,14 @@ module ActiveRecord
@through_records[record.object_id] ||= begin
ensure_mutable
- through_record = through_association.build(*options_for_through_record)
- through_record.send("#{source_reflection.name}=", record)
+ attributes = through_scope_attributes
+ attributes[source_reflection.name] = record
+ attributes[source_reflection.foreign_type] = options[:source_type] if options[:source_type]
- if options[:source_type]
- through_record.send("#{source_reflection.foreign_type}=", options[:source_type])
- end
-
- through_record
+ through_association.build(attributes)
end
end
- def options_for_through_record
- [through_scope_attributes]
- end
-
def through_scope_attributes
scope.where_values_hash(through_association.reflection.name.to_s).
except!(through_association.reflection.foreign_key,
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 81ab502824..ae1501f5a1 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -13,7 +13,7 @@ module ActiveRecord
:destroy_all, :delete_all, :update_all, :destroy_by, :delete_by,
:find_each, :find_in_batches, :in_batches,
:select, :reselect, :order, :reorder, :group, :limit, :offset, :joins, :left_joins, :left_outer_joins,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
+ :where, :rewhere, :preload, :extract_associated, :eager_load, :includes, :from, :lock, :readonly, :extending, :or,
:having, :create_with, :distinct, :references, :none, :unscope, :optimizer_hints, :merge, :except, :only,
:count, :average, :minimum, :maximum, :sum, :calculate, :annotate,
:pluck, :pick, :ids
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 6f0f2125dc..c37855172b 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -154,6 +154,19 @@ module ActiveRecord
self
end
+ # Extracts a named +association+ from the relation. The named association is first preloaded,
+ # then the individual association records are collected from the relation. Like so:
+ #
+ # account.memberships.extract_associated(:user)
+ # # => Returns collection of User records
+ #
+ # This is short-hand for:
+ #
+ # account.memberships.preload(:user).collect(&:user)
+ def extract_associated(association)
+ preload(association).collect(&association)
+ end
+
# Use to indicate that the given +table_names+ are referenced by an SQL string,
# and should therefore be JOINed in any query rather than loaded separately.
# This method only works in conjunction with #includes.
diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb
index 67e013c6e0..fa540c9dab 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -1474,6 +1474,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [subscription2], post.subscriptions.to_a
end
+ def test_child_is_visible_to_join_model_in_add_association_callbacks
+ [:before_add, :after_add].each do |callback_name|
+ sentient_treasure = Class.new(Treasure) do
+ def self.name; "SentientTreasure"; end
+
+ has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet!
+ has_many :pets, through: :pet_treasures
+
+ def check_pet!(added)
+ raise "No pet!" if added.pet.nil?
+ end
+ end
+
+ treasure = sentient_treasure.new
+ assert_nothing_raised { treasure.pets << pets(:mochi) }
+ end
+ end
+
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
diff --git a/activerecord/test/cases/boolean_test.rb b/activerecord/test/cases/boolean_test.rb
index ab9f974e2c..18824004d2 100644
--- a/activerecord/test/cases/boolean_test.rb
+++ b/activerecord/test/cases/boolean_test.rb
@@ -40,4 +40,13 @@ class BooleanTest < ActiveRecord::TestCase
assert_equal b_false, Boolean.find_by(value: "false")
assert_equal b_true, Boolean.find_by(value: "true")
end
+
+ def test_find_by_falsy_boolean_symbol
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ b_false = Boolean.create!(value: value)
+
+ assert_not_predicate b_false, :value?
+ assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym)
+ end
+ end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index 36b4000018..131e034c66 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -602,6 +602,13 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_extracted_association
+ relation_authors = assert_queries(2) { Post.all.extract_associated(:author) }
+ root_authors = assert_queries(2) { Post.extract_associated(:author) }
+ assert_equal relation_authors, root_authors
+ assert_equal Post.all.collect(&:author), relation_authors
+ end
+
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments).order("posts.id")