65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Insert/Update/Delete/Query 语句访问 XML

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (26投票s)

2005年4月12日

CPOL

6分钟阅读

viewsIcon

144637

downloadIcon

3315

XML 的伪数据库。

引言

最近,我正在对 Advanced Unit Test WinForm runner 应用程序进行一些更改,当时我想为测试树中的每个节点保存一些状态信息。我认为 XML 是存储这些信息的不错格式,因为它像树一样是分层的。在思考这个问题时,我决定操作 XML Document 并不那么有趣,而在其他应用程序中更方便、可能更容易使用的是一种工作方式更像数据库交互,具有插入、更新、删除和查询功能的东西。但实际上,似乎这样的东西应该已经被实现了。

Paul Wilson 的 XmlDataProvider

在我搜索的过程中,我发现了 Paul Wilson (以 WilsonORMapper 闻名) 的一个 XmlDataProvider。你可以在 他的博客 上阅读他的作品,并 在这里 下载他的项目。它绝对值得一看,我没有使用它的唯一原因是底层 XML 实现使用了 DataSet,因此你会得到 DataSet 的 XML 格式。

为什么我不喜欢 DataSet

DataSet 提供了各种强大的功能,如错误检测等,但它的 XML 格式很难看。如果我创建一个带有 C1、C2、C3 列的表的几行,你会得到这样的结果:

<?xml version="1.0" standalone="yes"?>
<NewDataSet>
<Table1>
<C1>1</C1>
<C2>2</C2>
<C3>3</C3>
</Table1>
<Table1>
<C1>4</C1>
<C2>5</C2>
<C3>6</C3>
</Table1>
</NewDataSet>

而我真正想要的是类似这样的东西:

<?xml version="1.0" standalone="yes"?>
<Database>
<Table1>
<Row C1="1" C2="2" C3="3"/>
<Row C1="4" C2="5" C3="6"/>
</Table1>
</Database>

更重要的是,我的要求是能够在不被特定格式约束的情况下操作 XML,就像 DataSet 序列化器那样。如果我使用 XmlDataProvider,我将被限制在处理由 DataSet 序列化的 XML。因此,我开始考虑编写一个 XmlDocument 类的轻量级包装器,它提供我期望的功能,而不会施加格式约束。

请注意

在某些情况下,处理通用 XML 可能会导致问题。例如,XML 的编写方式可能无法唯一标识单个行。在上面的 XML 示例中,考虑如何更新第二行的“4”到“5”的值。如果没有任何信息可以唯一标识该行,那么我的解决方案将最终更新所有行的 C1 属性。

一些有趣的优势

当 XML 作为数据库运行时,你可以做一些有趣的事情。XML 是分层的,而数据库是关系的。如果合适,你可以使用 XML 来维护多个数据库。现在想想你可以利用这种层次结构做什么。你可以创建同一数据库的不同版本。

<?xml version="1.0" standalone="yes"?>
<Database>
  <Version1>
    <Table1>
      <Row C1="1" C2="2" C3="3"/>
      <Row C1="4" C2="5" C3="6"/>
    </Table1>
  </Version1>
</Database>

你也可以在数据库中创建同一表的不同版本。

<?xml version="1.0" standalone="yes"?>
<Database>
  <Table1>
    <Version1>
      <Row C1="1" C2="2" C3="3"/>
      <Row C1="4" C2="5" C3="6"/>
    </Version1>
  </Table1>
</Database>

这很有趣!或者你可以根据用户维护不同的数据集。

<?xml version="1.0" standalone="yes"?>
<Database>
  <Table1>
    <MarcData>
      <Row C1="1" C2="2" C3="3"/>
      <Row C1="4" C2="5" C3="6"/>
    </MarcData>
    <KarenData>
      <Row C1="11" C2="22" C3="33"/>
      <Row C1="44" C2="55" C3="66"/>
    </KarenData>
  </Table1>
</Database>

很多时候,我需要这种功能来处理少量数据。

非常有限的用途

抛开这些炫酷的功能,请记住,将 XML 用作数据库的用途非常有限。索引、排序、分组、表连接、存储过程以及所有其他有用的功能和性能驱动的功能都不存在。这并不意味着它不能做到,但它肯定不是我认为有用的。为工作选择合适的工具。在某些情况下,我这里提出的实现可能是用于简单的配置状态管理、维护不同的配置集或处理非常有限数据量的合适工具。

示例用法

我使用这个工具来保存单元测试运行器中树视图的勾选和展开状态。为了保存这些值,我递归遍历 TreeNode

XmlDatabase config=new XmlDatabase("Config");
string path=StringHelpers.LeftOf(tvUnitTests.Nodes[0].Text, ',');
config.Insert(path);
foreach(TreeNode node in tvUnitTests.Nodes[0].Nodes)
{
  WriteConfig(config, node, path);
}

