You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Ruby on Rails 6.0中基于XML构建Service资源及关联模型方案咨询

看起来你已经用Nokogiri搭好了基础的XML读取逻辑,但想要更像ActiveRecord那样优雅地管理嵌套的Service、Request、Choice这些资源对吧?下面我给你一套可行的方案,既能封装重复的XML操作,又能定义属性和关联关系:

1. 构建通用XML模型基类

先创建一个XmlRecord基类,把解析、保存、查找这些通用逻辑封装起来,替代ActiveRecord::Base的角色。它会处理Nokogiri的底层操作,让具体的资源类只需要关注自己的属性和关联。

# app/models/xml_record.rb
class XmlRecord
  class << self
    # 定义XML文件路径(子类需要重写)
    def xml_path
      raise NotImplementedError, "Subclasses must define xml_path"
    end

    # 加载整个XML文档
    def xml_doc
      @xml_doc ||= Nokogiri::XML(File.open(xml_path))
    end

    # 查找所有符合类型的Tag节点
    def all(type:)
      xml_doc.xpath("//Tag[Type='#{type}']").map { |node| new(node) }
    end

    # 根据ID查找节点
    def find(id)
      node = xml_doc.xpath("//Tag[ID='#{id}']").first
      node ? new(node) : nil
    end
  end

  attr_reader :node

  def initialize(node)
    @node = node
    load_attributes
  end

  # 加载节点属性(子类重写映射规则)
  def load_attributes
    raise NotImplementedError, "Subclasses must define load_attributes"
  end

  # 获取子节点文本值的快捷方法
  def text_for(xpath)
    node.xpath(xpath).text.strip
  end
end

2. 定义具体的资源类

接下来针对Service、Request、Choice分别创建子类,继承XmlRecord,定义各自的属性映射和关联关系。

Service类

对应XML里Type="Service"的Tag:

# app/models/service.rb
class Service < XmlRecord
  def self.xml_path
    "xml/eidos/LeftTree/Services.xml"
  end

  attr_accessor :id, :name, :image_index, :requests

  def load_attributes
    @id = text_for("ID")
    @name = text_for("Name")
    @image_index = text_for("ImageIndex").to_i
    @requests = Request.where(parent_id: id) # 关联子Request
  end
end

Request类

对应Type="Request"的Tag,并且关联父Service:

# app/models/request.rb
class Request < XmlRecord
  def self.xml_path
    "xml/eidos/LeftTree/Services.xml"
  end

  def self.where(parent_id:)
    # 根据父ID匹配子节点(ID包含父ID前缀)
    xml_doc.xpath("//Tag[Type='Request' and starts-with(ID, '#{parent_id}/')]").map { |node| new(node) }
  end

  attr_accessor :id, :name, :image_index, :choices

  def load_attributes
    @id = text_for("ID")
    @name = text_for("Name")
    @image_index = text_for("ImageIndex").to_i
    @choices = Choice.where(parent_id: id) # 关联子Choice
  end
end

Choice类

对应Type="Choice"的Tag:

# app/models/choice.rb
class Choice < XmlRecord
  def self.xml_path
    "xml/eidos/LeftTree/Services.xml"
  end

  def self.where(parent_id:)
    xml_doc.xpath("//Tag[Type='Choice' and starts-with(ID, '#{parent_id}/')]").map { |node| new(node) }
  end

  attr_accessor :id, :name, :image_index, :multiplicity_min, :multiplicity_max, :required

  def load_attributes
    @id = text_for("ID")
    @name = text_for("Name")
    @image_index = text_for("ImageIndex").to_i
    @multiplicity_min = text_for("Multiplicity/MinOccurrence")
    @multiplicity_max = text_for("Multiplicity/MaxOccurrence")
    @required = text_for("Required") == "True"
  end
end

3. 简化控制器逻辑

现在你的控制器代码可以像用ActiveRecord一样简洁了,不用再写重复的Nokogiri解析:

# app/controllers/services_controller.rb
class ServicesController < ApplicationController
  def index
    @services = Service.all(type: "Service")
  end

  def show
    @service = Service.find(params[:id])
    # 自动加载了@service.requests,每个request又自动加载了choices
  end
end

4. 扩展:添加保存逻辑(如果后续需要编辑)

如果之后需要支持修改XML,你可以在XmlRecord基类里添加保存方法,比如:

# 在XmlRecord类中添加
def save
  # 更新节点内容的逻辑,比如:
  node.xpath("Name").first.content = @name
  File.write(self.class.xml_path, node.document.to_xml(indent: 2))
  true
rescue => e
  Rails.logger.error "Failed to save XML: #{e.message}"
  false
end

然后在具体子类里可以重写save方法,处理自己的属性更新。

这样一来,你就有了一套类似ActiveRecord的模型层,把XML操作完全封装起来,控制器只需要和模型交互,嵌套的关联关系也能轻松管理。

内容的提问来源于stack exchange,提问作者Paolo Di Pietro

火山引擎 最新活动