mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Add change tracking methods for belongs_to associations
Permit checking whether a belongs_to association has been pointed to a new target record in the previous save and whether it will point to a new target record in the next save. post.category # => #<Category id: 1, name: "Ruby"> post.category = Category.second # => #<Category id: 2, name: "Programming"> post.category_changed? # => true post.category_previously_changed? # => false post.save! post.category_changed? # => false post.category_previously_changed? # => true
This commit is contained in:
parent
6442aac195
commit
fc3acf2e8d
8 changed files with 193 additions and 7 deletions
|
@ -1,3 +1,14 @@
|
|||
* Two change tracking methods are added for `belongs_to` associations.
|
||||
|
||||
The `association_changed?` method (assuming an association named `:association`) returns true
|
||||
if a different associated object has been assigned and the foreign key will be updated in the
|
||||
next save.
|
||||
|
||||
The `association_previously_changed?` method returns true if the previous save updated the
|
||||
association to reference a different associated object.
|
||||
|
||||
*George Claghorn*
|
||||
|
||||
* Add option to disable schema dump per-database
|
||||
|
||||
Dumping the schema is on by default for all databases in an application. To turn it off for a
|
||||
|
|
|
@ -364,6 +364,8 @@ module ActiveRecord
|
|||
# create_other(attributes={}) | X | | X
|
||||
# create_other!(attributes={}) | X | | X
|
||||
# reload_other | X | X | X
|
||||
# other_changed? | X | X |
|
||||
# other_previously_changed? | X | X |
|
||||
#
|
||||
# === Collection associations (one-to-many / many-to-many)
|
||||
# | | | has_many
|
||||
|
@ -1624,6 +1626,10 @@ module ActiveRecord
|
|||
# if the record is invalid.
|
||||
# [reload_association]
|
||||
# Returns the associated object, forcing a database read.
|
||||
# [association_changed?]
|
||||
# Returns true if a new associate object has been assigned and the next save will update the foreign key.
|
||||
# [association_previously_changed?]
|
||||
# Returns true if the previous save updated the association to reference a new associate object.
|
||||
#
|
||||
# === Example
|
||||
#
|
||||
|
@ -1634,6 +1640,8 @@ module ActiveRecord
|
|||
# * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>)
|
||||
# * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>)
|
||||
# * <tt>Post#reload_author</tt>
|
||||
# * <tt>Post#author_changed?</tt>
|
||||
# * <tt>Post#author_previously_changed?</tt>
|
||||
# The declaration can also include an +options+ hash to specialize the behavior of the association.
|
||||
#
|
||||
# === Scopes
|
||||
|
|
|
@ -68,6 +68,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def target_changed?
|
||||
owner.attribute_changed?(reflection.foreign_key) || (!foreign_key_present? && target&.new_record?)
|
||||
end
|
||||
|
||||
def target_previously_changed?
|
||||
owner.attribute_previously_changed?(reflection.foreign_key)
|
||||
end
|
||||
|
||||
def saved_change_to_target?
|
||||
owner.saved_change_to_attribute?(reflection.foreign_key)
|
||||
end
|
||||
|
||||
|
|
|
@ -10,6 +10,14 @@ module ActiveRecord
|
|||
end
|
||||
|
||||
def target_changed?
|
||||
super || owner.attribute_changed?(reflection.foreign_type)
|
||||
end
|
||||
|
||||
def target_previously_changed?
|
||||
super || owner.attribute_previously_changed?(reflection.foreign_type)
|
||||
end
|
||||
|
||||
def saved_change_to_target?
|
||||
super || owner.saved_change_to_attribute?(reflection.foreign_type)
|
||||
end
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
define_accessors model, reflection
|
||||
define_callbacks model, reflection
|
||||
define_validations model, reflection
|
||||
define_change_tracking_methods model, reflection
|
||||
reflection
|
||||
end
|
||||
|
||||
|
@ -117,6 +118,10 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
# noop
|
||||
end
|
||||
|
||||
def self.define_change_tracking_methods(model, reflection)
|
||||
# noop
|
||||
end
|
||||
|
||||
def self.valid_dependent_options
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
@ -158,6 +163,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
|
||||
private_class_method :build_scope, :macro, :valid_options, :validate_options, :define_extensions,
|
||||
:define_callbacks, :define_accessors, :define_readers, :define_writers, :define_validations,
|
||||
:valid_dependent_options, :check_dependent_options, :add_destroy_callbacks, :add_after_commit_jobs_callback
|
||||
:define_change_tracking_methods, :valid_dependent_options, :check_dependent_options,
|
||||
:add_destroy_callbacks, :add_after_commit_jobs_callback
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,7 +30,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
model.after_update lambda { |record|
|
||||
association = association(reflection.name)
|
||||
|
||||
if association.target_changed?
|
||||
if association.saved_change_to_target?
|
||||
association.increment_counters
|
||||
association.decrement_counters_before_last_save
|
||||
end
|
||||
|
@ -87,7 +87,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
if reflection.counter_cache_column
|
||||
touch_callback = callback.(:saved_changes)
|
||||
update_callback = lambda { |record|
|
||||
instance_exec(record, &touch_callback) unless association(reflection.name).target_changed?
|
||||
instance_exec(record, &touch_callback) unless association(reflection.name).saved_change_to_target?
|
||||
}
|
||||
model.after_update update_callback, if: :saved_changes?
|
||||
else
|
||||
|
@ -127,7 +127,20 @@ module ActiveRecord::Associations::Builder # :nodoc:
|
|||
end
|
||||
end
|
||||
|
||||
private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks, :define_validations,
|
||||
:add_counter_cache_callbacks, :add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks
|
||||
def self.define_change_tracking_methods(model, reflection)
|
||||
model.generated_association_methods.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def #{reflection.name}_changed?
|
||||
association(:#{reflection.name}).target_changed?
|
||||
end
|
||||
|
||||
def #{reflection.name}_previously_changed?
|
||||
association(:#{reflection.name}).target_previously_changed?
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
private_class_method :macro, :valid_options, :valid_dependent_options, :define_callbacks,
|
||||
:define_validations, :define_change_tracking_methods, :add_counter_cache_callbacks,
|
||||
:add_touch_callbacks, :add_default_callbacks, :add_destroy_callbacks
|
||||
end
|
||||
end
|
||||
|
|
|
@ -27,11 +27,13 @@ require "models/treasure"
|
|||
require "models/parrot"
|
||||
require "models/book"
|
||||
require "models/citation"
|
||||
require "models/tree"
|
||||
require "models/node"
|
||||
|
||||
class BelongsToAssociationsTest < ActiveRecord::TestCase
|
||||
fixtures :accounts, :companies, :developers, :projects, :topics,
|
||||
:developers_projects, :computers, :authors, :author_addresses,
|
||||
:essays, :posts, :tags, :taggings, :comments, :sponsors, :members
|
||||
:essays, :posts, :tags, :taggings, :comments, :sponsors, :members, :nodes
|
||||
|
||||
def test_belongs_to
|
||||
client = Client.find(3)
|
||||
|
@ -1508,6 +1510,104 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
|
|||
assert_equal 1, Comment.where(post_id: post.id).count
|
||||
assert_equal post.id, Comment.last.post.id
|
||||
end
|
||||
|
||||
test "tracking change from one persisted record to another" do
|
||||
node = nodes(:child_one_of_a)
|
||||
assert_not_nil node.parent
|
||||
assert_not node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.parent = nodes(:grandparent)
|
||||
assert node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.save!
|
||||
assert_not node.parent_changed?
|
||||
assert node.parent_previously_changed?
|
||||
end
|
||||
|
||||
test "tracking change from persisted record to new record" do
|
||||
node = nodes(:child_one_of_a)
|
||||
assert_not_nil node.parent
|
||||
assert_not node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.parent = Node.new(tree: node.tree, parent: nodes(:parent_a), name: "Child three")
|
||||
assert node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.save!
|
||||
assert_not node.parent_changed?
|
||||
assert node.parent_previously_changed?
|
||||
end
|
||||
|
||||
test "tracking change from persisted record to nil" do
|
||||
node = nodes(:child_one_of_a)
|
||||
assert_not_nil node.parent
|
||||
assert_not node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.parent = nil
|
||||
assert node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.save!
|
||||
assert_not node.parent_changed?
|
||||
assert node.parent_previously_changed?
|
||||
end
|
||||
|
||||
test "tracking change from nil to persisted record" do
|
||||
node = nodes(:grandparent)
|
||||
assert_nil node.parent
|
||||
assert_not node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.parent = Node.create!(tree: node.tree, name: "Great-grandparent")
|
||||
assert node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.save!
|
||||
assert_not node.parent_changed?
|
||||
assert node.parent_previously_changed?
|
||||
end
|
||||
|
||||
test "tracking change from nil to new record" do
|
||||
node = nodes(:grandparent)
|
||||
assert_nil node.parent
|
||||
assert_not node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.parent = Node.new(tree: node.tree, name: "Great-grandparent")
|
||||
assert node.parent_changed?
|
||||
assert_not node.parent_previously_changed?
|
||||
|
||||
node.save!
|
||||
assert_not node.parent_changed?
|
||||
assert node.parent_previously_changed?
|
||||
end
|
||||
|
||||
test "tracking polymorphic changes" do
|
||||
comment = comments(:greetings)
|
||||
assert_nil comment.author
|
||||
assert_not comment.author_changed?
|
||||
assert_not comment.author_previously_changed?
|
||||
|
||||
comment.author = authors(:david)
|
||||
assert comment.author_changed?
|
||||
|
||||
comment.save!
|
||||
assert_not comment.author_changed?
|
||||
assert comment.author_previously_changed?
|
||||
|
||||
assert_equal authors(:david).id, companies(:first_firm).id
|
||||
|
||||
comment.author = companies(:first_firm)
|
||||
assert comment.author_changed?
|
||||
|
||||
comment.save!
|
||||
assert_not comment.author_changed?
|
||||
assert comment.author_previously_changed?
|
||||
end
|
||||
end
|
||||
|
||||
class BelongsToWithForeignKeyTest < ActiveRecord::TestCase
|
||||
|
|
|
@ -839,7 +839,7 @@ If the table of the other class contains the reference in a one-to-one relation,
|
|||
|
||||
#### Methods Added by `belongs_to`
|
||||
|
||||
When you declare a `belongs_to` association, the declaring class automatically gains 6 methods related to the association:
|
||||
When you declare a `belongs_to` association, the declaring class automatically gains 8 methods related to the association:
|
||||
|
||||
* `association`
|
||||
* `association=(associate)`
|
||||
|
@ -847,6 +847,8 @@ When you declare a `belongs_to` association, the declaring class automatically g
|
|||
* `create_association(attributes = {})`
|
||||
* `create_association!(attributes = {})`
|
||||
* `reload_association`
|
||||
* `association_changed?`
|
||||
* `association_previously_changed?`
|
||||
|
||||
In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration:
|
||||
|
||||
|
@ -865,6 +867,8 @@ build_author
|
|||
create_author
|
||||
create_author!
|
||||
reload_author
|
||||
author_changed?
|
||||
author_previously_changed?
|
||||
```
|
||||
|
||||
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
|
||||
|
@ -913,6 +917,34 @@ The `create_association` method returns a new object of the associated type. Thi
|
|||
|
||||
Does the same as `create_association` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
|
||||
|
||||
##### `association_changed?`
|
||||
|
||||
The `association_changed?` method returns true if a new associated object has been assigned and the foreign key will be updated in the next save.
|
||||
|
||||
```ruby
|
||||
@book.author # => #<Book author_number: 123, author_name: "John Doe">
|
||||
@book.author_changed? # => false
|
||||
|
||||
@book.author = Author.second # => #<Book author_number: 456, author_name: "Jane Smith">
|
||||
@book.author_changed? # => true
|
||||
|
||||
@book.save!
|
||||
@book.author_changed? # => false
|
||||
```
|
||||
|
||||
##### `association_previously_changed?`
|
||||
|
||||
The `association_previously_changed?` method returns true if the previous save updated the association to reference a new associate object.
|
||||
|
||||
```ruby
|
||||
@book.author # => #<Book author_number: 123, author_name: "John Doe">
|
||||
@book.author_previously_changed? # => false
|
||||
|
||||
@book.author = Author.second # => #<Book author_number: 456, author_name: "Jane Smith">
|
||||
@book.save!
|
||||
@book.author_previously_changed? # => true
|
||||
```
|
||||
|
||||
#### Options for `belongs_to`
|
||||
|
||||
While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this association uses two such options:
|
||||
|
|
Loading…
Reference in a new issue