在rails中动态构建查询

Moh*_*qui 12 ruby mysql json ruby-on-rails

我试图使用ruby on rails复制crunchbase的搜索列表样式.我有一系列过滤器,看起来像这样:

[
   {
      "id":"0",
      "className":"Company",
      "field":"name",
      "operator":"starts with",
      "val":"a"
   },
   {
      "id":"1",
      "className":"Company",
      "field":"hq_city",
      "operator":"equals",
      "val":"Karachi"
   },
   {
      "id":"2",
      "className":"Category",
      "field":"name",
      "operator":"does not include",
      "val":"ECommerce"
   }
]
Run Code Online (Sandbox Code Playgroud)

我将这个json字符串发送到我已经实现了这个逻辑的ruby控制器:

filters = params[:q]
table_names = {}
filters.each do |filter|
    filter = filters[filter]
    className = filter["className"]
    fieldName = filter["field"]
    operator = filter["operator"]
    val = filter["val"]
    if table_names[className].blank? 
        table_names[className] = []
    end
    table_names[className].push({
        fieldName: fieldName,
        operator: operator,
        val: val
    })
end

table_names.each do |k, v|
    i = 0
    where_string = ''
    val_hash = {}
    v.each do |field|
        if i > 0
            where_string += ' AND '
        end
        where_string += "#{field[:fieldName]} = :#{field[:fieldName]}"
        val_hash[field[:fieldName].to_sym] = field[:val]
        i += 1
    end
    className = k.constantize
    puts className.where(where_string, val_hash)
end
Run Code Online (Sandbox Code Playgroud)

我所做的是,我遍历json数组并使用键创建一个哈希作为表名,值是具有列名,运算符和应用该运算符的值的数组.在table_names创建哈希之后我会有类似的东西:

{
   'Company':[
      {
         fieldName:'name',
         operator:'starts with',
         val:'a'
      },
      {
         fieldName:'hq_city',
         operator:'equals',
         val:'karachi'
      }
   ],
   'Category':[
      {
         fieldName:'name',
         operator:'does not include',
         val:'ECommerce'
      }
   ]
}
Run Code Online (Sandbox Code Playgroud)

现在我遍历table_names哈希并使用Model.where("column_name = :column_name", {column_name: 'abcd'})语法创建where查询.

所以我会生成两个查询:

SELECT "companies".* FROM "companies" WHERE (name = 'a' AND hq_city = 'b')
SELECT "categories".* FROM "categories" WHERE (name = 'c')
Run Code Online (Sandbox Code Playgroud)

我现在有两个问题:

1.运营商:

我有很多运算符可以应用于像'开头','结束','等于','不等于','包含','不包括','大于','小于".我猜测最好的方法是在运算符上做一个switch case并在构造where字符串时使用适当的符号.因此,例如,如果运营商是'开始',我会where_string += "#{field[:fieldName]} like %:#{field[:fieldName]}"为其他人做类似的事情.

这种方法是否正确,这种类型的通配符语法是否允许.where

2.超过1张桌子

如您所见,我的方法为超过2个表构建了2个查询.我不需要2个查询,我需要类别名称在类别属于公司的同一查询中.

现在我想要做的是我需要创建一个这样的查询:

Company.joins(:categories).where("name = :name and hq_city = :hq_city and categories.name = :categories[name]", {name: 'a', hq_city: 'Karachi', categories: {name: 'ECommerce'}})
Run Code Online (Sandbox Code Playgroud)

但这不是它.搜索可能变得非常复杂.例如:

公司有很多FundingRound.FundingRound可以有很多投资和投资可以有很多IndividualInvestor.所以我可以选择创建一个过滤器,如:

{
  "id":"0",
  "className":"IndividualInvestor",
  "field":"first_name",
  "operator":"starts with",
  "val":"za"
} 
Run Code Online (Sandbox Code Playgroud)

我的方法会创建一个这样的查询:

SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)
Run Code Online (Sandbox Code Playgroud)

这个查询是错误的.我想询问个人投资者对公司融资的投资情况.这是很多连接表.

我使用的方法适用于单个模型,无法解决我上面提到的问题.

我该如何解决这个问题?

dfh*_*err 3

您可以根据哈希值创建 SQL 查询。最通用的方法是原始 SQL,它可以由ActiveRecord.

以下是一些概念代码,应该可以为您提供正确的想法:

query_select = "select * from "
query_where = ""
tables = [] # for selecting from all tables
hash.each do |table, values|
  table_name = table.constantize.table_name
  tables << table_name
  values.each do |q|
    query_where += " AND " unless query_string.empty?
    query_where += "'#{ActiveRecord::Base.connection.quote(table_name)}'."
    query_where += "'#{ActiveRecord::Base.connection.quote(q[fieldName)}'"
    if q[:operator] == "starts with" # this should be done with an appropriate method
      query_where += " LIKE '#{ActiveRecord::Base.connection.quote(q[val)}%'"
    end
  end
end
query_tables = tables.join(", ")
raw_query = query_select + query_tables + " where " + query_where 
result = ActiveRecord::Base.connection.execute(raw_query)
result.to_h # not required, but raw results are probably easier to handle as a hash
Run Code Online (Sandbox Code Playgroud)

这是做什么的:

  • query_select指定您想要在结果中包含哪些信息
  • query_where构建所有搜索条件并转义输入以防止 SQL 注入
  • query_tables是您需要搜索的所有表的列表
  • table_name = table.constantize.table_name将为您提供模型使用的 SQL table_name
  • raw_query是上面部分的实际组合 SQL 查询
  • ActiveRecord::Base.connection.execute(raw_query)在数据库上执行sql

确保将所有用户提交的输入放在引号中并正确转义以防止 SQL 注入。

对于您的示例,创建的查询将如下所示:

select * from companies, categories where 'companies'.'name' LIKE 'a%' AND 'companies'.'hq_city' = 'karachi' AND 'categories'.'name' NOT LIKE '%ECommerce%'
Run Code Online (Sandbox Code Playgroud)

此方法可能需要额外的逻辑来连接相关的表。就您而言,如果companycategory有关联,您必须将类似的内容添加到query_where

"AND 'company'.'category_id' = 'categories'.'id'"
Run Code Online (Sandbox Code Playgroud)

简单方法:您可以为所有可查询的模型/表对创建一个哈希,并在其中存储适当的连接条件。即使对于中型项目,这个哈希也不应该太复杂。

硬方法:has_many如果您有,has_one并且belongs_to在模型中正确定义,这可以自动完成。您可以使用Reflect_on_all_associations获取模型的关联。实现Breath-First-SearchorDepth-First Search算法并从任何模型开始,然后从 json 输入中搜索与其他模型的匹配关联。开始新的 BFS/DFS 运行,直到 json 输入中没有未访问的模型为止。从找到的信息中,您可以导出所有连接条件,然后将它们作为表达式添加到where原始 sql 方法的子句中,如上所述。更复杂但也可行的是读取数据库schema并使用此处定义的类似方法通过查找foreign keys.

使用关联:如果它们全部与has_many/关联,您可以使用“最重要”模型上的方法来has_one处理连接,如下所示:ActiveRecordjoinsinject

base_model = "Company".constantize
assocations = [:categories]  # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
Run Code Online (Sandbox Code Playgroud)

这是做什么的:

  • 它将 base_model 作为起始输入传递给Enumerable.inject,它将重复调用 input.send(:joins, :assoc) (对于我的示例,这相当于Company.send(:joins, :categories)`Company.categories
  • 在组合连接上,它执行 where 条件(如上所述构造)

免责声明您需要的确切语法可能会根据您使用的 SQL 实现而有所不同。