我有一个模型Foo,其state_code
作为外键。 States表是一个(或多或少)静态表,用于保存50个州的代码和名称,以及其他美国邮政编码(例如" PR"波多黎各)。我选择使用state_code
作为状态的主键和Foo上的外键,而不是state_id
。它对人类更好,并简化了我想调用状态代码的视图逻辑。 (编辑 - 只是为了澄清:我并不是说从视图中调用代码来访问模型;我的意思是将状态显示为@foo.state_code
似乎比{{1}更简单}。)
Foo与模特Bar也有@foo.state.state_code
的关系。两个模型规范都传递了有效工厂的规范,但由于某些原因,在运行构建Bar实例的功能规范时,由于与has_many
我为所有模型传递了模型规格,包括有效工厂的测试。但是,每当我尝试为Bar'创建测试对象时,我都会遇到麻烦。使用state_code
炸毁了Foo中build
的外键错误(尽管事实上Foo工厂明确指定了一个确认在状态中作为state_code存在的值)。使用state_code
作为Bar对象似乎不会持久存在该对象。
模特:
build_stubbed
下面的工厂为我的Foo和Bar模型传递绿色,所以从模型的角度看工厂似乎很好:
# models/foo.rb
class Foo < ActiveRecord
belongs_to :state, foreign_key: 'state_code', primary_key: 'state_code'
has_many :bars
validates :state_code, presence: true, length: { is: 2 }
# other code omitted...
end
# models/state.rb
class State < ActiveRecord
self.primary_key = 'state_code'
has_many :foos, foreign_key: 'state_code'
validates :state_code, presence: true, uniqueness: true, length: { is: 2 }
# other code omitted...
end
# models/bar.rb
class Bar < ActiveRecord
belongs_to :foo
# other code omitted
end
......基本规格是:
# spec/factores/foo_bar_factory.rb
require 'faker'
require 'date'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state_code { 'AZ' } # Set code for Arizona b/c doesn't matter which state
end
factory :bar do
name { Faker::Name.name }
website_url { Faker::Internet.url }
# other columns omitted
association :foo
end
end
此规范通过,没有任何问题。但是如果我使用# spec/models/foo_spec.rb
require 'rails_helper'
describe Foo, type: :model do
let(:foo) { build(:foo) }
it "has a valid factory" do
expect(foo).to be_valid
end
# code omitted...
end
# spec/models/bar_spec.rb
require 'rails_helper'
describe Bar, type: :model do
let(:bar) { build_stubbed(:bar) } # have to build_stubbed - build causes error
it "has a valid factory" do
expect(bar).to be_valid
end
end
代替build(:bar)
而不是build_stubbed
,我会在外键上出错:
1) Bar has a valid factory
Failure/Error: let(:bar) { build(:bar) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "bars" violates foreign key constraint "fk_rails_3dd3a7c4c3"
DETAIL: Key (state_code)=(AZ) is not present in table "states".
代码&#39; AZ&#39;肯定是在州表中,所以我不清楚它失败的原因。
在功能规范中,我尝试创建持久存储在数据库中的bar实例,因此我可以测试它们是否在#index,#show和#edit actions中正确显示。但是我似乎无法让它正常工作。功能规范失败: #spec / features / bar_pages_spec.rb 要求&#39; rails_helper&#39;
feature "Bar pages" do
context "when signed in as admin" do
let!(:bar_1) { build_stubbed(:bar) }
let!(:bar_2) { build_stubbed(:bar) }
let!(:bar_3) { build_stubbed(:bar) }
# code omitted...
scenario "clicking manage bar link shows all bars" do
visit root_path
click_link "Manage bars"
save_and_open_page
expect(page).to have_css("tr td a", text: bar_1.name)
expect(page).to have_css("tr td a", text: bar_2.name)
expect(page).to have_css("tr td a", text: bar_3.name)
end
end
此规范失败,并显示一条消息,指出没有匹配项。使用save_and_open_page
不会在视图中显示预期的项目。 (我有一个包含开发数据的工作页面,所以我知道逻辑实际上按预期工作)。 The thoughtbot post on build_stubbed
表示它应该持久化对象:
它使对象看起来像是被持久化,创建的 与build_stubbed策略的关联(而build仍然使用 创建),并存储一些与之交互的方法 如果你打电话给数据库并加注。
...但在我的规范中它似乎没有这样做。在此规范中尝试使用build
代替build_stubbed
会产生上述相同的外键错误。
我真的被困在这里了。模型似乎有有效的工厂并通过所有规格。但是功能规格要么会破坏外键关系,要么似乎不会在视图之间保留build_stubbed
对象。感觉就像一团糟,但我无法找到解决问题的正确方法。我在实践中有实际的,工作的观点,做我期望的 - 但我希望测试覆盖率有效。
更新
我回去并更新了所有模型代码,以删除state_code
的自然键。我遵循了@ Max的所有建议。 Foo表现在使用state_id
作为states
的外键;我按照推荐的那样复制了app/models/concerns/belongs_to_state.rb
的代码等等。
更新了schema.rb:
create_table "foos", force: :cascade do |t|
# columns omitted
t.integer "state_id"
end
create_table "states", force: :cascade do |t|
t.string "code", null: false
t.string "name"
end
add_foreign_key "foos", "states"
模型规格通过,我的一些更简单的功能规格通过。我现在意识到只有在创建了多个Foo对象时才会出现问题。发生这种情况时,由于列:code
Failure/Error: let!(:foo_2) { create(:foo) }
ActiveRecord::RecordInvalid:
Validation failed: Code has already been taken
我试图在工厂中直接设置:state_id
列:foo以避免调用:state工厂。 E.g。
# in factory for foo:
state_id { 1 }
# generates following error on run:
Failure/Error: let!(:foo_1) { create(:foo) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "foos" violates foreign key constraint "fk_rails_5f3d3f12c3"
DETAIL: Key (state_id)=(1) is not present in table "states".
显然state_id
不在州,因为它在状态上是id
,在foos中是state_id
。另一种方法:
# in factory for foo:
state { 1 } # alternately w/ same error -> state 1
ActiveRecord::AssociationTypeMismatch:
State(#70175500844280) expected, got Fixnum(#70175483679340)
或者:
# in factory for foo:
state { State.first }
ActiveRecord::RecordInvalid:
Validation failed: State can't be blank
我真正想做的就是创建一个Foo对象的实例,让它包含与states
表中某个状态的关系。我不希望对states
表进行大量更改 - 它只是一个参考。
我 DON&#39; T 需要创建一个新状态。我只需要使用states表中state_id
列中的66个值之一填充Foo对象上的外键:id
。从概念上讲,:foo
的工厂理想情况下只需为:state_id
选择1到66之间的整数值。它适用于控制台:
irb(main):001:0> s = Foo.new(name: "Test", state_id: 1)
=> #<Foo id: nil, name: "Test", city: nil, created_at: nil, updated_at: nil, zip_code: nil, state_id: 1>
irb(main):002:0> s.valid?
State Load (0.6ms) SELECT "states".* FROM "states" WHERE "states"."id" = $1 LIMIT 1 [["id", 1]]
State Exists (0.8ms) SELECT 1 AS one FROM "states" WHERE ("states"."code" = 'AL' AND "states"."id" != 1) LIMIT 1
=> true
我现在唯一能看到的方法是摆脱:code
中states
列的唯一性限制。或者 - 删除foos
和states
之间的外键约束,并让Rails强制执行该关系。
很抱歉这篇文章......
答案 0 :(得分:3)
我将成为一个痛苦的人,并认为约定可能比开发人员的便利性和感知可读性更重要。
Rails的一大优点是强大的约定允许我们打开任何项目并快速找出正在发生的事情(前提是原作者不是完全黑客)。尝试使用PHP项目。
其中一个约定是外键后缀为_id
。 FactoryGirl等许多其他组件都依赖于这些惯例。
我还认为,如果您的应用程序曾在美国境外使用过,那么使用州代码作为主要ID会导致问题。当您需要跟踪加拿大各省或印度的州和地区时会发生什么?你打算怎么处理不可避免的冲突?即使您认为这可能不是今天的交易,也要记住要求会随着时间而变化。
我会将其建模为:
create_table "countries", force: :cascade do |t|
t.string "code", null: false # ISO 3166-1 alpha-2 or alpha-3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "countries", ["code"], name: "index_countries_on_code"
create_table "states", force: :cascade do |t|
t.integer "country_id"
t.string "code", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "states", ["code"], name: "index_states_on_code"
add_index "states", ["country_id", "code"], name: "index_states_on_country_id_and_code"
add_index "states", ["country_id"], name: "index_states_on_country_id"
“并简化了我想调用状态代码的视图逻辑”
我认为如果可以避免的话,你根本不应该从你的视图中进行数据库调用。从您的控制器预先查询并将数据传递到您的视图。它使优化查询和避免N + 1问题变得更加简单。
使用演示者或辅助方法来帮助管理复杂性。必须执行State.find_by(code: 'AZ')
代替State.find('AZ')
的轻微不便很可能不像您想象的那么重要。
这是您在FactoryGirl中正确使用关联的方法。考虑这个解决方案的简单性,最后一个论点是为什么你的自定义外键安排可能会导致更多的悲伤而不是方便。
模型:
class State < ActiveRecord::Base
# Only the State model should be validating its attributes.
# You have a major violation of concerns.
validates_uniqueness_of :state_code
validates_length_of :state_code, is: 2
end
# app/models/concerns/belongs_to_state.rb
module BelongsToState
extend ActiveSupport::Concern
included do
belongs_to :state
validates :state, presence: true
validates_associated :state # will not let you save a Foo or Bar if the state is invalid.
end
def state_code
state.state_code
end
def state_code= code
self.assign_attributes(state: State.find_by!(state_code: code))
end
end
class Foo < ActiveRecord::Base
include BelongsToState
end
class Bar < ActiveRecord::Base
include BelongsToState
end
工厂:
# spec/factories/foos.rb
require 'faker'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state
end
end
# spec/factories/states.rb
FactoryGirl.define do
factory :state do
state_code "AZ"
name "Arizona"
end
end
这些规范使用shoulda-matchers
作为极端的succint验证示例:
require 'rails_helper'
RSpec.describe Foo, type: :model do
let(:foo) { build(:foo) }
it { should validate_presence_of :state }
it 'validates the associated state' do
foo.state.state_code = 'XYZ'
foo.valid?
expect(foo.errors).to have_key :state
end
describe '#state_code' do
it 'returns the state code' do
expect(foo.state_code).to eq 'AZ'
end
end
describe '#state_code=' do
let!(:vt) { State.create(state_code: 'VT') }
it 'allows you to set the state with a string' do
foo.state_code = 'VT'
expect(foo.state).to eq vt
end
end
end
# spec/models/state_spec.rb
require 'rails_helper'
RSpec.describe State, type: :model do
it { should validate_length_of(:state_code).is_equal_to(2) }
it { should validate_uniqueness_of(:state_code) }
end
https://github.com/maxcal/sandbox/tree/31773581
您还需要使用FactoryGirl.create
而不是build_stubbed
在功能,控制器或集成规范中 。 build_stubbed
不会将模型持久保存到数据库中,在这些情况下,您需要控制器能够从数据库加载记录。
如果可能,您应该避免在功能规格中使用CSS
选择器。功能规范应该从用户的POV描述您的应用程序。
feature "Bar management" do
context "as an Admin" do
let!(:bars){ 3.times.map { create(:bar) } }
background do
visit root_path
click_link "Manage bars"
end
scenario "I should see all the bars on the management page" do
# just testing a sampling is usually good enough
expect(page).to have_link bars.first.name
expect(page).to have_link bars.last.name
end
scenario "I should be able to edit a Bar" do
click_link bars.first.name
fill_in('Name', with: 'Moe´s tavern')
# ...
end
end
end
答案 1 :(得分:0)
这里发生了很多事情,但就FactoryGirl问题引发了Foo和State之间的外键关系,我已经明白了。
@Max是关于在states
表上使用自然键作为主键的问题。它不遵循Rails惯例,并导致一些混淆问题,例如可能需要验证Foo表上的外键(例如长度2)。
但即使在修复了链接Rails友好密钥(:state_id
上的表作为foos
上的外键,:id
作为states
上的主键之后 - 我仍然找不到使用:foo
工厂创建多个Foo对象实例的方法。当我试图将整数值“插入”state_id
时,或者:state
工厂在第二个实例上失败时,它会失败,说明代码已经存在。 (有关尝试和相关失败的详细信息,请参阅问题中的更新。)
唯一的方法似乎是删除State上的唯一性验证,或消除数据库层的外键关系(Postgres 9.4)。我决定不想做前者。在考虑后者时,我意识到我确实不需要数据库中的外键约束。 states
表仅用于提供一致的状态代码列表作为参考点。如果由于某种原因我要删除这个表,那么我想要销毁所有的Foo记录是不正确的。他们基本上是独立的,国家只是Foo的一个属性。我简要地考虑将状态信息放入常量,但是meh。
为我删除数据库级外键约束固定的东西。
bin/rails g migration RemoveForeignKeyStatesFromFoos
class RemoveForeignKeyStatesFromFoos < ActiveRecord::Migration
def change
remove_foreign_key :foos, :states
end
end
这使我的:state_id
表格中的foos
列保持不变,但从我的schema.rb中删除了行add_foreign_key "foos", "states"
bin/rails g migration AddIndexToStateIdInFoos
class AddIndexToStateIdInFoos < ActiveRecord::Migration
def change
add_index :foos, :state_id
end
end
...将行add_index "foos", ["state_id"], name: "index_foos_on_state_id", using: :btree
添加到我的架构中。
迁移后,我最初错误地删除了:state
工厂,认为我不需要创建新状态。经过测试中的一些令人头疼的事情后,我意识到测试数据库通常不会被rake db:seed
播种 - 所以我的测试由于Module::DelegationError
的奇怪错误而失败了。我没有构建一个脚本来为状态设定测试dB,而是修改了工厂,并将关联保留在:foo
工厂。
# spec/factories/foo_factory.rb
FactoryGirl.define do
factory :foo do
# columns omitted
state
end
factory :state do
code { Faker::Address.state_abbr }
code { Faker::Address.state }
end
end
此时,Rails仍然成功验证模型中的has_many
和belongs_to
关系(未更改)。
我理解add_foreign_key
方法对于Rails来说相对较新,as of 4.2。我将这种关系的事实与在数据库层建立实际外键约束的需要混为一谈。
来自Rails Guide for ActiveRecord Associations:
您有责任维护数据库架构以匹配您的 关联。在实践中,这意味着两件事,取决于什么 您正在创建的一些关联。对于belongs_to协会你 需要创建外键,以及has_and_belongs_to_many 您需要创建适当的连接表的关联。
在这种情况下使用术语“外键”对于Rails来说似乎意味着与Postgres不同。只要belongs_to
表中的列与约定[parent_table_name]_id
匹配,Rails就会非常满意。这可以通过显式添加列或在迁移中使用references
来实现:
使用t.integer:supplier_id使外键命名明显 明确。在当前版本的Rails中,您可以抽象出来 使用t.references实现细节:供应商改为
就我而言,这已足够 - 实际的外键不是必需的。