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






4.67/5 (26投票s)
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 文件视为类似于数据库处理的工具是方便的。