Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/ruby-on-rails/64.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Mysql 在rails中动态构建查询_Mysql_Ruby On Rails_Json_Ruby - Fatal编程技术网

Mysql 在rails中动态构建查询

Mysql 在rails中动态构建查询,mysql,ruby-on-rails,json,ruby,Mysql,Ruby On Rails,Json,Ruby,我正在尝试使用RubyonRails复制crunchbase的搜索列表样式。 我有一组过滤器,看起来像这样: [ { "id":"0", "className":"Company", "field":"name", "operator":"starts with", "val":"a" }, { "id":"1", "className":"Company", "field":"hq

我正在尝试使用RubyonRails复制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"
   }
]
Company.joins(:categories).where("name = :name and hq_city = :hq_city and categories.name = :categories[name]", {name: 'a', hq_city: 'Karachi', categories: {name: 'ECommerce'}})
SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)
select * from companies, categories where 'companies'.'name' LIKE 'a%' AND 'companies'.'hq_city' = 'karachi' AND 'categories'.'name' NOT LIKE '%ECommerce%'
base_model = "Company".constantize
assocations = [:categories]  # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
我将此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
我要做的是,在json数组上循环并创建一个散列,其中键是表名,值是包含列名、运算符和要应用该运算符的值的数组。因此,在创建
表\u name
散列之后,我会有类似的内容:

{
   'Company':[
      {
         fieldName:'name',
         operator:'starts with',
         val:'a'
      },
      {
         fieldName:'hq_city',
         operator:'equals',
         val:'karachi'
      }
   ],
   'Category':[
      {
         fieldName:'name',
         operator:'does not include',
         val:'ECommerce'
      }
   ]
}
现在,我循环遍历表\u name散列,并使用
模型创建一个where查询。where(“column\u name=:column\u name”,“column\u name:'abcd'})
语法

因此,我将生成两个查询:

SELECT "companies".* FROM "companies" WHERE (name = 'a' AND hq_city = 'b')
SELECT "categories".* FROM "categories" WHERE (name = 'c')
我现在有两个问题:

1。操作员:

我有许多运算符可以应用于列,如“开始于”、“结束于”、“等于”、“不等于”、“包含”、“不包含”、“大于”、“小于”。我猜最好的方法是在操作符上做一个switch case,并在构建where字符串时使用适当的符号。因此,例如,如果运算符是“start with”,我将执行类似于
的操作,其中的字符串+=“#{field[:fieldName]}类似于%:#{field[:fieldName]}”
以及其他类似操作

那么,这种方法正确吗?这种类型的通配符语法在这种
。where
中允许吗

2。多个表

如您所见,我的方法为2个以上的表构建了2个查询。我不需要2个查询,我需要类别名称位于该类别所属公司的同一查询中

现在我要做的是创建一个如下的查询:

[
   {
      "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"
   }
]
Company.joins(:categories).where("name = :name and hq_city = :hq_city and categories.name = :categories[name]", {name: 'a', hq_city: 'Karachi', categories: {name: 'ECommerce'}})
SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)
select * from companies, categories where 'companies'.'name' LIKE 'a%' AND 'companies'.'hq_city' = 'karachi' AND 'categories'.'name' NOT LIKE '%ECommerce%'
base_model = "Company".constantize
assocations = [:categories]  # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
但事实并非如此。搜索可能变得非常复杂。例如:

一家公司有很多资金来源。基金会可以有很多投资,投资可以有很多个人投资者。因此,我可以选择创建一个过滤器,如:

{
  "id":"0",
  "className":"IndividualInvestor",
  "field":"first_name",
  "operator":"starts with",
  "val":"za"
} 
我的方法将创建如下查询:

[
   {
      "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"
   }
]
Company.joins(:categories).where("name = :name and hq_city = :hq_city and categories.name = :categories[name]", {name: 'a', hq_city: 'Karachi', categories: {name: 'ECommerce'}})
SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)
select * from companies, categories where 'companies'.'name' LIKE 'a%' AND 'companies'.'hq_city' = 'karachi' AND 'categories'.'name' NOT LIKE '%ECommerce%'
base_model = "Company".constantize
assocations = [:categories]  # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
这个问题是错误的。我想查询一下个人投资者对公司融资轮的投资情况。这是很多连接表

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


如何解决此问题?

完整的SQL字符串是一个安全问题,因为它会使您的应用程序受到SQL注入攻击。如果您能够解决这个问题,那么完全可以进行这些查询连接,只要您使它们与DB()兼容即可

除此之外,您还可以创建一些字段,将一些查询标记为已连接,正如我在注释中提到的,您可以使用一些变量将所需的表标记为查询的输出,类似于:

[
  {
    "id":"1",
    "className":"Category",
    "field":"name",
    "operator":"does not include",
    "val":"ECommerce",
    "queryModel":"Company"
  }
]

