环境
Ruby 2.3.1
Rails 5.0.1
代码
class Car < ActiveRecord::Base
has_many :prices, :as => :priceable, :dependent => :destroy
end
class Bicycle < Car
end
class Price < ActiveRecord::Base
belongs_to :priceable, :polymorphic => true, :counter_cache => true
end
很奇怪,想取 Bicycle 的所有价目明细
Bicycle.first.prices
Sql 居然是这样的
Price Load (0.9ms) SELECT "prices".* FROM "prices" WHERE ("prices"."pricetable_id" = 1 and "prices"."priceable_type" = 'Car') ORDER BY created_at
priceable_type = 'Car'
居然取的是 Car,不是预期的 Bicycle
看了很多资料,说是取的 base_class 而不是 self_class
有个人提了个 PR https://github.com/rails/rails/pull/20963
但是被关闭了,有人解释下为什么这个问题一直在,然后该怎么解决? (ps: 我现在在 Bicycle model 写了个 prices 方法,自己去取)
参考资料:
https://github.com/rails/rails/issues/20893
补充:Cars 的 migration
class CreateCars < ActiveRecord::Migration[5.0]
def change
create_table :Cars do |t|
t.string :name
t.string :bio
t.integer :prices_count
t.string :type
t.timestamps
end
end
end
#4 楼 @mizuhashi 我前提就是 sti,有 type 我应该不需要特殊说明,sti 没有 type 怎么叫 sti 呢?
初始化是 bicycle,对象也是 bicycle,级联拿 prices 的时候,priceable_type 确实是 Car 而不是 Bicycle
您看懂我的问题了吗?
如果您觉得我的阐述不清楚,可以翻阅文末的链接,同样的问题。
#6 楼 @mizuhashi 都说了 Price 类没有 Bicycle 的知识。。STI 和多态关联是两套独立的系统,多态关联只要把 STI 相关的过程委托给 STI 的基类就可以了,根本不需要知道他有什么子类。。。
你如果发现取不出正确的数据可以讨论一下,如果是觉得实现不爽,或者是哪里还要用数据表这个栏位,那你只能自己去改 AR 的查询代码,反正 ruby 有打开类,按你喜欢的实现就好了
其实我已经在第三条回复里写出答案了,但是当时在车上,没法验证,我又给删了,23333 ~~~
这个行为和 Rails 是没什么关系的,先看个栗子:
class A
@@n = self.name
def self.my_name
@@n
end
end
class B < A
def self.b_name
@@n
end
end
puts A.my_name #=> "A"
puts B.my_name #=> "A"
puts B.b_name #=> "A"
类继承的时候,是会连着类变量一起继承的
Car
这个类在初始化的时候,会写入一个类变量 @@priceable_type = car
Bicycle
在继承的时候并不会主动去改写这个变量
因为这是 ruby 本身的行为,所以 Rails 开发组认为这种行为就是符合预期的行为。如果真的按楼主设想的那种行为去修改 rails 的话,仔细想想对开发者反而会成为一种负担,因为模型间关系的可读性会变得很差,需要跳多个文件才能理清楚关联关系,在一个稍微复杂一点的系统里这简直就是个噩梦。
其实我不建议 Bicycle < Car
除非从对象自身来说他们就是继承关系,比如 Polo < Car
如果你只是想实现代码复用,可以尝试用一下 Model 的 concerns,然后在 Bicycle 和 Car 中去 include。模型间的关系,老老实实在每个模型里声明就好了。
呃 我结合 #20963 那个 PR 看了下 Rails 的实现,我上面关于 类变量 的猜测应该是不对的,这里不是类变量导致的
按这个来看,Rails 直接指定了使用 base_class.name
,看来是有意为之啊
@Qcoder 到底是不是 bug 还是有争议的。因为实际上你的代码是可以动的。 参考你给的连接尝试了一下,下面的测试是通过的
最后的测试里面, assert_equal Bicycle.name, price.priceable.class.name
是重点。
price.priceable_type
确实是 Car
,但这只是从Price model
方向去找priceable
的时候的问题,
而实际上创建 price.priceable 这个对象的时候,到底是Car
还是Bicycle
,看的是cars
这个表里的type
字段的值。
Bicycle
级连拿prices
的时候,不定义方法应该也是正常能够跑的Price
去找priceable
的纪录的时候,应该用Car
还是实际的Bicycle
,这个属于 Rails 的设计问题,个人觉得可以选择性的忽略 require 'active_record'
require 'minitest/autorun'
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Base.connection.instance_eval do
create_table(:cars) do |t|
t.string :name
t.string :type
t.string :bicycle_column1
t.integer :prices_count
end
create_table(:prices) do |t|
t.string :name
t.string :priceable_type
t.integer :priceable_id
end
end
class Car < ActiveRecord::Base
has_many :prices, :as => :priceable, :dependent => :destroy
end
class Bicycle < Car
def aaa?
bicycle_column1 == 'aaa'
end
end
class Price < ActiveRecord::Base
belongs_to :priceable, :polymorphic => true, :counter_cache => true
end
class MyTest < Minitest::Test
def test_1
bicycle1 = Bicycle.create name: 'bircycle1', bicycle_column1: 'aaa'
bicycle1.prices.create name: 'price1'
price = Bicycle.first.prices.first
assert_equal Bicycle.name, price.priceable.class.name
assert_equal true, price.priceable.aaa?
assert_equal bicycle1.prices.first, Price.first
end
end
#27 楼 @blueplanet 哪有,非常感谢您,我的操作跟您的一样,但是 bicycle1.prices 却找不出来东西,因为它的 sql 找的是 priceable_type='Car',我再新建一个纯净项目做尝试,排除别的可能的影响。
其实这并没有什么问题。虽然 sql 找的是 priceable_type='Car',但是你的 Bicycle 是继承自 Car 的,他去 Car 中根据 id 找最终找出来的如果 type 是 Bicycle 那就是 Bicycle。 另:个人觉得这并不需要用到多态,如果仅仅是 Bicycle 继承 Car 的话。在 Price 中只需要 car_id
2.0.0-p648 :004 > Price.first.priceable
Price Load (0.2ms) SELECT "prices".* FROM "prices" ORDER BY "prices"."id" ASC LIMIT 1
Car Load (0.1ms) SELECT "cars".* FROM "cars" WHERE "cars"."id" = ? LIMIT 1 [["id", 1]]
=> #<Bicycle id: 1, name: "A", type: "Bicycle", created_at: "2016-12-21 01:30:05", updated_at: "2016-12-21 01:30:05">
2.0.0-p648 :005 > Bicycle.first.prices
Bicycle Load (0.2ms) SELECT "cars".* FROM "cars" WHERE "cars"."type" IN ('Bicycle') ORDER BY "cars"."id" ASC LIMIT 1
Price Load (0.1ms) SELECT "prices".* FROM "prices" WHERE "prices"."priceable_id" = ? AND "prices"."priceable_type" = ? [["priceable_id", 1], ["priceable_type", "Car"]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Price id: 1, price: "10.0", priceable_type: "Car", priceable_id: 1, created_at: "2016-12-21 01:36:21", updated_at: "2016-12-21 01:36:21">, #<Price id: 2, price: nil, priceable_type: "Car", priceable_id: 1, created_at: "2016-12-21 01:36:41", updated_at: "2016-12-21 01:36:41">]>
诶 我又回来了
接 #25 楼贴的图
那个就是 model 生成关联属性的方法,它指明了 as
这种关联的 type 是 owner.class.base_class.name
owner
是当前的 AR 记录,可以看到它取的 base_class
的名称
Car.first.association('prices').owner.class.base_class.name #=> "Car"
Bicycle.first.association('prices').owner.class.base_class.name #=> "Car"
当然,这里只是取值的时候,如果要强行篡改,可以在它初始化之后修改它的接收器 attributes
,但是这是个内部变量,我没有找到可以用来修改的 setter,也就怪不得那个歪果仁提了个 PR 来处理这个问题
另外我上面也提到了,这是一种设计的行为,并非 bug,即使抛开类继承这种限制。而且我同意 #32 的看法,这里应该是不影响使用的吧,只是数据上面 "prices"."priceable_type"
是 Car
, Bicycle
的数据也是记录在 cars
表中,这样的数据库设计是健壮的,脱离 rails 也是没问题的
如果真的是 "prices"."priceable_type"
存的 bicycle
, 但是根本没有 bicycles
这张表,Bicycle
只出现在 cars
表中有个叫 type
的字段上。。。。。当这个数据库脱离了你的代码,换一个系统来驱动时,估计你同事会骂死你,哈哈哈~