是这样的,我们准备提供一个服务,允许用户在我们的项目上搭建自己的 API 服务器
用户会在项目 A 下创建一条允许访问的 API,格式:请求方法 GET,请求 URI /publishers/:p_id/magazines/:m_id/photos/:p_id 我们要把这个 API 作为一条记录存到数据库里
然后,当我们需要用以下条件去查找某条 API,比如: 方法参数是 GET URL 参数(字符串)是 /publishers/15/magazines/3/photos/2
我该如何存记录,如何查找记录,才能把这个 " /publishers/15/magazines/3/photos/2 " 匹配到数据库中的 " /publishers/:p_id/magazines/:m_id/photos/:p_id " 这条记录?
我尝试着去用 Rails 内置的 route 方法解析和查找记录,但是没有头绪无从下手
@huacnlee 可能需求没有描述清楚,实际上需求是通过一个字符串精准地去匹配一个 route path
" /publishers/15/magazines/3/photos/2 " 匹配到数据库中的 " /publishers/:p_id/magazines/:m_id/photos/:p_id " 这条记录
" /publishers/15/magazines/3 " 的话就不应该匹配到数据库中的 " /publishers/:p_id/magazines/:m_id/photos/:p_id " 这条记录
并且 " /publishers/:p_id/magazines/:m_id/photos/:p_id" 这个 path 是写在数据库里,不是写在 route.rb 里面...
我个人想到用 trie 树的概念去实现。。 举个栗子,数据库记录如下:
id | route |
---|---|
1 | /publishers/:p_id/magazines/:m_id/photos/:p_id |
2 | /publishers/:p_id/magazines/:m_id |
然后构建一棵树,按照分隔符划分为词,变量转为*表示通配,is_end == true 代表有记录,id 存储数据库 id,refer_count 引用计数
root
/
publishers (refer_count=2)
|
* (refer_count=2)
|
magazines
|
* (is_end=true, id=2, refer_count=2)
|
photos (refer_count=1)
|
* (is_end=true, id=1,refer_count=1)
我用 ruby 实现了基本逻辑测试了一下。随机 100 万 url,查是很快,但是构建非常慢而且很吃内存,占了大概 2 个 G 的内存。要写出用在生产环境的代码有点难,所以嘛....
看看楼下有没有解决方案....
@saiga 我有个想法,就是为每个用户建一个路由表,然后写入
get /publishers/:p_id/magazines/:m_id/photos/:p_id => 35(数据库中的 ID) post /publishers/:p_id/magazines/:m_id => 34
然后把传进来的字符串例如 " /publishers/15/magazines/3/photos/2 " 作为 URL 去用这个路由表做匹配
因为本质上,实际需求其实就是将用户传过来的字符串去做路由匹配
只是不知道用 Rails 该怎么实现?
#5 楼 @Insub 如果像 p_id, m_id 这些 param 是有规律的话那好说,举个例子,可以把路径转换成正则表达式,然后按顺序一个个 match,看能不能匹配。但是这样得考虑一下性能。
str = '/publishers/:p_id/magazines/:m_id/photos/:p1_id'.gsub(/:(\w+_id)/, '(?<\1>.*?)')
#=> /publishers/(?<p_id>.*?)/magazines/(?<m_id>.*?)/photos/(?<p_id>.*?)
reg = Regexp.new(str)
reg.match('/publishers/15/magazines/3/photos/2')
#=> #<MatchData "/publishers/15/magazines/3/photos/" p_id:"15" m_id:"3" p1_id:"2">
@saiga 没有规律的,我现在想想,需求其实可以简化描述为这样:
允许每个用户建立自己的路由表
然后能判断某个 URL 形式的字符串,在这个用户的路由表里是否有可以匹配的路由规则
这个,该如何实现呢?
正常来讲不是应该一个 api 一个域名么。。不然怎么区分哪个 url 是哪个用户的,难道添加的时候先检查占用?所以直接开多个 Rails 应用,然后把数据库里的路由导入成 rails 路由就可以了。。。
另外我觉得这个 controller 怎么设计也还很多疑问,如果开应用成本太高,不一定要用 rails 的。。
我觉得这个需求跟 CMS 很类似,所以 LZ 可以先找一下 Ruby 社区的开源 CMS,看看它们的实现能否有所帮助。如果自己做的话,可以找一下有没有能在运行时动态增减路由规则的 router 组件,然后 mount 进 Rails。
其实直接在 rails route 里面把你 record 的路由全部写出来就好了 一旦有新的记录就重新 reload 一次,
match your_record_url, to: 'controller#action', via: :all, defaults: { id: record_id }
如果 试着去用Rails内置的route方法解析和查找记录
或许可以这样用
ps: 如果路由记录太多话 可能需要考虑其他办法
require 'singleton'
require 'action_dispatch'
require 'active_support/all'
class RequestRoutes
include Singleton
def match(path, method)
memos = simulate.simulate(path).try(:memos)
return nil if memos.blank?
memos.reverse.find { |memo| memo[:request_method] == method }
end
def add_route(api_request)
append_to_ast(api_request)
clear_cache
end
def clear_cache
@simulate = nil
end
def reload
@ast = nil
@simulate = nil
end
private
def simulate
@simulate ||= begin
builder = ActionDispatch::Journey::GTG::Builder.new ActionDispatch::Journey::Nodes::Or.new ast
table = builder.transition_table
ActionDispatch::Journey::GTG::Simulator.new table
end
end
def ast
@ast ||= []
# 这里可以把你所有的记录全部转换成ast
# YourRecord.all.map do |api_request|
# parse_to_nodes(api_request)
# end
end
def parse_to_nodes(api_request)
memo = {
request_id: api_request[:id],
request_method: api_request[:method],
pattern: ActionDispatch::Journey::Path::Pattern.from_string(api_request[:url])
}
nodes = parser.parse (api_request[:url])
nodes.each { |n| n.memo = memo }
nodes
end
def append_to_ast(api_request)
@ast << parse_to_nodes(api_request)
end
def parser
ActionDispatch::Journey::Parser.new
end
end
request = {
id: 1,
method: 'GET',
url: '/publishers/:p_id/magazines/:m_id/photos/:p_id'
}
RequestRoutes.instance.add_route(request)
memo = RequestRoutes.instance.match('/publishers/1/magazines/2/photos/1', 'GET')
match_date = memo[:pattern].match('/publishers/1/magazines/2/photos/1')
match_date.names.zip(match_date.captures).to_h
@angelfan 非常感谢! 我现在是每次有用户请求进来,把该用户所有 api record 循环建立 route,然后匹配,类似于你说的:试着去用 Rails 内置的 route 方法解析和查找记录
不过我用的是第三方的 route 库,因为没有找到 rails 内置的新建 route 并匹配的方法...也因为不想污染 rails 项目本身的 route