前面讲解了UserDao接口的JDBC实现,列举了其中一个方法的实现。其它方法与addUser方法代码模式上基本相差不大,就sql语句和设置参数时有少许改变。完全实现UserDao接口后,就可以模拟一下业务层调用数据访问层的过程。模拟过程中,用的父类(UserDao)的引用指向子类(UserDao的实现类)的对象。虽然可以替换掉实现类,但是还是需要在代码中修改,然后重新编译。运用工厂模式,可以把实现类写在配置文件中,这样就不用修改代码,直接修改配置文件就能起到换数据访问层的作用。
先编写一个key和value值对的属性文件,关键字是userDaoClass,值是类的名字,如下:
userDaoClass=cn.itcast.jdbc.dao.impl.UserDaoJdbcImpl
然后通过Properties类来读取配置文件中的键值对,从而构造出相应的UserDao的实现类,即数据访问层。
public class DaoFactory {
private static UserDao userDao = null;
private static DaoFactory instance = new DaoFactory();
private DaoFactory() {
try {
Properties prop = new Properties();
InputStream inStream = DaoFactory.class.getClassLoader()
.getResourceAsStream("daoconfig.properties");
prop.load(inStream);
String userDaoClass = prop.getProperty("userDaoClass");
Class clazz = Class.forName(userDaoClass);
userDao = (UserDao) clazz.newInstance();
} catch (Throwable e) {
throw new ExceptionInInitializerError(e);
}
}
public static DaoFactory getInstance() {
return instance;
}
public UserDao getUserDao() {
return userDao;
}
}
例子中采用单例模式,在构造函数中初始化UserDao。在载入配置文件的过程中,有两种方式可供选择,一是通过FileInputStream文件输入流,利用相对路径或者绝对路径来载入文件。二是通过载入当前类的类加载器来载入。这种方式的好处是,将属性文件放在源文件夹下,部署的时候该文件也会被复制到相应的class目录下,被相应的类加载器加载。载入文件后,通过Properties的load方法使该Properties对象完全初始化,这样就能继续通过getProperty方法获取相应关键字对应的值。最后通过反射构造出UserDao的实例对象。在上面的代码中,如果将最开始的两个静态成员变量交换位置,则将会出现初始化错误。因为先执行new DaoFactory()时,UserDao初始化为正常的值,然后再次执行UserDao=null,刚刚获取的实例对象就又被覆盖成null了,这将使getInstance和getUserDao返回的都是null值。因此,有时候变量的顺序(初始化顺序)也能导致异常。这样的错误往往很难查找。
学习了上面比较实用的JDBC的例子,下面来学习JDBC的另外一个比较重要的方面——事务。事务在企业级应用中经常遇到,比如在银行的两个账户的转账过程,就必须保证一个账户加钱,一个账户减钱,不能存在只对一个账户进行操作,而另外一个却没有做。事务有以下四个特性:
1.原子性(atomicity):组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分。
2.一致性(consistency):在事务处理执行前后,数据库是一致的(数据库数据完整性约束)。
3.隔离性(isolcation):一个事务处理对另一个事务处理的影响。
4.持续性(durability):事务处理的效果能够被永久保存下来。
了解一些概念后,用一个演示程序模拟了事务。用到的还是user这张表,要求在张三(id=1)的money减10块,李四(id=2)的money加10块,但是如果李四的money大于400后,则抛出异常,程序要求张三、李四的money同时操作。所以,当李四的money小于400时,程序执行结果是正常的,一个减一个加,但是当李四的money大于400时,张三的money减了,但是李四的却没有加。所以要保证这些操作的原子性,加入事务控制语句就能解决问题,下面是示例代码:
static void test() throws SQLException {
Connection conn = null;
Statement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);
st = conn.createStatement();
String sql = "update user set money=money-10 where id=1";
st.executeUpdate(sql);
sql = "select money from user where id=2";
rs = st.executeQuery(sql);
float money = 0.0f;
if (rs.next()) {
money = rs.getFloat("money");
}
if (money > 400)
throw new RuntimeException("已经超过最大值!");
sql = "update user set money=money+10 where id=2";
st.executeUpdate(sql);
conn.commit();
} catch (SQLException e) {
if (conn != null)
conn.rollback();
throw e;
} finally {
JdbcUtils.free(rs, st, conn);
}
}
开启事务的语句为conn.setAutoCommit(false),即把事务自动提交设置成false。当一切正常时,在try的末尾有commit语句,数据库修改生效。当try语句块中抛出异常时,在catch语句块中通过rollback语句回滚事务到开启事务的地方。因此,从事务开启到commit语句之间的SQL操作保证了原子性。上面的例子是完全回滚,如果当只想撤销事务中的部分操作时可使用SavePoint。使用下面语句可以在当前语句处建立一个存储点:
SavePoint sp = connection.setSavepoint();
当需要将数据库回滚到某个存储点并保存回滚点之前的所有SQL操作,则使用下面两个语句:
connection.rollerbak(sp);
connection.commit();
最后提及的是JTA,当需要跨越多个数据源的事务时,就应该使用JTA容器实现事务。这个虽然复杂,但是企业级应用也经常用到。比如EJB、分布式事务处理等等。