XMeLon Schema






4.81/5 (9投票s)
另一种将XML转换为数据库的方法
介绍
基于XML的应用程序的一个问题是,为了访问文件中的数据,通常需要开发一个特定的解析器。即使有大量的库和工具可以生成解析器的大部分代码,但所需的工作量仍然很高,而且当然,每次模式发生变化或扩展时,都必须维护解析器。此外,典型的两种方法,SAX和DOM,都有其固有的且无法解决的局限性。总之,为每种XML模式开发一个解析器可能是一种昂贵、不灵活且受限的解决方案。
将数据方便地存储在数据库中可以是一个不错的选择,特别是对于那些包含记录形式结构化数据的XML文件。在本文中,我们将解释一个能够存储任何XML文件的数据库模式,以及如何使用它来系统地从其内容中获取特定模式或表,我们称之为XMeLon模式。此外,还提供并解释了一个用于生成任何XML文件的XMeLon数据库的解析器(一次性解析器)。
该模式最初是为XML问题开发的,但事实上,它是一个通用且灵活的数据库模式,可用于许多其他应用程序。仅举一个可能的用途,JSON数据也可以适应XMeLon模式。
XMeLon模式的目的
我们希望构建一个系统,将XML文件中包含的所有信息放入SQL数据库。当然,不是仅仅作为原始数据,而是以某种方式将它们组织成程序可以查询和方便使用“逻辑”表。它不必涵盖或支持XML提供的所有方面,但主要侧重于提取和正确存储可存储为表记录的XML文件的数据。
除了这个考虑之外,我们希望系统能够接受多个文件,即使是具有不同XML模式的文件,并且只使用XML文件作为输入。也就是说,不需要对XML模式进行任何配置或额外定义,逻辑结构将从内容中推断出来。此外,不仅可以提取内容表的模式,还可以提取结果表之间的关系。最后一条信息可以被系统用于极大地简化对生成数据库的查询。一种自动连接SQL表的概念在文章The Deep Table中有解释和实现。
例如,如果我们解析文章中提供的示例XML,我们希望获得“shell (name, owner, ..)”和“shell_content_cd (title, author, year, ..)”表以及它们之间的关系,以便可以建立这两个表的连接。
XMeLon方法分两个阶段进行,每个阶段使用不同类型的模式。首先,解析器生成一个包含XML内容的“基本模式”数据库。然后,从基本模式数据库中,可以系统地生成内容模式,该模式反映了特定的XML结构。最后,应用程序可以通过使用SQL Select语句查询数据库来访问整个XML内容。
XML的维度(基本模式)
通过基本模式,我们希望将任何XML内容存储在一个表或少数几个定义良好的表中。因此,我们希望发现XML作为容器有多少个维度。第一个(而且肯定是仓促的)尝试是观察XML只包含文本,这些文本属于节点信息(路径)或数据,然后我们可以将我们的示例数据表示为
就好像XML只有两个维度,但这并不真实。但在寻找缺失的XML维度之前,让我们更方便地排列路径,以便以后更容易找到我们感兴趣的表和维度,即内容模式中的那些。我们将路径分为两部分:除最后一个标签之外的路径,我们称之为层;最后一个标签,我们称之为维度(此时不是指XML维度)。我们甚至获取层的最后一个标签,并将其放在一个单独的列中。现在相同的数据看起来像
我们添加了冗余列,因此表仍然不完整。事实上,所示的示例是模棱两可的,不同的XML结构可能会产生相同的结果。例如,使用相同的数据但有三个shell而不是一个,来自XML:
<shell>
<name>oldies</name>
<owner>evariste</owner>
<contents> <cd> ... </cd> </contents>
</shell>
<shell>
<contents> <cd> ... </cd> </contents>
</shell>
<shell>
<contents> <cd> ... </cd> </contents>
</shell>
因此,不仅知道数据元素的路径很重要,而且知道它属于哪个“路径实例”也很重要。特别是我们有兴趣将层实例作为一个新列。它可以由一个计数器表示,该计数器在层更改时递增。通常每个层都有一个父层,我们可以将其存储在新列中。再次,相同的样本以及所有这些新列
现在,查看layerCounter
的值,可以清楚地知道每一块数据属于哪个CD。
到目前为止,我们还没有提到另外两个XML元素:属性和“自由文本”。属性可以被视为维度,所以例如这两种构造将产生基本相同的结果
<somenode myatt="my value">
<field2> XX </field2>
</somenode>
<somenode>
<myatt>my value</myatt>
<field2> XX </field2>
</somenode>
自由文本是指不直接包含在最终标签中的文本。XML允许这样做,但从记录的角度来看,它并不特别有趣。例如
<sometag>
SOME FREE TEXT
<afield>A text but data</afield>
MORE FREE TEXT
</sometag>
这些数据无论如何都会被存储,只是它会使用额外的字段dataOrigin
进行标记,该字段将反映所有这些情况。来自解析器源代码
DATA_PLACE_ATTRIBUTE = 'A'; // as attribute e.g. <a myAtt="data"> ... </a>
DATA_PLACE_VALUEATT = 'V'; // as "value" attribute e.g. <a myAtt="xyz">data</a>
DATA_PLACE_FREETEXT = 'X'; // unstructured text e.g. <a> data <b> ... </b> data </a>
DATA_PLACE_TAGVALUE = 'D'; // normal data e.g. <a> data </a>
这基本上就是XMeLon基本模式的精髓,这个理想化的——如果你愿意,可以说是天真的——模式只包含一个表。
CREATE TABLE xmelon_data (layerCounter, //instance or unique id for the layer
layerParentCounter, //parent's layer counter
layerFullName, //layer=XML path except the last tag
layerName, //last tag of the layer
dimensionName, //specific layer's dimension
dataOrigin, //origing of data: A, V, X or D
dataValue); //data value
为了能够存储多个文件以及其他几个技术原因,需要扩展该模式。尽管如此,我们仍然使用理想模式,这不仅是为了解释的清晰度,而且因为它在简单的情况下可能很有用。
在进入解析器(只需要基本模式)的详细信息之前,让我们完成理论部分,看看如何从已解析的XML内容构建内容模式。
表在哪里?(构建内容模式)
内容模式是反映XML文件中包含的记录的表。由于这些依赖于内容,因此在将所有数据存储到基本模式之后,在单独的过程中构建它们是有意义的。该模式可以使用几个SQL语句完全构建。
让我们从最简单的SQL语句开始,即提供内容表名称的语句。这些表就是我们定义的层,所以一个SELECT
及其结果可以如下
SELECT layerFullName AS tableName FROM xmelon_data GROUP BY layerName
tableName
-------------------
shelf
shelf/contents/cd
美化表名并获取所有列名也可以通过以下方式完成
SELECT "t"||REPLACE(layerFullName, '/', '_') AS tableName,
"c_"||dimensionName AS columName
FROM xmelon_data
WHERE layerCounter+0 > 0
GROUP BY layerFullName, dimensionName
tableName |columName
-------------------- |-----------
t_shelf |c_name
t_shelf |c_owner
t_shelf_contents_cd |c_author
t_shelf_contents_cd |c_title
t_shelf_contents_cd |c_year
这可以用于创建所有空内容表,但也可以用于更有趣的事情:为每个表生成一个SQL语句,该语句一次返回所有记录。这实际上是XMeLon方法的关键步骤。如果没有这种可能性,基本模式数据库本身将只是一个原始容器,并且提取内容并以结构化方式呈现的目标将无法实现。
我们正在寻找的记录包含在每个层实例中,因此按layerCounter
对特定层(=内容表)的xmelon_data表进行分组,将得到与内容表具有的记录数量相同的行。现在,在这个组内,我们必须找到所有列的值。为此,我们为每个维度应用以下SQL表达式
MAX (SUBSTR (value, 1, LENGTH(value) * (dimensionName = "xxxxxxx"))) AS c_xxxxxxx
其中xxxxxxx必须替换为维度名称。也许这不是唯一的解决方案,但由于它表现得很好(我必须承认:令我惊讶!),所以该表达式一直保留至今。构建shell_conten_cd表的完整SQL是
SELECT
layerCounter,
parentLayerCounter,
MAX (SUBSTR (value, 1, LENGTH(value) * (dimensionName = "author"))) AS c_author,
MAX (SUBSTR (value, 1, LENGTH(value) * (dimensionName = "title"))) AS c_title,
MAX (SUBSTR (value, 1, LENGTH(value) * (dimensionName = "year"))) AS c_year
FROM xmelon_data
WHERE layerFullName = '/shelf/contents/cd'
GROUP BY layerCounter ;
layerCounter|parentLayerCounter|c_author |c_title |c_year
----------- |------------------|-------------|----------------------------|--------
5 |1 |Bob Dylan |Blood on the tracks |
8 |1 |David Bowie |The man who sold the world |1971
12 |1 |Pau Riba |Dioptria |1971
现在,如果我们对“CREATE TABLE ... AS <select>
”或“CREATE VIEW ... AS <select>
”应用select,我们可以直接创建内容表或仅为其创建一个视图,这对于应用程序来说更方便。使用视图,内容模式对新内容或更新内容保持更灵活,并且表可能性能更高,因为表可以被索引。
此时,我们能够生成所有内容表并填充数据。这不算差,但仍然可以挤出更多,或者在我们的例子中,挤出xmelon_data。只需一个SQL SELECT
,我们就可以获得所有内容表之间的父子关系。如开头所述,文章The Deep Table提供了更多关于如何使用此信息自动生成有用的SELECT
join的详细信息。
要获得连接,只需使用layerCounter
和layerParentCounter
将xmelon_data与自身连接即可,如下所示
SELECT
REPLACE(parent.layerName, '/', '') AS connName,
'v'||REPLACE(parent.layerFullName, '/', '_') AS tableSource,
'parentLayerCnt' AS fieldSource,
'v'||REPLACE(child.layerFullName, '/', '_') AS tableTarget,
'layerCnt' AS fieldTarget
FROM
xmelon_data AS child, xmelon_data AS parent
WHERE parent.layerCounter+0 > 0 AND
parent.layerCounter+0 = child.parentLayerCounter+0
GROUP BY child.layerFullName
ORDER BY tableSource, connName ;
connName |tableSource |fieldSource |tableTarget |fieldTarget
---------|-------------|---------------|---------------------|------------
shelf |v_shelf |parentLayerCnt |v_shelf_contents_cd |layerCnt
对于最终的基本模式,此SQL更大,因为文件ID必须包含在所有连接中作为键字段,但它仍然可以在一个select中完成。
最后一点是,“layerName”字段可能根据要处理的XML模式有更多用途。例如,在许多情况下,它可以替代上述所有内容模式SQL中的layerFullName。当XML模式支持结构组合时,这尤其有用。这是XMeLon模式的一个特殊且更高级的用法,我们在此不予介绍。
XMeLon解析器
XMeLon解析器将从一个或一组XML文件创建并填充基本模式。它之所以是通用的,是因为它不依赖于XML模式,因此只需要开发一次。我们为理想的基本模式和最终模式都提供了解析器,但仅为第一个解析器提供更详细的解释。无论如何,使用某些diff工具检查两种解析器之间的差异应该足以理解最终解析器。
它是用Java开发的,因为该语言默认提供了许多有用的库。例如,我们的实现使用了SAX解析器org.xml.sax.XMLReader
。
解析器的主要类是xmelonIdealParser
,它有一个XMLReader
类型的对象将读取XML文件并负责其语法有效性。构造函数初始化它,如下面的代码所示,而parseFile
方法调用其parse方法。从此时开始,XMLReader
对象将读取并识别XML元素和数据,它将
- 每次打开标签时调用回调方法
startElement
,并提供任何属性 - 每次找到数据时调用回调方法
characters
- 每次关闭标签时调用回调方法
endElement
这些是我们解析器必须实现的org.xml.sax.ContentHandler
Java接口中的方法。此外,我们还扩展了org.xml.sax.DefaultHandler
类,以便对其他必需的实现由默认完成,并实现了EntityResolver
接口,以避免在XML文件需要此功能时出现某些异常,但对于我们的目的,resolveEntity
的dummy实现已经足够了。
与XMLReader
相关的代码是
import javax.xml.parsers.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
public class xmelonIdealParser extends DefaultHandler implements ContentHandler, EntityResolver
{
...
private XMLReader saxParserReader = null; // SAX reader from java library
public xmelonIdealParser ()
{
try
{
// prepare it as XML reader
saxParserReader = ( (SAXParserFactory.newInstance ()).newSAXParser () ).getXMLReader();
saxParserReader.setContentHandler (this);
saxParserReader.setEntityResolver (this);
}
catch (Exception e) ; //LOG SEVERE getting a new saxParserReader ...
}
public void parseFile (String fileName)
{
...
try
{
saxParserReader.parse (new org.xml.sax.InputSource (fileName));
}
catch (Exception e) ; //ERROR while parsing XML file fileName ...
}
//methods that has to be implemented
//
public void startElement (String namespace, String localname,
String type, Attributes attributes) throws SAXException
public void endElement (String namespace, String localname, String type) throws SAXException
public void characters (char[] ch, int start, int len)
public InputSource resolveEntity (String publicId, String systemId)
在查看XMLReader
回调(startElement
等)的实现之前,我们需要另外两个类。正如在基本模式中看到的,我们希望将路径分离为层和维度,并跟踪所有层的实例和父实例。所有这些都在解析器中通过使用一个包含我们所需信息的XML元素(标签)堆栈来实现。将代表我们所需的XML元素的类是xmelonSaxElement
。
public class xmelonSaxElement
{
public xmelonSaxElement ()
{
}
public xmelonSaxElement (String pTagName, long pLayerCounter, long pLayerParentCounter)
{
tagName = pTagName;
layerCounter = pLayerCounter;
layerParentCounter = pLayerParentCounter;
}
public String tagName = "?";
public long layerCounter = 0;
public long layerParentCounter = 0;
public boolean hasData = false;
public boolean hasAttribute = false;
}
堆栈中的特定xmelonSaxElement
将代表一个特定的层实例,因此除了我们在构造时提供的tagName之外,我们还有层计数器和父层计数器。Sax元素还将跟踪是否在层实例中存储了记录或属性,特别是第一个标志很重要,可以避免创建仅包含链接信息的伪造内容表,这在XML中由于节点的冗长而非常常见。
然后,xmelonSaxElementStack
类只是一个xmelonSaxElement
元素的堆栈,提供典型的push
、pop
、size
、elementAt
方法以及另外两个方法,用于根据堆栈中的元素构建layerFullName和layerName。
利用这个堆栈的信息和三个额外值:dimensionName、dataOrigin和dataValue,我们就可以存储xmelon_data的记录了。这是通过outData
方法完成的。与SQL输出相关的方法是
private xmelonSaxElementStack eleStack; // stack of xmelonSaxElement
private void out (String str)
{
System.out.println (str);
}
protected void startSQLScript ()
{
out ("CREATE TABLE xmelon_data (layerCounter,
parentLayerCounter, layerName, dimensionName, valueOrigin, value);");
out ("INSERT INTO TABLE xmelon_data VALUES (0, 0, '/', '', '', '', '');");
out ("BEGIN TRANSACTION;");
}
private void outData (String dimensionName, char dataPlace, String value)
{
xmelonSaxElement last = eleStack.lastElement ();
last.hasData = true; // we are storing a record in this layer, therefore it has data
// insert new data in table xmelon_data
//
out ("INSERT INTO xmelon_data VALUES ("
+ last.layerCounter + ", " // layerCounter
+ last.layerParentCounter + ", '" // layerParentCounter
+ eleStack.getFullLayerName () + "', '" // layerFullName
+ eleStack.getLayerName () + "', '" // layerName
+ dimensionName + "', '" // dimensionName
+ dataPlace + "', '" // valueOrigin
+ strUtil.escapeStr(value) + "');" // value
);
}
protected void endSQLScript ()
{
out ("COMMIT TRANSACTION;");
}
解析器仅输出用于创建和填充数据库的SQL语句。输出可以重定向到文件,并用作SQL引擎的输入,在我们的例子中是SQLite。或者简单地在一个命令行中完成所有操作,例如
java -cp . xmelonIdealParser myfile.xml | sqlite3 myXmelon.db
这将解析XML文件myfile.xml并从中生成SQLite数据库myXmelon.db。
使用SQLite SQL引擎不仅有用且非常方便,因为它免费、开源且属于公共领域,而且还因为它 the typeless concept(无需定义每个字段类型)允许我们只专注于SQL中真正具有生产力的方面。而且,我们最终得到的数据库模式不会对字段允许的值有任意限制,这很难有用。
现在让我们检查回调方法。startElement
方法基本上是将一个新的xmelonSaxElement
推入堆栈,并存储任何属性。
public void startElement (String namespaceURI, String localname,
String qName, Attributes attributes) throws SAXException
{
lastWasClosingTag = false;
storeTextDataIfAny ();
// push element stack
xmelonSaxElement xElem = new xmelonSaxElement(qName,
(currentLayerCounter ++),
getLayerParentCounter ());
xElem.hasAttribute = attributes.getLength () > 0;
eleStack.pushElement (xElem);
if (attributes.getLength () > 0)
for (int ii = 0; ii < attributes.getLength (); ii ++)
outData (attributes.getQName (ii),
DATA_PLACE_ATTRIBUTE,
attributes.getValue (ii));
}
characters
方法只是将数据添加到成员变量中。
public void characters (char[] ch, int start, int len)
{
currentStrData += (new String (ch, start, len)).trim ();
}
最后,endElement
方法基本上存储元素的,对“匿名字段”进行特殊检查。例如,这会出现在XML结构中
<sometag att1="valatt1">
some data
</sometag>
sometag是一个层,因为它在属性att1中给出了一个维度,但是some data是什么?它没有维度名称。解析器将其视为一个匿名字段,并为其赋予维度名称“sometag_value”。
只有在“正常”情况下,当字段在显式标签内给出时,才必须先弹出堆栈以存储值,以便它位于层位置。
public void endElement (String namespace, String localname, String type) throws SAXException
{
if (lastWasClosingTag)
{
storeTextDataIfAny ();
eleStack.popElement ();
}
else
{
if (currentStrData.length () > 0) // has value
{
if (eleStack.lastElement ().hasAttribute)
{
// as "tag_value" e.g. <tag myAtt="xyz">data</tag>
outData (type + "_value", DATA_PLACE_VALUEATT, currentStrData);
eleStack.popElement ();
}
else
{
// normal data <tag> data </tag>
eleStack.popElement ();
outData (type, DATA_PLACE_TAGVALUE, currentStrData);
}
}
else eleStack.popElement ();
}
currentStrData = "";
lastWasClosingTag = true;
}
自由文本的情况在startElement
和endElement
方法中都会被检查。让我们重复我们的自由文本示例
<sometag>
SOME FREE TEXT
<afield>A text but data</afield>
MORE FREE TEXT
</sometag>
在开始<afield>时,我们有一些数据,因此它是自由文本。当结束</sometag>时,我们有一些数据,并且最后的操作也是关闭了一个标签(</afield>),因此数据是自由文本。
最终的XMeLon模式和解析器
现在我们将开发基于理想模式的最终XMeLon基本模式。
为了支持多个输入文件,我们在文件源表中添加了一个新表,并在xmelon_data表中添加了一个新的fileID列来引用数据所属的文件。
CREATE TABLE xmelon_files (fileID, timeParse, fullPath);
CREATE TABLE xmelon_data (fileID, layerCounter, ...);
层和维度的名称,特别是层的名称,可能很长,重复存储它们会效率低下。此外,这些名称可能包含标点符号和其他符号,这些符号在表名或列名中是不被接受的,因此我们必须先将它们转换为有效的名称。我们可以通过添加另外两个表来建模此行为,一个用于维度,一个用于层,并在xmelon_data中用对表的引用替换其值。所以最终完整的XMeLon基本模式是
CREATE TABLE xmelon_files (fileID, //unique id of the file
timeParse, //date time when the file was parsed
fullPath); //full path of the file parsed
CREATE TABLE xmelon_layerDef (layerID, //layer's unique id
layerParentID, //layer's unique id of the parent layer
layerFullNameRaw, //layer full name as given in the xml file
layerFullName, //layer full name as feasible name
layerName); //layer last name as feasible name
CREATE TABLE xmelon_dimensionDef (dimensionID, //dimension's unique id
dimensionNameRaw, //dimension's name as given in the xml file
dimensionName); //dimension's name as feasible name
CREATE TABLE xmelon_data (fileID, //file unique id
layerCounter, //layer instance to which the data belongs
layerParentCounter, //layer instance of the parent layer
layerID, //reference to the layer in table xmelon_layerDef
dimensionID, //reference to the dimension in table xmelon_dimensionDef
dataOrigin, //data origin 'A' attribute, 'X' free text, 'V' tag value, 'D' data
dataValue); //data value
最后,为了避免我们的解析器中的特殊字符冲突以及支持Unicode,所有值和名称都将在数据库中进行编码。为此,使用java.net.URLEncoder.encode
方法就足够了,逻辑上,在读取数据库时必须使用java.net.URLEncoder.decode
方法。
结论
XMeLon模式和解析器不仅将XML文件存储到数据库中,还通过表和关系来提取和构建这些文件中包含的结构。如果所有这些工作都是自动完成的,那么应用程序只需利用强大的SQL功能,如排序、过滤和分组,来完成任何需要用数据完成的事情。如果XML系统使用多个文件和/或不同的XML模式,那么使用XMeLon方法的优势将更加明显。