提交 00f55516 编写于 作者: S Sean Griffin

Add a `required` option to singular associations

In addition to defining the association, a `required` association will
also have its presence validated.

Before:

```ruby
belongs_to :account
validates_presence_of :account
```

After:

```ruby
belongs_to :account, required: true
```

This helps to draw a distinction between types of validations, since
validations on associations are generally for data integrity purposes,
and aren't usually set through form inputs.
上级 a6cc7b0e
......@@ -1309,6 +1309,10 @@ def has_many(name, scope = nil, options = {}, &extension)
# that is the inverse of this <tt>has_one</tt> association. Does not work in combination
# with <tt>:through</tt> or <tt>:as</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# has_one :credit_card, dependent: :destroy # destroys the associated credit card
......@@ -1320,6 +1324,7 @@ def has_many(name, scope = nil, options = {}, &extension)
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
# has_one :credit_card, required: true
def has_one(name, scope = nil, options = {})
reflection = Builder::HasOne.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
......@@ -1421,6 +1426,10 @@ def has_one(name, scope = nil, options = {})
# object that is the inverse of this <tt>belongs_to</tt> association. Does not work in
# combination with the <tt>:polymorphic</tt> options.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
# This will validate the association itself, not the id. You can use
# +:inverse_of+ to avoid an extra query during validation.
#
# Option examples:
# belongs_to :firm, foreign_key: "client_of"
......@@ -1433,6 +1442,7 @@ def has_one(name, scope = nil, options = {})
# belongs_to :post, counter_cache: true
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
# belongs_to :company, required: true
def belongs_to(name, scope = nil, options = {})
reflection = Builder::BelongsTo.build(self, name, scope, options)
Reflection.add_reflection self, name, reflection
......
......@@ -36,6 +36,7 @@ def self.build(model, name, scope, options, &block)
reflection = builder.build(model)
define_accessors model, reflection
define_callbacks model, reflection
define_validations model, reflection
builder.define_extensions model
reflection
end
......@@ -124,6 +125,10 @@ def #{name}=(value)
CODE
end
def self.define_validations(model, reflection)
# noop
end
def self.valid_dependent_options
raise NotImplementedError
end
......
......@@ -3,7 +3,7 @@
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
def valid_options
super + [:remote, :dependent, :primary_key, :inverse_of]
super + [:remote, :dependent, :primary_key, :inverse_of, :required]
end
def self.define_accessors(model, reflection)
......@@ -27,5 +27,12 @@ def create_#{name}!(*args, &block)
end
CODE
end
def self.define_validations(model, reflection)
super
if reflection.options[:required]
model.validates_presence_of reflection.name
end
end
end
end
require "cases/helper"
class RequiredAssociationsTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
class Parent < ActiveRecord::Base
end
class Child < ActiveRecord::Base
end
setup do
@connection = ActiveRecord::Base.connection
@connection.create_table :parents, force: true
@connection.create_table :children, force: true do |t|
t.belongs_to :parent
end
end
teardown do
@connection.execute("DROP TABLE IF EXISTS parents")
@connection.execute("DROP TABLE IF EXISTS children")
end
test "belongs_to associations are not required by default" do
model = subclass_of(Child) do
belongs_to :parent, inverse_of: false,
class_name: "RequiredAssociationsTest::Parent"
end
assert model.new.save
assert model.new(parent: Parent.new).save
end
test "required belongs_to associations have presence validated" do
model = subclass_of(Child) do
belongs_to :parent, required: true, inverse_of: false,
class_name: "RequiredAssociationsTest::Parent"
end
record = model.new
assert_not record.save
assert_equal ["Parent can't be blank"], record.errors.full_messages
record.parent = Parent.new
assert record.save
end
test "has_one associations are not required by default" do
model = subclass_of(Parent) do
has_one :child, inverse_of: false,
class_name: "RequiredAssociationsTest::Child"
end
assert model.new.save
assert model.new(child: Child.new).save
end
test "required has_one associations have presence validated" do
model = subclass_of(Parent) do
has_one :child, required: true, inverse_of: false,
class_name: "RequiredAssociationsTest::Child"
end
record = model.new
assert_not record.save
assert_equal ["Child can't be blank"], record.errors.full_messages
record.child = Child.new
assert record.save
end
private
def subclass_of(klass, &block)
subclass = Class.new(klass, &block)
def subclass.name
superclass.name
end
subclass
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册