Rails 如何将一个 URL 字符串去匹配一个数据库里的路由规则?

Insub · 2017年02月10日 · 最后由 Insub 回复于 2017年02月16日 · 2720 次阅读

是这样的,我们准备提供一个服务,允许用户在我们的项目上搭建自己的 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 方法解析和查找记录,但是没有头绪无从下手

match '/publishers/*'

@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 的内存。要写出用在生产环境的代码有点难,所以嘛....

看看楼下有没有解决方案.... 😅

不知道路由支不支持 method_missing,支持的话直接使用这个方法就好了,然后在里面去搞事

@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 没有规律的,我现在想想,需求其实可以简化描述为这样:

  1. 允许每个用户建立自己的路由表

  2. 然后能判断某个 URL 形式的字符串,在这个用户的路由表里是否有可以匹配的路由规则

这个,该如何实现呢?

正常来讲不是应该一个 api 一个域名么。。不然怎么区分哪个 url 是哪个用户的,难道添加的时候先检查占用?所以直接开多个 Rails 应用,然后把数据库里的路由导入成 rails 路由就可以了。。。

另外我觉得这个 controller 怎么设计也还很多疑问,如果开应用成本太高,不一定要用 rails 的。。

Rei [该话题已被删除] 提及了此话题。 02月11日 14:45

我觉得这个需求跟 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

#7 楼 @Insub 直接把‘格式'写到数据库里呢?postgresql 支持正则表达式查询,SIMILAR TO

@jun1st 用的 mysql...

需要 登录 后方可回复, 如果你还没有账号请 注册新账号