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

XMeLon Schema

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.81/5 (9投票s)

2013年11月10日

CPOL

14分钟阅读

viewsIcon

23422

downloadIcon

199

另一种将XML转换为数据库的方法

XMELON Schema for 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 data as plain table

就好像XML只有两个维度,但这并不真实。但在寻找缺失的XML维度之前,让我们更方便地排列路径,以便以后更容易找到我们感兴趣的表和维度,即内容模式中的那些。我们将路径分为两部分:除最后一个标签之外的路径,我们称之为层;最后一个标签,我们称之为维度(此时不是指XML维度)。我们甚至获取层的最后一个标签,并将其放在一个单独的列中。现在相同的数据看起来像

xmelon layer and dimension

我们添加了冗余列,因此表仍然不完整。事实上,所示的示例是模棱两可的,不同的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>

因此,不仅知道数据元素的路径很重要,而且知道它属于哪个“路径实例”也很重要。特别是我们有兴趣将层实例作为一个新列。它可以由一个计数器表示,该计数器在层更改时递增。通常每个层都有一个父层,我们可以将其存储在新列中。再次,相同的样本以及所有这些新列

xml data as plain table

现在,查看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的详细信息。

要获得连接,只需使用layerCounterlayerParentCounter将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元素的堆栈,提供典型的pushpopsizeelementAt方法以及另外两个方法,用于根据堆栈中的元素构建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;
}

自由文本的情况在startElementendElement方法中都会被检查。让我们重复我们的自由文本示例

<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方法的优势将更加明显。

© . All rights reserved.