DbUnit数据库测试之备份与还原

说明:有朋友联系我说代码无法运行,但是我却没有发现问题,后来才发现原来是数据库的驱动包版本的问题,在最新的JDBC4.x下是无法运行的!

解析:驱动的加载方式发生了转变,JDBC4.x开始自动加载驱动,而不需要手动加载Class.forName。

一、DbUnit简介

  DBUnit是一个基于JUnit扩展的数据库测试框架。它提供了大量的类对与数据库相关的操作进行了抽象和封装。它会把数据库表里的数据和一个xml文件关联起来,也就是说它可以让数据在XML文件和数据库之间转换。

  基于这种设计模式DBUnit可以在每个单元测试之前,先备份数据库到一个临时XML文件中,然后删除数据库中的所有数据接着把我们写好的模拟数据存入数据库中,最后,在当前单元测试测试完成后,删除现有数据再存入之前备份的数据,回溯到测试前的状态以达到各个单元测试互不影响的目的。

二、详解

提示:以下实例中使用的是FlatXmlDataSet,所以生成的备份的xml文件是FlatXmlDataSet格式,而并非是XmlDataSet格式。

1.备份特定表

@Test
public void backUpOneTable(){
	try {
		//创建DbUnit的Connection,需要传入一个数据库的connection作为参数
		IDatabaseConnection con = new DatabaseConnection([数据库链接]);
		//通过QueryDataSet可以有效的选择处理的表作为数据集
		QueryDataSet backup=new QueryDataSet(con);
		//添加备份表
		backup.addTable([备份的表名称]);
		//将backup中的数据通过FlatXmlDataSet的格式写到文件中
		FlatXmlDataSet.write(backup, new FileWriter([备份文件的路径及名称]));
	} catch (AmbiguousTableNameException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (DataSetException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (DatabaseUnitException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}		
}

2.备份整个数据库

@Test
public void backUpAllTable(){
	try {
		//创建DbUnit的Connection,需要传入一个数据库的connection作为参数
		IDatabaseConnection con = new DatabaseConnection([数据库链接]);
		//根据con创建相应的dataset,这个dataset包含了所有的表
		IDataSet ds=con.createDataSet();
		//将ds中的数据通过FlatXmlDataSet的格式写到文件中
		FlatXmlDataSet.write(ds, new FileWriter([备份文件的路径及名称]));//文件必须要添加.xml后缀
	} catch (DataSetException e) {
		e.printStackTrace();
	} catch (DatabaseUnitException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	} catch (IOException e) {
		e.printStackTrace();
	}
}

3.还原备份表&还原数据库

@Test
public void testResume() {// 数据库还原
	try {
		//创建DbUnit的Connection,需要传入一个数据库的connection作为参数
		IDatabaseConnection con = new DatabaseConnection([数据库链接]);
		// 根据备份文件创建dataset
		IDataSet ds = new FlatXmlDataSet(new FlatXmlProducer(
				new InputSource(new FileInputStream([备份文件路径及名称]))));
		//将数据库中的数据清空,并将数据插入到数据库中(还原)
		DatabaseOperation.CLEAN_INSERT.execute(con, ds);
	} catch (DataSetException e) {
		e.printStackTrace();
	} catch (FileNotFoundException e) {
		e.printStackTrace();
	} catch (DatabaseUnitException e) {
		e.printStackTrace();
	} catch (SQLException e) {
		e.printStackTrace();
	}
}

4.外键约束

(这一部分,我在参考了《JUnit实战  第二版》后有了新的发现,或者说更好的解决方法,后期我在进行完善。)

  通常数据库设计中经常会使用到外键约束,然而我们在进行备份后删除或还原前删除数据时DBUniit无法知道应该先删除哪张表再删除哪张表,因此很容易报com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException错误。

解决方法:禁止外键约束检查。下面两种方式都可以保证当前会话不作外键约束检查。(但是破坏了完整性)

1.DBUniit是通过jdbc的Connection对象来获取连接的,可以在URL上加上sessionVariables=foreign_key_checks=0来禁止外键约束检查。

public static Connection getConnection() throws SQLException{
    Connection con = null;
    con = DriverManager.getConnection("jdbc:mysql://localhost:3306/[数据库名称]?sessionVariables=foreign_key_checks=0",
                                      "root", "root");
    return con;
}

2.我们一般都会使用工具类来获取链接,所以我们不应该把外键约束检查禁止掉(全局性的)。所以我们可以在获取链接后再加上禁止外键约束的检查。(原理和上面的一样,但是执行的顺序有差别。)

//Disable foreign key check! 
connection.getConnection().prepareStatement("set @@session.foreign_key_checks = 0").execute();

警告:外键约束禁止掉了,但是也同时破坏了数据库的表的约束结构。换句话说当我们获取到链接后随即禁止掉外键约束,那么在测试中我们的所有关于外键的操作都是无效的,因为外键已经被去除了。因此,最佳的禁止约束检查的时机是在我们回复备份文件之前进行的操作,此时所有的测试已经结束。

不知道大家测试了没,我后来经过了测试发现表中有外键约束,你在写入自己的xml测试数据的时候也报这种异常,所以我感觉还是应该先直接禁止掉外键约束,然后在测试单个方法的时候在添加比较好。

5.数据库自动提交操作

  前言:我在运行下面实例进行数据库备份和还原操作时,有种情况我无法测试成功,一开始我百思不得其解,然后我经过多次检验,却发现备份和还原操作都正常。但是有一种情况很特殊:

操作:备份数据库—>执行插入数据操作—>还原数据库操作

问题:数据库还原操作未执行,但是其代码块以及内部用syso(输出语句)验证,结果输出了输出语句,显然不合理。

解决的途径:我在执行插入数据库操作的时候使用了conn.setAutoCommit(false);关闭了数据库的自动提交,我把这句话屏蔽掉,数据库还原操作又可以执行了。

疑问:哪位大神指导这是为什么。。。。。。急求!!!!

声明:我工具类使用的是单例模式,也就是说数据库链接都是用的是同一个。

实验:我将还原数据库操作的最前面,将自动提交打开,而插入操作的自动提交仍然关闭着,最终结果,数据库恢复成功。

实验结果:DbUnit操作数据库的时候(DatabaseOperation)自动提交必须打开,可能是DbUnit的所有的数据库操作都是自动提交。

三、综合实例

测试用例:

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileWriter;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlProducer;
import org.dbunit.operation.DatabaseOperation;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.xml.sax.InputSource;
import static org.junit.Assert.*;
import per.fxb.firpro.DB_Operation.ConnectionFactory;

/**
 * 针对对数据库账户的操作进行的测试
 * 
 * @author fxb
 *
 */
public class TestAccountDao {
	static Connection conn;
	@BeforeClass
	public static void setUp() {// 对数据进行备份及数据库链接获取
		// 获取数据库链接
		conn = ConnectionFactory.INSTANCE.getConnectioin();

		try {// 备份数据库
				// 创建DbUnit的Connection,需要传入一个数据库的connection作为参数
			IDatabaseConnection con = new DatabaseConnection(conn);
			// 根据con创建相应的dataset,这个dataset包含了所有的表
			IDataSet ds = con.createDataSet();
			// 将ds中的数据通过FlatXmlDataSet的格式写到文件中
			FlatXmlDataSet.write(ds, new FileWriter("dbBackUp/dbBackup.xml"));// 文件必须要添加.xml后缀
			// 清空数据库
			DatabaseOperation.DELETE_ALL.equals(con);
		} catch (DataSetException e) {
			e.printStackTrace();
		} catch (DatabaseUnitException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
	@AfterClass
	public static void tearDown() {// 对数据进行恢复及关闭数据库链接
		// 忽略外键的检查
		try {
			conn.prepareStatement("set @@session.foreign_key_checks = 0")
					.execute();
		} catch (SQLException e1) {
			e1.printStackTrace();
		}
		try {// 还原备份数据库
				// 创建DbUnit的Connection,需要传入一个数据库的connection作为参数
			IDatabaseConnection con = new DatabaseConnection(conn);
			// 根据备份文件创建dataset
			IDataSet ds = new FlatXmlDataSet(
					new FlatXmlProducer(new InputSource(
							new FileInputStream("dbBackUp/dbBackup.xml"))));
			// 将数据库中的数据清空,并将数据插入到数据库中(还原)
			DatabaseOperation.CLEAN_INSERT.execute(con, ds);
		} catch (DataSetException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (DatabaseUnitException e) {
			e.printStackTrace();
		} catch (SQLException e) {
			e.printStackTrace();
		}
		try {// 关闭数据库
			if (!conn.isClosed()) {
				conn.close();
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}

	@Test
	public void ceshi() {
		fail("添加测试代码");
	}
}

获取数据库链接的工具类:

import java.awt.HeadlessException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

import javax.swing.JFrame;
import javax.swing.JOptionPane;

import org.apache.log4j.Logger;

import per.fxb.firpro.propertyRD.info.DataBaseInfo;
import per.fxb.firpro.propertyRD.util.ReadPropertyOfDB;
/**
 * 数据库连接工厂,采用单例模式
 * 
 * @author fxb
 *
 */
public enum ConnectionFactory {
	INSTANCE;
	private Connection conn = null;
	Logger logger = Logger.getLogger(ConnectionFactory.class);
	private DataBaseInfo dbInfo = null;
	private ConnectionFactory() {
		dbInfo = ReadPropertyOfDB.loadProperties();
		try {
            // JDBC4.x自动加载驱动,此处可略去
            // com.mysql.cj.jdbc.Driver【新的驱动】
            // 没有进行验证,若行不通可在博文留言
			Class.forName(dbInfo.getDriver());
		} catch (ClassNotFoundException e) {
			logger.error("加载数据库驱动异常", e);
		}
	}
	/**
	 * 
	 * @return 返回一个数据库链接
	 */
	public Connection getConnectioin() {
		boolean flag = false;// true表示链接关闭或者链接为空
		try {
			if (null != conn) {
				flag = conn.isClosed();
				if (flag) {
					logger.info("数据库连接已经关闭");
					flag = true;
				}
			}
		} catch (SQLException e1) {
			logger.debug("检测数据库连接是否关闭异常", e1);
		}
		if (null == conn || flag) {
			try {
				conn = DriverManager.getConnection(dbInfo.getUrl(),
						dbInfo.getAccount(), dbInfo.getPassword());
				logger.info("重新获取数据库连接中.....");
				flag = false;
			} catch (SQLException e) {
				StringBuffer errInfo = new StringBuffer("无法连接数据库\r\n");
				errInfo.append("================\r\n");
				errInfo.append("请做以下检查:\r\n");
				errInfo.append("1.数据库服务是否开启\r\n");
				errInfo.append("2.检查账号密码以及连接的URL是否正确\r\n");
				errInfo.append("================");
				logger.warn(errInfo.toString(), e);
				JOptionPane.showMessageDialog(new JFrame(), "无法链接数据库", "警告",
						JOptionPane.ERROR_MESSAGE);
			}
		}

		if (null != conn && !flag) {
			Statement sta = null;
			try {// 设置编码为UTF-8,防止数据库中文乱码
				sta = conn.createStatement();
				String sql = "set names utf8;";
				if (sta.execute(sql)) {
					logger.warn("防止数据库中文乱码设置失败");
				} else {
					logger.info("防止数据库中文乱码设置成功");
				}
			} catch (HeadlessException e) {
				logger.error("", e);
			} catch (SQLException e) {
				logger.error("", e);
			} catch (Exception e) {
				logger.error("非预知错误", e);
			}
		}

		return conn;
	}
}

数据库信息的javabean

/**
 * 数据库配置信息对象
 * @author fxb
 *
 */
public class DataBaseInfo {
	private String url = null;
	private String driver = null;
	private String account = null;
	private String password = null;
	public String getUrl() {
		return url;
	}
	public void setUrl(String url) {
		this.url = url;
	}
	public String getDriver() {
		return driver;
	}
	public void setDriver(String driver) {
		this.driver = driver;
	}
	public String getAccount() {
		return account;
	}
	public void setAccount(String account) {
		this.account = account;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
}

读取数据库配置信息的工具类

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

import org.apache.log4j.Logger;

import per.fxb.firpro.propertyRD.info.DataBaseInfo;

/**
 * 读取数据库配置信息
 * 
 * @author fxb
 *
 */
public class ReadPropertyOfDB {
	private static Logger logger = Logger.getLogger(ReadPropertyOfDB.class);
	private static Properties prop = new Properties();
	/**
	 * 这是一个工具类,所以禁止随意生成实例对象
	 */
	private ReadPropertyOfDB() {
	}
	
	/**
	 * 
	 * @return 返回一个包含数据库配置信息的对象
	 */
	public static DataBaseInfo loadProperties(){
		DataBaseInfo dbInfo=new DataBaseInfo();
		try {
			InputStream in =ReadPropertyOfSys.class.getClassLoader().getResourceAsStream("database.properties");
			prop.load(in);
			dbInfo.setUrl(prop.getProperty("url"));
			dbInfo.setDriver(prop.getProperty("driver"));
			dbInfo.setAccount(prop.getProperty("account"));
			dbInfo.setPassword(prop.getProperty("password"));
			//下面是带有默认参数,测试后使用,系统整体完成时使用。
//			dbInfo.setUrl(prop.getProperty("url", "jdbc:mysql://localhost:3306/firpro"));
//			dbInfo.setDriver(prop.getProperty("driver", "com.mysql.jdbc.Driver"));
//			dbInfo.setAccount(prop.getProperty("account","root"));
//			dbInfo.setPassword(prop.getProperty("password","root"));
		} catch (IOException e) {
			logger.error("数据库配置文件加载异常", e);
		}
		return dbInfo;
	}
}

数据库的配置信息:database.properties

#数据库链接的URL,其中数据库名称为firpro
url=jdbc:mysql://localhost:3306/firpro
#采用的是MySQL数据库
driver=com.mysql.jdbc.Driver
#账户名称
account=root
#密码
password=root

  上面示例中日志框架我采用的是Log4j,在此不详细叙述,你们完全可以将其换成输出语句,自己灵活使用。

  我在写这篇博客的时候也是一名初学者,有任何疑问请留言!

参考资料:

赞赏

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值