在处理查询时,您将使用它将此查询的结果输出为
queryModel
,而不是
className
,在这些情况下,
className
将仅用于联接表条件

您可以基于哈希创建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
这种方法可能需要额外的逻辑来连接相关的表。 在您的情况下,如果
公司
类别
有关联,您必须在
查询中添加类似的内容,其中

"AND 'company'.'category_id' = 'categories'.'id'"
简单方法:您可以为可以查询的所有模型/表对创建哈希,并在其中存储适当的连接条件。即使对于中等规模的项目,这个散列也不应该太复杂

硬方法:如果在模型中正确定义了
有多个
有一个
属于
,则这可以自动完成。可以使用获取模型的关联。实施
呼吸优先搜索
深度优先搜索
算法,从任何模型开始,从json输入中搜索与其他模型的匹配关联。启动新的BFS/DFS运行,直到json输入中没有未访问的模型为止。根据找到的信息,您可以派生所有连接条件,然后将它们作为表达式添加到原始sql方法的
where
子句中,如上所述。更复杂但也可行的方法是读取数据库
模式
,并使用此处定义的类似方法,通过查找
外键

使用关联:如果所有关联都与
有多个关联
/
有一个关联
,您可以在“最重要”模型上使用
连接
方法和
注入
处理
与ActiveRecord的连接,如下所示:

[
   {
      "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"
   }
]
Company.joins(:categories).where("name = :name and hq_city = :hq_city and categories.name = :categories[name]", {name: 'a', hq_city: 'Karachi', categories: {name: 'ECommerce'}})
SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)
select * from companies, categories where 'companies'.'name' LIKE 'a%' AND 'companies'.'hq_city' = 'karachi' AND 'categories'.'name' NOT LIKE '%ECommerce%'
base_model = "Company".constantize
assocations = [:categories]  # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
它的作用是:

  • 它将基本_模型作为起始输入传递给,它将重复调用input.send(:joins,:assoc)(例如,这将执行
    Company.send(:joins,:categories)
    ,相当于'Company.categories'
  • 在组合联接上,它执行where条件(如上所述构造)

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

我建议更改您的JSON数据。现在您只发送模型名称,而不发送上下文,如果您的模型具有上下文,则会更容易

在您的示例中,数据必须如下所示

data = [
  {
    id: '0',
    className: 'Company',
    relation: 'Company',
    field: 'name',
    operator: 'starts with',
    val: 'a'
  },
  {
    id: '1',
    className: 'Category',
    relation: 'Company.categories',
    field: 'name',
    operator: 'equals',
    val: '12'
  },  
  {
    id: '3',
    className: 'IndividualInvestor',
    relation:     'Company.founding_rounds.investments.individual_investors',
    field: 'name',
    operator: 'equals',
    val: '12'
  }
]
然后将此
数据
发送到
QueryBuilder

query=QueryBuilder.new(数据)
results=query.find_记录

注意:
find_records
返回每个执行查询的
model
的哈希数组。 例如,它将返回
[{Company:[..]]

class QueryBuilder
  def initialize(data)
    @data = prepare_data(data)
  end

  def find_records
    queries = @data.group_by {|e| e[:model]}
    queries.map do |k, v|
      q = v.map do |f|
        {
          field: "#{f[:table_name]}.#{f[:field]} #{read_operator(f[:operator])} ?",
          value: value_based_on_operator(f[:val], f[:operator])
        }
      end

      db_query = q.map {|e| e[:field]}.join(" AND ")
      values = q.map {|e| e[:value]}

      {"#{k}": k.constantize.joins(join_hash(v)).where(db_query, *values)}
    end
  end

  private

  def join_hash(array_of_relations)
    hash = {}
    array_of_relations.each do |f|
      hash.merge!(array_to_hash(f[:joins]))
    end
    hash.map do |k, v|
      if v.nil?
        k
      else
        {"#{k}": v}
      end
    end
  end

  def read_operator(operator)
    case operator
    when 'equals'
      '='
    when 'starts with'
      'LIKE'
    end
  end

  def value_based_on_operator(value, operator)
    case operator
    when 'equals'
      value
    when 'starts with'
      "%#{value}"
    end
  end

  def prepare_data(data)
    data.each do |record|
      record.tap do |f|
        f[:model] = f[:relation].split('.')[0]
        f[:joins] = f[:relation].split('.').drop(1)
        f[:table_name] = f[:className].constantize.table_name
      end
    end
  end

  def array_to_hash(array)
    if array.length < 1
      {}
    elsif array.length == 1
      {"#{array[0]}": nil}
    elsif array.length == 2
      {"#{array[0]}": array[1]}
    else
      {"#{array[0]}": array_to_hash(array.drop(1))}
    end
  end
end
类查询生成器
def初始化(