...

private void WriteConfig(XmlDatabase config, TreeNode node, string path)
{
  string newPath=path+"/"+node.Text;
  config.Insert(newPath,
  new XmlDatabase.FieldValuePair[]
  {
    new XmlDatabase.FieldValuePair("Checked", node.Checked.ToString()),
    new XmlDatabase.FieldValuePair("Expanded", node.IsExpanded.ToString())
  });
  foreach(TreeNode child in node.Nodes)
  {
    WriteConfig(config, child, newPath);
  }
}

同样,为了读取上次勾选和展开的状态,在树加载后,我会再次遍历配置。请注意,我测试 XML 数据库中是否存在记录,因为树可能已更改。

private void LoadAssemblyConfig()
{
  XmlDatabase config=new XmlDatabase("Config");
  string path=StringHelpers.LeftOf(tvUnitTests.Nodes[0].Text, ',');
  tvUnitTests.CollapseAll();
  tvUnitTests.Nodes[0].Expand();
  foreach(TreeNode node in tvUnitTests.Nodes[0].Nodes)
  {
    ReadConfig(config, node, path);
  }
}

private void ReadConfig(XmlDatabase config, TreeNode node, string path)
{
  string newPath=path+"/"+node.Text;

  string isChecked=config.QueryScalar(newPath, "Checked");
  if (isChecked != null)
  {
    node.Checked=Convert.ToBoolean(isChecked);
  }

  string isExpanded=config.QueryScalar(newPath, "Expanded");
  if (isExpanded != null)
  {
    if (Convert.ToBoolean(isExpanded))
    {
      node.Expand();
    }
  }

  foreach(TreeNode child in node.Nodes)
  {
    ReadConfig(config, child, newPath);
  }
}

以测试夹具本身为例,树配置文件的外观如下:

<?xml version="1.0" encoding="utf-8"?>
<Config>
  <Marc.XmlConfigurationManagement>
    <XmlConfigUnitTests Checked="True">
      <UnitTests Checked="True" Expanded="True">
        <Setup Checked="True" Expanded="False"/>
        <InsertRecord Checked="True" Expanded="False"/>
        <InsertRecordWithValue Checked="True" Expanded="False"/>
        <InsertRecordWithMultipleValues Checked="True" Expanded="False"/>
        <UpdateField Checked="True" Expanded="False"/>
        <DeleteField Checked="True" Expanded="False"/>
        <InsertMultipleUniqueRecords Checked="True" Expanded="False"/>
        <QueryUniqueRecords Checked="True" Expanded="False"/>
        <DeleteUniqueRecord Checked="True" Expanded="False"/>
        <VerifyMissingRecordNullReturn Checked="True" Expanded="False"/>
        <UpdateMultipleRows Checked="True" Expanded="False"/>
        <DeleteMultipleRows Checked="True" Expanded="False"/>
        <MultiRowMultiFieldUpdate Checked="True" Expanded="False"/>
        <MultirowQuery Checked="True" Expanded="False"/>
        <MultiRowQueryOfSingleField Checked="True" Expanded="False"/>
        <MultiRowQueryOfMultipleFields Checked="True" Expanded="False"/>
        <Save Checked="True" Expanded="False"/>
      </UnitTests>
    </XmlConfigUnitTests>
  </Marc.XmlConfigurationManagement>
</Config>

实现

插入操作

插入路径是一个递归过程。如果指定路径中的任何子节点不存在,它将自动创建。例如,如果你调用基本的 Insert 方法,

public XmlNode Insert(string path)
{
  if (path==null)
  {
    throw(new ArgumentNullException("path cannot be null."));
  }

  string path2=rootName+"/"+path;
  string[] segments=path2.Split('/');
  XmlNode lastNode=InsertNode(xdoc, segments, 0);
  return lastNode;
}

它会调用一个受保护的方法,该方法会递归遍历路径,根据需要创建 XML 节点。

protected XmlNode InsertNode(XmlNode node, string[] segments, int idx)
{
  XmlNode newNode=null;

  if (idx==segments.Length)
  {
    // All done.
    return node;
  }

  // Traverse the existing hierarchy but ensure that we create a 
  // new record at the last leaf.
  if (idx+1 < segments.Length)
  {
    foreach(XmlNode child in node.ChildNodes)
    {
      if (child.Name==segments[idx])
      {
        newNode=InsertNode(child, segments, idx+1);
        return newNode;
      }
    }
  }
  newNode=xdoc.CreateElement(segments[idx]);
  node.AppendChild(newNode);
  XmlNode nextNode=InsertNode(newNode, segments, idx+1);
  return nextNode;
}

更新操作

更新操作是对 XPath 路径限定的所有节点进行的。

