在rails

时间:2017-10-28 01:53:57

标签: mysql ruby-on-rails json ruby

我试图在rails上使用ruby复制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"
   }
]

我将这个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数组并使用键创建一个哈希作为表名,值是具有列名,运算符和应用该运算符的值的数组。所以在创建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'
      }
   ]
}

现在我遍历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')

我现在有两个问题:

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'}})

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

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

{
  "id":"0",
  "className":"IndividualInvestor",
  "field":"first_name",
  "operator":"starts with",
  "val":"za"
} 

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

SELECT "individual_investors".* FROM "individual_investors" WHERE (first_name like %za%)

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

我使用的方法适用于单个模型,无法解决上述问题。

我该如何解决这个问题?

4 个答案:

答案 0 :(得分:2)

完整的SQL字符串是一个安全问题,因为它会将您的应用程序暴露给SQL注入攻击。如果你可以解决这个问题,那么只要你使它们与你的数据库(yes, this solution is DB specific)兼容,就可以完成那些查询连接。

除此之外,您可以创建一些将某些查询标记为已加入的字段,正如我在注释中提到的那样,您可以使用一些变量将所需的表标记为查询的输出,如:

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

在处理查询时,您将使用queryModel而不是className来输出此查询的结果,在这些情况下,className仅用于:='(format "=+")加入表条件。

答案 1 :(得分:2)

您可以根据哈希创建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

这是做什么的:

  • 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%'

此方法可能需要额外的逻辑来连接相关的表。 在您的情况下,如果companycategory有关联,则必须向query_where

添加类似内容
"AND 'company'.'category_id' = 'categories'.'id'"

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

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

使用关联:如果所有关联都与has_many / has_one相关联,则可以使用{{1}来处理与ActiveRecord的联接在“最重要”模型上使用joins的方法,如下所示:

inject

这是做什么的:

  • 它将base_model作为起始输入传递给Enumerable.inject,它将重复调用input.send(:joins,:assoc)(对于我的例子,这将base_model = "Company".constantize assocations = [:categories] # and so on result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where) ,相当于`Company。类
  • 在组合连接上,它执行where条件(如上所述构造)

免责声明您需要的确切语法可能因您使用的SQL实现而异。

答案 2 :(得分:1)

我建议更改您的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'
  }
]

您将data发送给QueryBuilder

query = QueryBuilder.new(data) results = query.find_records

注意: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

答案 3 :(得分:1)

我觉得你通过为所有东西配备一个控制器使事情变得复杂。我会为你想要显示的每个模型或实体创建一个控制器,然后像你说的那样实现过滤器。

实现动态where和order by并不是很难,但如果如你所说,你需要有逻辑来实现一些连接,那么你不仅要使解决方案复杂化(因为你必须更新这个控制器)每次添加新模型,实体或更改基本逻辑时,您也可以让人们开始使用您的数据。

我对Rails不是很熟悉,所以很遗憾我不能给你任何特定的cde,除了说你的方法对我来说似乎没问题。我会把它分解成多个控制器。