摘要:有时候我们的数据存放在Excel中(特别是对于用户来说更喜欢使用Excel收集一些常用数据),而系统又需要这些数据来处理其他业务,那么此时我们就需要将这些数据导入到数据库中。但是鉴于Excel的样式多种多样,因此每次导入时都必须书写很多重复的代码。很明显对于一个软件开发者做这些重复劳动是一件很无趣的事情。那么怎样来寻中一种通用的方法呢?今天我们就一块看一下如何来解决这个问题。
主要内容
在开始今天的主题之前我们先简单的看一个基础的问题,那就是如何进行Excel的读写。关于Excel的读写操作目前主要分为:1.Oledb数据库连接方式2.使用Excel.exe Com组件3.使用第三方控件。具体哪种方式好我们要依据具体情况而定,对于第一种方式则要求excel表格必须是像数据库中的表一样规范,例如如果excel牵扯到合并单元格的情况就很难处理了。对于第二种方式则要求用户必须安装Excel,而且其效率比较低。考虑到我们的需求,所以这里选择第三种方式来操作Excel。操作Excel的第三方控件比较多,常见的如NPOI、myxls、Aspose.Cells等。前两者都是开源的,并且NPOI除了写Excel功能比较强之外对于Excel读取也是十分优秀(myxls读取excel不如NPOI)。Aspose.Cells是一款商业控件,其操作方便性当然也是十分强大的,而且Aspose是一个系列组件,不仅有操作Excel的组件还有关于word、ppt、pdf、flash等操作组件。这里因为项目开发中使用的是Aspose.Cells,因此下面的例子中我们就拿Aspose.Cells来进行Excel操作(大家可以去找破解版或者使用NPOI,当然也可以用myxls等)。
我们去设计通用Excel的目的就是为了避免重复工作,也就是说不必因为Excel的样式、数据等变化而重新从零做起、重复劳动。因此我们就必须抽取一个通用的东西出来,使用时只需要关注相关的业务而不必过度关注相关excel操作和存储。再简单一点就是封装共同点,暴漏个性点。考虑到这种情况,我们可以使用配置文件的方式来解决这个问题。在配置文件中我们配置Excle要导入的表、字段等信息,在进行导入时再依据配置文件将数据导入到数据库中。这样一来,在需要进行Excel导入时只需要为某个或多个excel配置一个xml文件,然后调用相关的类就可以完成整个excel导入工作了。
补充:通用的局限性
在这里说明一下,虽然我们设计的是一个通用的Excel导入程序,但是这里的"通用"只是相对来说的,并不是考虑了所有Excel的情况,因为Excel的设计情况十分的复杂多样,要将所有的情况都考虑进去是一个漫长的过程。我们这里的程序只考虑对于单sheet导入一个或多个表中的情况,并且不考虑包含统计行的情况(可以包含合并行、代码表字段等)。
既然考虑使用xml配置的方式来设计通用Excel导入,因此如何设计好xml也就成了设计的重点。对于单表导入(一个Excel主要导入到一个数据库表中,当然这并不排除牵扯其他代码表的情况)我们的配置文件无论以数据库为基础设计(主要是依据数据库表结构)还是以Excel(主要是依据Excel格式设计)为基础设计都可以,但是如果是多表导入(也就是一个Excel可以导入到几张表中的情况)的话考虑其复杂性还是以数据库为基础更为合适。因此考虑到这种情况,我们整个配置设计会以数据库表结构为基础来设计。最终我们的设计样例如下:
在最外层为Config节点,代表整个配置。其属性EndTag(数据读取的结束标志,例如"RowBlank"代表空行结束,在读取Excel时遇到某行没有任何数据的情况则视为结束;也可以为某个列地址,在导入时到了此列就会结束导入操作);属性HeaderIndex代表excel表头对应的行值(从1开始);DataIndex表示数据列起始行索引(从1开始)。
接着是Table节点,对应数据库中的表,可以有多个。其Name属性对应要导入的表名称;DeleteRepeat属性表示是否删除重复行(如果为true则会根据主键先删除重复行再执行插入操作);ExcludedColumns表示排除列,多个列名使用","分割(这些字段不会导入)。
Table节点内当然就是Column节点,也就是对应的列,通常有多个(注意对于excel中没有的列,而数据库表需要导入的,也需要配置Column节点,此时HeaderText为空或不配置HeaderText属性)。IsPrimarykey属性表示是否为主键(当Table节点配置DeleteRepeat为ture时必须指定一个Column节点的IsPrimaryKey为true,因为此属性是为了delete条件做准备的[有可能它不是真正的主键]);ColumnName表示对应的列名;HeaderText表示对应的Excel列头(在依据Excel别名导入时根据此值确定导入的列);Required指定此列是否为必须导入的列(如果配置为true,excel中此列为空并且没有配置默认值的话则会抛出异常);DataType为数据类型(例如string、number,用于数据校验);DefaultValue为默认值(注意其值不一定是指定的字符值,可以是"Max"、"NewID".如果为Max,那么此列必须为数值类型,此时在导入的时候如果需要使用默认值,就会在原来数据库表中此列最大值的基础上加上1导入到数据库中,如果为NewID在导入的时候如果需要使用默认值系统就会自动创建id);Comment是此列的说明。
在Column节点中还可以配置CodeTable节点,表示代码表。Name属性值主表的表名称;PrimaryKey指主表的主键,也就是字表的外键;ReferenceColumn表示对应代码字段关联名称列,也就是我们导入时所依据的excel对应值(例如CategoryID对应CategoryName,那么ReferenceColumn就是CategoryName,因为往往Excel中可能存放的是类似于CategoryName的东西而不是CategoryID,而导入操作时需要CategoryID)。
我们有了思路之后,接下来就来看一下类的设计吧。
在这些类中Excel类是整个导入的核心,其最初要的方法就是Import(),当然除此之外所有对于Excel的读取和对数数据库的操作以及对配置对象的解析都是由此类负责;Config类是对整个配置的抽象,其对应的方法图中也已经标出,每个Config类对应多个实体类;Entity是对于表的抽象,就是表对应的实体类;Property类是对于列的抽象,每个Entity中包含多个Property;另外DictionaryEntity是数据字典,是对代码表的抽象,每个Property可以对应一个代码表;除此之外ConfigHelper是对于应用程序配置的封装;AsposeCell是对Aspose.Cells的封装,包含常用的Excel读写方法。
接下来我们就开始实现整个设计吧,相信有了上面的说明和代码中的注释,理解起来应该很简单的,我就不再过多赘余了。
AsposeCell类
ConfigHelper类
DictionaryEntity类
Property类
Entity类
Config类
Excel类
注意:程序中用到了Cmj.dll(自定义的一个类库)中的几个类,具体说明如下(如果需要具体的代码可以发Email给我,[email protected] ):
SqlHelper:数据库操作类,使用时在配置文件的ConnectionStrings中配置name为"DBCon"的连接串,指明ProviderName即可,如果不使用配置可以直接在构造函数中传递这两个参数。
MyXML:对于XML文件操作的封装了,用于XML文件操作。
MyFileBase:对文件操作的封装。
FileSearch:对文件查找的封装,可以按照关键字查找文件。
为了更加容易理解,我对程序中的一些约定和处理做少许补充。
Excel类中有参构造函数两个参数分别是excel路径(也可以是目录)以及是否启用事务的bool型变量。如果用户使用无参构造函数的话,就必须在应用程序配置文件的appSettings配置节点配置中配置"ExcelPath"和"UseTransaction"两个节点。由于对数据库的操作需要访问应用程序的配置文件,因此需要配置connectionStrings节点。
一般的导入按照上面的配置就可以完成了。为了方便使用此类还提供了其他导入方式。假如觉得列配置较麻烦的,觉得没有必要配置那么多列的话,可以考虑下面几种方式。
ⅰ 配置文件中如果没有Column配置,可以通过配置单元格的别名,来指示每一个列对应的表字段。
ⅱ 如果配置文件中没有Column配置,而又不希望指定单元格别名,但是Excel中的列和表中的列又不是一一对应的(很可能有些列数据库表中有,而Excel中没有),此时如果Excel中列的顺序和表中顺序一致,你就可以通过在配置文件中设置Table的ExcludedColumns来将这些列排除(多个需要排除的列中间用","分割)。
ⅲ 如果我的Excel中的列和数据库表中完全一致且顺序一样,那么你出来配置文件中配置Table属性外就不需要任何额外的配置了。
另外,如果需要在导入时检查是否有默写列唯一的情况的情况(例如ProductName我不希望有重复),对于重复的先删除再倒入,此时可以再此列上设置IsPrimaryKey为true,然后在Table的属性中配置DeleteRepeat为true就可以了;如果所有的导入过程中你需要使用事务机制,可以在应用程序配置appSettings 配置节点中指定useTransaction为true(注意先要保证事务服务是启动的);程序支持合并行的导入。
关于配置文件和Excel文件的关系,是这样约定的,多数情况下ExcelPath的配置为目录(当然可以是一个Excel文件路径,此时就只导入该Excel文件),对于此种情形可能出现下列情况。
ⅰ 目录中只有一个配置文件,而有一个或多个Excel。这种情况下程序认为此配置文件对应所有Excel文件,将会使用此配置将所有Excel导入。
ⅱ 目录中有多个配置文件,并且有多个Excel文件。此时程序户根据配置文件的名称自动匹配Excel文件,匹配的规则就是它对应的Excle文件的名称中要出现配置文件的名称。
下面我们简单的测试一下我们的类库,应该说使用起来十分简单。
ⅰ 在使用之前当然要有一个需要导入的Excel
ⅱ 根据Excel编写xml文件
ⅲ 将excel文件和对应xml文件放到F:/Cmj.DataExchange/wfTest/excel
ⅳ 建立一个项目,添加Cmj.DataExchange.dll引用
ⅴ 在配置文件中配置数据库连接以及Excel目录等
ⅵ 接着确保Distributed Transaction Coordinator服务是启动的(因为程序中用的是.Net2.0的事务,当然你也可以使用.Net1.0事务,此时就无需启动此服务了)。
ⅶ 最后添加下面两句代码就可以完成整个导入了。
下面是导入前后数据库Northwind中Products表中的数据(已经成功导入)
导入之前
导入之后
对于一般的Excel导入情况程序基本上都能够解决,但是开发过程中难免遇到更复杂的情况,此时就需要更多的考虑和更复杂多样的设计,这种情况有机会我们再一看探讨。由于代码相对比较长阅读多少有些不便,这里提供源代码和示例下载 ,需要的朋友可以看一下。