public int Update(string path, string field, string val)
{
  if (path==null)
  {
    throw(new ArgumentNullException("path cannot be null."));
  }
  if (field==null)
  {
    throw(new ArgumentNullException("field cannot be null."));
  }
  if (val==null)
  {
    throw(new ArgumentNullException("val cannot be null."));
  }

  XmlNodeList nodeList=xdoc.SelectNodes(rootName+"/"+path);
  foreach(XmlNode node in nodeList)
  {
    node.Attributes[field].Value=val;
  }
  return nodeList.Count;
}

如果你指定一个“where”子句(作为 XPath 表达式),它将自动作为路径的限定符添加。

public int Update(string path, string where, string field, string val)
{
  ...
  return Update(path+"["+where+"]", field, val);
}

你必须自己将 XPath 语句中的任何限定符编码到非最后一个子节点的节点中。

删除操作

删除操作与更新类似——你可以删除 XPath 路径限定的所有节点,或者你可以包含一个“where”子句来过滤限定的节点。

public int Delete(string path)
{
  if (path==null)
  {
    throw(new ArgumentNullException("path cannot be null."));
  }

  XmlNodeList nodeList=xdoc.SelectNodes(rootName+"/"+path);
  foreach(XmlNode node in nodeList)
  {
    node.ParentNode.RemoveChild(node);
  }
  return nodeList.Count;
}

查询操作

查询操作分为两类:标量和向量。标量查询返回一个单一的字符串,表示特定记录的特定属性的值。

标量查询

典型的实现如下所示:

public string QueryScalar(string path, string field)
{
  if (path==null)
  {
    throw(new ArgumentNullException("path cannot be null."));
  }
  if (field==null)
  {
    throw(new ArgumentNullException("field cannot be null."));
  }

  string ret=null;
  XmlNode node=xdoc.SelectSingleNode(rootName+"/"+path);
  if (node != null)
  {
    XmlAttribute xa=node.Attributes[field];
    if (xa != null)
    {
      ret=xa.Value;
    }
  }
  return ret;
}

查询可以使用“where”子句进一步限定,或者直接将限定信息嵌入到 XPath 字符串本身。

向量查询

向量查询将返回一个 DataTable 实例。DataTable 的列初始化为属性名。如果你查看代码,你会发现它使用第一个返回的记录来创建列集合。因此,如果其他行具有不同的属性,此方法将失败。

public DataTable Query(string path)
{
  if (path==null)
  {
  throw(new ArgumentNullException("path cannot be null."));
  }

  DataTable dt=new DataTable();
  XmlNodeList nodeList=xdoc.SelectNodes(rootName+"/"+path);
  if (nodeList.Count != 0)
  {
    CreateColumns(dt, nodeList[0]);
  }
  foreach(XmlNode node in nodeList)
  {
    DataRow dr=dt.NewRow();
    foreach(XmlAttribute attr in node.Attributes)
    {
      dr[attr.Name]=attr.Value;
    }
    dt.Rows.Add(dr);
  }
  return dt;
}

其他重载允许你指定要在 DataTable 中填充的特定列。

数组查询

有一个特殊的查询,它返回所有限定行的特定字段(即 XML 属性)的值。它不返回 DataTable,而是返回一个字符串数组。

public string[] QueryField(string path, string field)
{
  if (path==null)
  {
    throw(new ArgumentNullException("path cannot be null."));
  }
  if (field==null)
  {
    throw(new ArgumentNullException("field cannot be null."));
  }

  XmlNodeList nodeList=xdoc.SelectNodes(rootName+"/"+path);
  string[] s=null;
  if (nodeList.Count != 0)
  {
    s=new string[nodeList.Count];
    int i=0;
    foreach(XmlNode node in nodeList)
    {
      s[i++]=node.Attributes[field].Value;
    }
  }
  return s;
}

我使用它作为一种便利,而不是为了多行查询单个字段而必须处理 DataTable

单元测试

我的 AUT 测试运行器编写了各种单元测试。测试按顺序运行,每个测试都测试由前一个测试建立的数据的进一步功能。我发现以这种方式执行某些类型的测试要容易得多,因为我不必为每个测试设置数据库——之前的测试为我完成了设置。

文档

代码文档齐全,下载包含 NDoc 参考。

结论

这个想法是为了通过创建我们熟悉的数据库世界和 XPath 语句之间的混合体,并且无需处理 XmlDocument 类的内部工作原理,从而使使用 XML 文档更加方便。我也不喜欢 DataSet 的 XML 序列化,因为我想要一些能够处理各种 XML 格式的东西,而不是被强加于我。我还想保持简单——将 XML 用作数据库是荒谬的,但对于某些配置信息来说,拥有一个将 XML 文件视为类似于数据库处理的工具是方便的。

© . All rights reserved.