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

Qcoder · December 20, 2016 · Last by Qcoder replied at December 21, 2016 · 3804 hits

环境

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

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

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

3 Floor has deleted

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

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

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

您看懂我的问题了吗?

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

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

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

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

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

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

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

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

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

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

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

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

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

17 Floor has deleted

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

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

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

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

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

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

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

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

呃 我结合 #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

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

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

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

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

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

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

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

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

是不是给 price 写个 bicycle 方法?

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

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

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

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

#35 楼 @chen_rb 只是个例子啦

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

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

You need to Sign in before reply, if you don't have an account, please Sign up first.