首页 文章

在查询中自定义生成的SELECT? [花岗岩ORM]

提问于
浏览
3

我正在尝试在遗留数据库(使用现有数据和表结构)的Amber(使用Granite ORM)中构建JOIN查询,并想知道是否可以自定义查询的SELECT FROM部分以支持跨表JOIN .

这是名为 vehicles 的表的当前表结构:

-----------------------------------------------
| vehicleid | year | makeid | modelid |
-----------------------------------------------
| 1 | 1999 | 54 | 65 |
| 2 | 2000 | 55 | 72 |
| ... | ... | ... | ... |
-----------------------------------------------

等等

makeidmodelidmakesmodels 表的外键引用 . 在这些表中是命名列(分别为 makenamemodelname ) .

我正在尝试生成一个JOIN查询来引入名称:

SELECT vehicle.yearid, make.makename AS make, model.modelname AS model FROM vehicles JOIN....

(剪掉JOIN细节) .

因此,当查询返回时,我有一个 Vehicle 对象,可以访问:

Vehicle.yearid

Vehicle.make ,和

Vehicle.model

这可能使用花岗岩吗?

我可以通过使用原始SQL来获取查询的JOIN部分,但我无法弄清楚如何在SELECT部分中自定义表和列名称 . 我试过创建一个对象:

class Vehicle < Granite::ORM::Base
  adapter pg

  primary vehicleid : Int32
  field yearid : Int32
  field make : String
  field model : String
end

但是Granite正在生成以下SQL:

SELECT vehicle.yearid, vehicle.make, vehicle.model FROM vehicle JOIN...

那是因为 vehicle.makevehicle.model 实际上并不存在而引发错误 .

我想要的是这个SQL:

SELECT vehicle.yearid, make.makename AS make, model.modelname AS model FROM vehicles JOIN....

有没有办法让这项工作?

2 回答

  • 4

    根据this issue,Granite还没有一对一的关系,但作者提到通过使用 has_many 宏并定义一个调用宏定义的方法但返回第一个元素的方法,有一个临时的解决方法 . 该方法返回的数组(因为它只能是一个元素) .

    首先,您需要为另外两个表创建模型, modelmake

    class Model < Granite::ORM::Base
      adapter pg
    
      belongs_to :vehicle
    
      primary modelid : Int32
      field modelname : String
    end
    
    class Make < Granite::ORM::Base
      adapter pg
    
      belongs_to :vehicle
    
      primary makeid : Int32
      field makename : String
    end
    

    如果您有更多字段而不仅仅是 modelnamemakename ,请务必同时添加这些字段 .

    最后,您需要将 has_many 关系添加到原始 Vehicle 类,并定义 makemodel 方法:

    class Vehicle < Granite::ORM::Base
      adapter pg
    
      primary vehicleid : Int32
      field yearid : Int32
    
      has_many :makes
      has_many :models
    
      def make
        makes.first["makename"]
      end
    
      def model
        models.first["modelname"]
      end
    end
    

    然后查询就像这样简单:

    vehicle = Vehicle.find 2
    
    puts vehicle.model
    

    不幸的是,我不相信Granite在没有完全绕过ORM的情况下支持列别名( AS ),因此您必须显式返回这些列(上面的代码所做的)或直接使用 vehicle.model["modelname"] 访问属性 .

    注意:我可能已经得到了Granite返回的Hash类型错误,因为它们的源代码没有't got any type annotations and fully relies on Crystal'类型推断,这使得很难导航 . 但我认为这是 {} of String => DB::Any ,但我可能错了 . 如果出现编译器错误,请尝试使用 Symbol 而不是 String .

  • 4

    感谢@svenskunganka给了我一个思考这条路线的想法,我想出了一个与Granite精神相近的解决方案,它与原始SQL保持接近,让ORM坚持将字段映射到对象 .

    我在模型定义中添加了一个 sql 类方法,该方法与 all 几乎完全相同,但剥离了更多结构 . 我还必须向pg适配器添加一个新的 query 方法以支持它,但这现在适用于我的用例 . 这是猴子修补的代码:

    class Granite::Adapter::Pg < Granite::Adapter::Base
      def query(statement = "", params = [] of DB::Any, &block)
        statement = _ensure_clause_template(statement)
        log statement, params
        open do |db|
          db.query statement, params do |rs|
            yield rs
          end
        end
      end
    end
    
    module Granite::ORM::Querying
      def sql(clause = "", params = [] of DB::Any)
        rows = [] of self
        @@adapter.query(clause, params) do |results|
          results.each do
            rows << from_sql(results)
          end
        end
        return rows
      end
    end
    

    它有点难看(我愿意接受清理这个建议)但我现在可以编写以下代码:

    vehicles = Vehicle.sql("SELECT vehicle.vehicleid, vehicle.yearid, make.makename AS make, 
    model.modelname AS model FROM vehicle JOIN ...<snip>")
    

    然后我可以这样做:

    vehicles.each do |v| 
      puts "#{v.yearid} #{v.make} #{v.model}"
    end
    

    它按预期工作 .

相关问题