Mysql 在rails中动态构建查询
我正在尝试使用RubyonRails复制crunchbase的搜索列表样式。 我有一组过滤器,看起来像这样: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
[
{
"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初始化(