记得上篇文章,讲述了spring以jdbc作为数据源实现事务控制的列子,可能大家像我一样,觉得原来这样就实现事务啦,对,我一般学习一些新东西,首先会把这个东西能运行的跑出来个demo,不能运行的,我尝试用底层的类,写写代码,以便搞清楚其真实的逻辑,当然学习方法因人而异。
这一节,准备实现什么呢?我想从最基本的jdbc开始,看看不依赖其他的任何东西,如何实现,事务,这里很简单的讲事务,统一指本地事务,如果牵涉到分布式事务,我会着色标记,这里不考虑事务之间的传播属性,底层数据库的隔离级别,也许以后会有相关的分析,但是目前,我不准备提及。
好,说了那么多废话,咱们就开始进入正题。这一节,我写应用程序,你分析逻辑,当然如果没人分析的话,我还是要自己分析的,毕竟要引出问题,往下面接着写,写一个系列,一方面和大家分享,一方面作为自己的总结。
这一次我想验证一个问题,java的事务是决定在谁身上的,上篇博客记得我说过,是Connection。同样以上一小节举得例子,Customer和Address两个表来做说明,本例遵循一般java应用的DAO,Service层的模块组织结构,如果Customer和Address对应的DAO分别获取不同的Connection对象,那么在测试的service中,获取另外一个Connection对象,执行相应的插入操作,如果插入中间出现异常,则即使调用connection回滚,则不起作用,如果按照咱们的预测,那么一定程度上,辅证了上一节的观点。
模块的组成大致如下:
关于Customer和Address对应的一般的pojo,不在描述,这里可能有个误区,说你不使用spring的事务管理,干嘛要包含spring呢,其实spring不止有事务的便捷性,还有IOC,我不想手动的创建bean(这个词又坑了多少人啊,理解成包含变量,提供set get方法的类就好,逼格高的名词,我不是很喜欢),ok。
看完组成之后,我们来看对应的DAO的实现,拿CustomerDAO来说事吧,AddressDAO也类似,代码如下:
public class CustomerDAOImpl implements CustomerDAO { private DataSource dataSource; /** * @param dataSource */ public void setDataSource(final DataSource dataSource) { this.dataSource = dataSource; } @Override public void create(Customer customer) { String queryCustomer = "insert into Customer (id, name) values (?,?)"; Connection connection =null; PreparedStatement preparedStatement = null; try { connection = dataSource.getConnection(); connection.setAutoCommit(false); preparedStatement = connection.prepareStatement(queryCustomer); preparedStatement.setInt(1, customer.getId()); preparedStatement.setString(2, customer.getName()); preparedStatement.execute(); connection.commit(); } catch (SQLException e) { System.out.println(e); try { if(connection!=null){ connection.rollback(); } } catch (SQLException e1) { System.out.println(e1); } }finally{ try { if(connection!=null){ connection.rollback(); } } catch (SQLException e2) { System.out.println(e2); } } System.out.println("Inserted into Customer Table Successfully"); } }
哈哈,先不要骂我,我一点点说,首先功能是实现了是不是,至于是否优雅,暂且不论,我相信,每个人都写过这样的代码,ok。
在看一下使用这个类的“测试类",为什么测试类打引号呢,因为没有遵从maven的约定,不是在src/main/test目录下建立的测试类,本人比较懒,主测试类的代码:
public static void main(String[] args) { ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext( "spring.xml"); CustomerDAO customerDAO = (CustomerDAO) ctx.getBean("customerJDBCDAO"); AddressDAO addressDAO = (AddressDAO) ctx.getBean("addressJDBCDAO"); Customer cust = createDummyCustomer(); Address address = createDummyAddress(); DataSource dataSource = (DataSource) ctx.getBean("dataSource"); Connection connection =null; try { connection = dataSource.getConnection(); connection.setAutoCommit(false); customerDAO.create(cust); addressDAO.create(address); connection.commit(); } catch (Exception e) { // TODO: handle exception try { if(connection!=null) connection.rollback(); } catch (Exception e2) { // TODO: handle exception } }finally{ try { if(connection!=null) connection.rollback(); } catch (Exception e2) { // TODO: handle exception } } ctx.close(); } private static Customer createDummyCustomer() { Customer customer = new Customer(); customer.setId(2); customer.setName("lianzi"); return customer; } private static Address createDummyAddress() { Address address = new Address(); address.setId(2); address.setCountry("china"); // setting value more than 20 chars, so that SQLException occurs address.setAddress("********************************************"); return address; }
因为使用了spring ioc,所以要把相应的bean的配置加进来,这样你才明白,我为什么没在DAO里面传递DataSource的原因,对应的spring.xml文件的主要配置如下:
<!-- MySQL DB DataSource --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver" /> <property name="url" value="jdbc:mysql://localhost:3306/transactionDB" /> <property name="username" value="root" /> <property name="password" value="sishao_uestc" /> </bean> <!-- two daos use jdbc datasource --> <bean id="customerJDBCDAO" class="com.lianzi.jdbc.dao.impl.CustomerDAOImpl"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="addressJDBCDAO" class="com.lianzi.jdbc.dao.impl.AddressDAOImpl"> <property name="dataSource" ref="dataSource"></property> </bean>
好啦,到这里实现啦,运行测试类你会发现,结果和上一节讲的一样,因为AddressDAO里面抛了异常,但是我try catch住啦,也回滚啦,结果如下:
但是,你会发现,sql数据库里面,结果和上次不一样,customer表里有数据,而address里面没有,这说明回滚不起作用,原因上面说啦,不是同一个Connection,java事务是Connection对象所决定的。
发现的问题
1、分层带来的困扰
在业务里面,编写事务处理的代码,污染了业务代码,犯了分层的“忌讳”。
2、把事务说小拉
你怎么知道,在事务开启的时候,customerDAO和addressDAO在同一个事务里面?妄想的吧
3、代码重复太多
没写一个DAO,都要开启事务,填充sql,执行sql,把ResultSet解析到bean,或者称之为业务对象
问题的关键点
1、如何界定事务的边界,在哪里声明事务?
2、下一篇文章会重构这部分代码,会因为另外一个问题,这节的例子,事务回滚没有起作用,是因为用的不是一个Connection对象,很简单的想法是使用Connection对象,传递给每个DAO层,那样就会起作用啦,那是这是严重的api污染,于是引出,如何构建一个“上下文”,在事务开始与事务提交时,以及在事务过程中所有数据访问方法都能“隐式”地得到“同一个资源”(数据库连接)?
概念的说明:
1、资源:
无论是jdbc的connecttion、还是hibernate、mybatis的session,统称为资源。
关于资源、事务、线程,也是个很不容易理解的,地方,他们的生命周期是怎么样的呢?