我试图在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%)
此查询错误。我想询问个人投资者对公司融资的投资情况。这是很多连接表。
我使用的方法适用于单个模型,无法解决上述问题。
我该如何解决这个问题?
答案 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
将为您提供模型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%'
此方法可能需要额外的逻辑来连接相关的表。
在您的情况下,如果company
和category
有关联,则必须向query_where
"AND 'company'.'category_id' = 'categories'.'id'"
简单方法:您可以为可以查询的所有模型/表对创建一个哈希,并在那里存储适当的连接条件。即使对于中型项目,这种哈希也不应该太复杂。
硬方法:如果您的模型中已正确定义has_many
,has_one
和belongs_to
,则可以自动完成此操作。您可以使用reflect_on_all_associations获取模型的关联。实现Breath-First-Search
或Depth-First Search
算法,从任何模型开始,并从json输入中搜索与其他模型的匹配关联。启动新的BFS / DFS运行,直到没有来自json输入的未访问模型。从找到的信息中,您可以派生所有连接条件,然后将它们作为表达式添加到原始sql方法的where
子句中,如上所述。更复杂但也可行的是阅读数据库schema
并使用类似于此处定义的方法,查找foreign keys
。
使用关联:如果所有关联都与has_many
/ has_one
相关联,则可以使用{{1}来处理与ActiveRecord
的联接在“最重要”模型上使用joins
的方法,如下所示:
inject
这是做什么的:
base_model = "Company".constantize
assocations = [:categories] # and so on
result = assocations.inject(base_model) { |model, assoc| model.joins(assoc) }.where(query_where)
,相当于`Company。类免责声明您需要的确切语法可能因您使用的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,除了说你的方法对我来说似乎没问题。我会把它分解成多个控制器。