我在一个预订应用程序上工作,其中每个Home编码>可以有几个Phone编码>. 我想将Phone编码>的数量限制为Home编码>到3,并在Create Phone表单中显示一个漂亮的错误.

我如何才能以Rails的方式实现这一点呢?


编码

class Phone < ApplicationRecord
  belongs_to :user
  validates :number, phone: true

  # validates_associated :homes_phones

  has_many :homes_phones, dependent: :destroy
  has_many :homes, through: :homes_phones

end

编码>
class User < ApplicationRecord
  has_many :phones, dependent: :destroy
end

编码>
class HomesPhone < ApplicationRecord
  belongs_to :home
  belongs_to :phone
  validate :check_phones_limit

  def check_phones_limit
    errors.add(:base, "too_many_phones") if home.phones.size >= 3
  end

end
编码>

规格

  it 'should limit phones to 3' do
    user = create(:user)
    home = create(:home, :active, manager: user)
    expect(home.phones.create(user: user, number: "+33611223344")).to be_valid
    expect(home.phones.create(user: user, number: "+33611223345")).to be_valid
    expect(home.phones.create(user: user, number: "+33611223346")).to be_valid

    # unexpectedly raises a ActiveRecord::RecordInvalid
    expect(home.phones.create(user: user, number: "+33611223347")).to be_invalid
  end
编码>

附注

我对流程的理解是:

  • 打开一笔交易
  • 电话属性经过验证(且有效)
  • 电话已创建,主键可用
  • HOMES_PHONE为saved!编码>,并引发错误,因为验证失败
  • 回滚ALL事务,错误冒泡上升

我试过了:

  • Home编码>中的has_many before_add编码>,这也会引发错误;
  • 在Phone编码>中验证这些规则对我来说没有意义,因为这条规则是一个Home编码>关注点

推荐答案

You can just validate it in the controller, count the phones and render a flash error, before you actually try and save the records.
Doing this in callbacks is difficult and not foolproof.


我添加了一些快速测试,以涵盖创建手机的不同方式:

# spec/models/homes_phone_spec.rb

require "rails_helper"

RSpec.describe HomesPhone, type: :model do
  it "saves 3 new phones" do
    home = Home.create(phones: 3.times.map { Phone.new })
    expect(home.phones.count).to eq 3
  end

  it "doesn't save 4 new phones" do
    home = Home.create(phones: 4.times.map { Phone.new })
    expect(home.phones.count).to eq 0
    expect(home.phones.size).to eq 4
  end

  it "can create up to 3 three phones through association" do
    home = Home.create!
    expect do
      5.times { home.phones.create }
    end.to change(home.phones, :count).by(3)
  end

  it "doesn't add 4th phone to existing record" do
    Home.create(phones: 3.times.map { Phone.new })
    home = Home.last

    # NOTE: every time you call `valid?` or `validate`, it runs validations 
    #       again and all previous errors are cleared.
    # expect(home.phones.create).to be_invalid
    
    expect(home.phones.create).to be_new_record
    # or
    # expect(home.phones.create).not_to be_persisted
    # expect(home.phones.create.errors.any?).to be true
  end

  it "adds phone limit validation error to Phone" do
    home = Home.create(phones: 3.times.map { Phone.new })
    phone = home.phones.create
    expect(phone.errors[:base]).to eq ["too many phones"]
  end
end
# app/models/phone.rb
class Phone < ApplicationRecord
  has_many :homes_phones, dependent: :destroy
  has_many :homes, through: :homes_phones
end

# app/models/home.rb
class Home < ApplicationRecord
  has_many :homes_phones, dependent: :destroy
  has_many :phones, through: :homes_phones
end
# app/models/homes_phone.rb
class HomesPhone < ApplicationRecord
  belongs_to :home
  # NOTE: setting `inverse_of` option will stop raising errors and
  #       just return `phone` with errors that we'll add below
  belongs_to :phone, inverse_of: :homes_phones

  # because of the way through association is saved with `save!` call
  # it raises validation errors. `inverse_of` allows the through association
  # to be properly built and it skips `save!` call:
  # https://github.com/rails/rails/blob/v7.0.4.2/activerecord/lib/active_record/associations/has_many_through_association.rb#L79-L80

  validate do
    # NOTE: if you use `homes_phones` association, the `size` method returns
    #       current count, instead of a lagging `phones.size` count.
    if home.homes_phones.size > 3
      errors.add(:base, "too many phones")
      phone.errors.merge!(errors)
    end
  end
end
$ rspec spec/models/homes_phone_spec.rb

HomesPhone
  saves 3 new phones
  doesn't save 4 new phones
  can create up to 3 three phones through association
  doesn't add 4th phone to existing record
  adds phone limit validation to Phone

Finished in 0.10967 seconds (files took 2.35 seconds to load)
5 examples, 0 failures

但它并不涵盖所有内容,比如:

it "can append up to 3 three phones" do
  home = Home.create!
  expect do
    5.times { home.phones << Phone.new }
  end.to change(home.phones, :count).by(3)
end
$ rspec spec/models/homes_phone_spec.rb:38
Run options: include {:locations=>{"./spec/models/homes_phone_spec.rb"=>[38]}}

HomesPhone
  can append up to 3 three phones (FAILED - 1)

我以为我把一切都修好了.您可以try 执行以下操作:

after_validation do
  if home.homes_phones.size > 3
    errors.add(:base, "too many phones")
    phone.errors.merge! errors
    raise ActiveRecord::Rollback
  end
end
$ rspec spec/models/homes_phone_spec.rb -f p
......

Finished in 0.12411 seconds (files took 2.25 seconds to load)
6 examples, 0 failures

Ruby-on-rails相关问答推荐

Rails引擎importmap Uncaught ReferenceError:未定义tinymce

Rails 5.1.7:Sprockets::Rails::Helper::AssetNotFound

Rails 7 - 富文本不显示附加的媒体内容

Rails:序列化数据库中的对象?

ActiveRecord 包括.指定包含的列

Rails:用于创建固定长度 char(12) 列的迁移

使用带有 bootstrap-sass gem 的 Rails 4 无法让 CSS 在 Heroku 上工作

如果 URL 不存在,请将 http(s) 添加到 URL?

twitter bootstrap下拉突然不起作用

如何添加到序列化数组

FactoryGirl + Faker - 为数据库种子数据中的每个对象生成相同的数据

从子类中的重载方法调用基类方法

是否有与 PHP 的 isset() 等效的 Rails?

警告:引用了顶级常量

Rails HABTM - 正确删除关联

Rails:如何修复‘生产’环境缺少 secret_key_base

如何一次显示一条 Ruby on Rails 表单验证错误消息?

如何翻译 ActiveRecord 模型类名称?

如何在 Ruby 类/模块命名空间中翻译模型?

为什么 Mac OS X 带有 ruby​​/rails?