Rails 5.0.1,postgresql - 别名外键和包含别名的表包含失败

时间:2017-04-26 10:51:24

标签: ruby-on-rails ruby postgresql activerecord

tl;博士:

所有表和列(包括外键)的数据库模糊处理导致在使用所有rails查询从postresql数据库急切加载关联模型的结果时出现问题。 includesjoins等返回错误而不是应该错误。

开始:

我有一个完全混淆的数据库,每个表和列都使用20个字符的拉丁字母随机字符串命名,包括foreignkeys。 has_many关系的典型迁移是:

class CreateModelOne < ActiveRecord::Migration[5.0]
  def change
    create_table :abcdfeghijklmnopqrst do |t|
      t.boolean :bcdfeghijklmnopqrstu, null: true
      t.float :cdfeghijklmnopqrstuv, null: true, default: 0
      t.timestamps
    end
  end
end

class CreateModelTwo < ActiveRecord::Migration[5.0]
  def change
    create_table :dfeghijklmnopqrstuvx do |t|
      t.date :feghijklmnopqrstuvxy, null: true
      t.time :eghijklmnopqrstuvxyz, null: true
      t.integer :ghijklmnopqrstuvxyza
      t.timestamps
    end
    add_foreign_key :dfeghijklmnopqrstuvx, :abcdfeghijklmnopqrst, column: :ghijklmnopqrstuvxyza
  end
end

在模型文件中,我对关系产生了别名:

class ModelTwo < ApplicationRecord
  alias_attribute :model_one_id, :ghijklmnopqrstuvxyza
end

当我想加载与ModelTwo相关联的ModelOne时,我正在尝试使用标准includes方法:

@model_one = ModelOne.includes(:model_two).find(particular_model_one_id)

这会产生错误:

NoMethodError: undefined method `association' for nil:NilClass
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/associations/preloader/association.rb:67:in `block in associated_records_by_owner'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/core.rb:349:in `init_with'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/persistence.rb:69:in `instantiate'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/querying.rb:50:in `block (2 levels) in find_by_sql'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/result.rb:52:in `block in each'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/result.rb:52:in `each'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/result.rb:52:in `each'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/querying.rb:50:in `map'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/querying.rb:50:in `block in find_by_sql'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activesupport-5.0.2/lib/active_support/notifications/instrumenter.rb:21:in `instrument'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/activerecord-5.0.2/lib/active_record/querying.rb:49:in `find_by_sql'
    from /home/mark/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/bullet-5.5.1/lib/bullet/active_record5.rb:25:in `find_by_sql'
    from 

这对我来说似乎很奇怪,因为单独运行查询可以正常工作,如:

ModelOne.find(particular_model_one_id)

返回预期的ModelOne

ModelTwo.where(model_one_id: particular_model_one_id)

返回预期的ModelTwo s集。与SELECT一样,这些查询会向数据库生成完全相同的.includes查询。

查看ModelOne.includes(:model_two)返回的对象会返回我期望的查询以及消息:

ModelOne Load (2.3ms)  SELECT "abcdfeghijklmnopqrst".* FROM "abcdfeghijklmnopqrst"
  ModelTwo Load (2.6ms)  SELECT "dfeghijklmnopqrstuvx".* FROM "dfeghijklmnopqrstuvx" WHERE "dfeghijklmnopqrstuvx"."ghijklmnopqrstuvxyza" IN (4, 1, 2, 3, 8, 5, 7, 6, 9, 10, 11, 14, 12, 13, 18, 15, 17, 16, 19, 20, 21, 22, 28, 23, 25, 24, 27, 26, 29, 30, 31, 34, 32, 33, 38, 35, 37, 36, 39, 40, 41, 42, 48, 43, 45, 44, 47, 46, 49, 50, 51, 52, 57, 53, 55, 54, 58, 56)
(Object doesn't support #inspect)

我认为这也意味着(Object doesn't support #association)

声明:

我已经阅读了相关的stackoverflow条目和github上的Rails问题,它们都没有解决这个特定的问题,至少尝试实现其中提供的建议才能正常工作。

我已更改了在迁移中向foreign_key添加references的应用程序,这会产生完全相同的行为。

当然,我可以将正确的查询写入数据库,或者不急于加载关联的模型,但这对于开发人员来说似乎效率低下,因为解析手写JOIN的结果也需要处理并且(更多)真实模型有多个列和多个关联。

1 个答案:

答案 0 :(得分:0)

所以我制作了一个模块来解决这个问题,随意使用你可怕的混乱数据库......准备好hackiness:

module AliasedAttrsHelper
  def find_by_includes_aliased(id_col, id_match, *associations)
    # get foreign key column name
    class_name = self.to_s
    fk_column = class_name[0]
    class_name.split('').each_with_index do |char,i| 
      next if i == 0 
      fk_column += char.match(/[A-Z]/) ? "_#{char}" : char
    end
    fk_column += '_id'
    fk_column.downcase!
    # make id string and table name
    id_column = self.attribute_aliases[id_col.to_s]
    id_table = self.table_name
    # make the set of associations to query on
    associations.map! do |ass|
      name = ass.to_s.singularize.capitalize.constantize
      { class: name, table: name.table_name, fk: name.new.attribute_aliases[fk_column] }
    end
    # perform the composite query transaction
    klass = ""
    ActiveRecord::Base.transaction do
      klass = self.find_by_sql("SELECT c.* FROM #{id_table} AS c WHERE c.#{id_column} = '#{id_match}'")[0]
      associations.each do |ass|
        ass[:class].find_by_sql("SELECT b.* FROM #{ass[:table]} AS b JOIN #{id_table} as c ON c.id = b.#{ass[:fk]} WHERE c.#{id_column} = '#{id_match}'")
      end
    end
    klass
  end
end