Rails 夭寿了,关于单表继承和多态的使用 (STI and polymorphs)

Qcoder · 发布于 2016年12月20日 · 最后由 Qcoder 回复于 2016年12月21日 · 909 次阅读
29362

环境

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

http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#module-ActiveRecord::Associations::ClassMethods-label-Polymorphic+Associations

补充: 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
共收到 37 条回复
3035

看了几遍才明白你想表达什么 代码贴错了吧

29362

#1楼 @IChou 就那几个关系呢,还需要哪些代码,我贴出来。

3楼 已删除
23529

这个不属于STI吧,如果是STI的话你的car表应该有type列,然后初始化的时候应该就会初始化成Bicycle

29362

#4楼 @mizuhashi 我前提就是sti,有type我应该不需要特殊说明,sti没有type怎么叫sti呢?

初始化是bicycle,对象也是bicycle,级联拿prices的时候,priceable_type确实是Car而不是Bicycle

您看懂我的问题了吗?

如果您觉得我的阐述不清楚,可以翻阅文末的链接,同样的问题。😜

23529

#5楼 @Qcoder 多态关联存基类很正常....因为price没有bicycle的知识,他只能Car.find(1)然后再由Car根据type初始化成Bicycle,我觉得在有type字段的时候,应该能初始化出Bicycle,sql语句用car应该没啥问题

29362

#6楼 @mizuhashi 您能看下我的问题吗,期望的是找price的时候priceable_type为Bicycle,您不用称述事实啊。。😅

23529

#6楼 @mizuhashi 都说了Price类没有Bicycle的知识。。STI和多态关联是两套独立的系统,多态关联只要把STI相关的过程委托给STI的基类就可以了,根本不需要知道他有什么子类。。。

你如果发现取不出正确的数据可以讨论一下,如果是觉得实现不爽,或者是哪里还要用数据表这个栏位,那你只能自己去改AR的查询代码,反正ruby有打开类,按你喜欢的实现就好了

29362

#8楼 @mizuhashi 感觉脑子不够用了 😭

29362

冒昧请 @Rei 指点下,感觉睡不着了

22325

你将has_many :prices写在 Bicycle里面试试,至于原因,楼上已经给出答案了

29362

#11楼 @zxzllyj 你在刻意逗我笑,Bicycle是继承Car过来的哇。

22325

#12楼 @Qcoder 你似乎没怎么玩单表继承?Car只是基类,其他继承自他的类都可以有自己的一套关联关系,而写在基类中的关系可以被所有继承自他的类调用但严格来讲还是属于他本身。因为 Prices是属于car的关系而不是 Bicycle的,虽然 Bicycle可以调用 Prices,但是这个是因为Bicycle继承自CarPrices本身和Bicycle本身并没有直接的关系,所以在sql中表现就是多态的type是Car。 PS:认真回答你的问题如果是在逗你笑的话,那么我想下次不会了

29362

#13楼 @zxzllyj 抱歉,我可能没说完整,你说的早就尝试过啦,然而...

22325

#14楼 @Qcoder 你的数据库字段中有type字段么?如果有的话理论上会识别成单表继承然后生成正确的sql的,如果没有这个字段,那么单表继承是不生效的,单表继承不生效自然也就别想指望sql正确了。

29362

#15楼 @zxzllyj 当然有,我说过了,是标准的STI Single Table Inheritance 😕

17楼 已删除
22325

翻了下rails的源码,我已被老外的搞法绕晕了,期待大牛的解释。。。

29362

17楼的哥们让我补充Cars的migration,我刚补充完,为什么删了评论了 😭

3035

其实我已经在第三条回复里写出答案了,但是当时在车上,没法验证,我又给删了,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 的话,仔细想想对开发者反而会成为一种负担,因为模型间关系的可读性会变得很差,需要跳多个文件才能理清楚关联关系,在一个稍微复杂一点的系统里这简直就是个噩梦。

29362

#20楼 @IChou 多谢!知道为什么那个PR被关闭了

那么,如果我想在Bicycle去继承的时候改写@@priceable_type = Bicycle改如何操作呢?

3035

其实我不建议 Bicycle < Car 除非从对象自身来说他们就是继承关系,比如 Polo < Car

如果你只是想实现代码复用,可以尝试用一下 Model 的 concerns,然后在 Bicycle 和 Car 中去 include。模型间的关系,老老实实在每个模型里声明就好了。

29362

#23楼 @IChou 嗯,其实我就是想知道该如何改写,但不一定这么做,知道怎么改可能能了解一些更深的东西。 😁

3035

呃 我结合 #20963 那个 PR 看了下 Rails 的实现,我上面关于 类变量 的猜测应该是不对的,这里不是类变量导致的

按这个来看,Rails 直接指定了使用 base_class.name ,看来是有意为之啊

2650

@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
2650

@Qcoder 没注意已经这么多讨论了,画蛇添足了。。。

29362

#27楼 @blueplanet 哪有,非常感谢您,我的操作跟您的一样,但是bicycle1.prices却找不出来东西,因为它的sql找的是priceable_type='Car',我再新建一个纯净项目做尝试,排除别的可能的影响。

96

其实这并没有什么问题。 虽然sql找的是priceable_type='Car', 但是你的Bicycle是继承自Car的,他去Car中根据id找最终找出来的如果type是Bicycle那就是Bicycle。 另:个人觉得这并不需要用到多态,如果仅仅是Bicycle继承Car的话。在Price中只需要car_id

29362

#29楼 @chen_rb type在数据库的确实是Bicycle,但无法找到关联的prices

只有car_id的话,bicycle.prices就没法用了

96
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">]>
96

#30楼 @Qcoder 只有car_id的话,bicycle.prices就没法用了 你试过吗

29362

#31楼 @chen_rb 说反了,昨天我尝试的是price.bicycle... Nil... 😅

是不是给price写个bicycle方法?

96

。。。price既没有bicycle方法 也没有field。 你就要访问。 这是要上天啊。 你可以写个方法或者scope都可以啊

96

额, 还有我有点强迫症。 你那个Bicycle语意上讲应该不能继承Car啊。 应该都抽象继承自Vehicle

29362

#34楼 @chen_rb 见笑啦,我以为会找父类的方法

不过我还是想搞明白sti和多态共用的问题...

29362

#35楼 @chen_rb 只是个例子啦

3035

诶 我又回来了 接 #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 的字段上。。。。。当这个数据库脱离了你的代码,换一个系统来驱动时,估计你同事会骂死你,哈哈哈~

29362

#38楼 @IChou 非常感谢您,顺了下,好像清晰很多,非常感谢您!!😚

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册