Spring事务管理之前的话
简介:
事务管理是应用程序与DB进行交互的关键部分,应用程序必须确保数据的完整性和唯一性,spring提供了对众多当下流行的数据访问层框架的无缝集成(JDBC, JPA, Hibernate etc.)下面的文章我们将列举几个spring怎样去进行事务管理的例子,这篇文章力图简洁的体现spring事务管理的方式,所以可可能的白话。并且这篇文章中涵盖了声明式事务管理和编程式事务管理并最后介绍了如何用注解和aop的形式去实现事务管理。
spring究竟给我带来了什么,我们有必要探究一下当没有spring的时候我们是怎么进行事务管理的,下面先以一个很日常的例子购买电影票的例子去模拟事务管理的场景。
如果需要项目源码,可以留下您的邮箱,本文章只适合初学者。
1.本项目的依赖jar包
javase-1.5以上 ojdbc5.jar 针对oralce的JDBC驱动 commons-collections-3.2.1.jar 主要应用了泛型 commons-logging.jar 用于日志记录 spring.jar 主要使用了jsbcTemplete、ApplicationContext
2.项目中需要的sql脚本都放到了文章中,可以直接使用
这一部分我们将看到应用程序怎样去进行事务管理,我们将开发一个提供订购电影票服务的程序,涉及到用户,账户,和电影票三个对象,场景可以简单的理解为当一个用户购买电影票后,他的账户款项被相应的扣除,电影院票的数量也将相应的变化。
public class Account { private String id; private double amount; public String getId() { return id; } public void setId(String id) { this.id = id; } public double getAmount() { return amount; } public void setAmount(double amount) { this.amount = amount; }
相应的数据库脚本:
create table ACCOUNT ( ID NUMBER(4), AMOUNT NUMBER(4) ) ; --准备测试数据: insert into ACCOUNT (ID, AMOUNT) values (22, 100); insert into ACCOUNT (ID, AMOUNT) values (32, 200); insert into ACCOUNT (ID, AMOUNT) values (34, 300); insert into ACCOUNT (ID, AMOUNT) values (45, 400); commit;
上边的代码清单是账户对象的定义,其中我们声明了属性id,和总额(款项),下面是我们定义的电影票对象,其中我们声明了电影的名称和在任意时刻时总门票的数量
public class MovieTicket { private String id; private String movieName; private int totalTicketsCount; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getMovieName() { return movieName; } public void setMovieName(String movieName) { this.movieName = movieName; } public int getTotalTicketsCount() { return totalTicketsCount; } public void setTotalTicketsCount(int totalTicketsCount) { this.totalTicketsCount = totalTicketsCount; } }
相应的数据库脚本
create table MOVIE_TICKET ( ID VARCHAR2(50), MOVIENAME VARCHAR2(50), TOTALTICKETSCOUNT NUMBER(4) ) ; --填充测试数据: insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('1', '精武风云', 197); insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('2', '精武风云', 200); insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('3', '精武风云', 200); insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('4', '精武风云', 200); insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('5', '精武风云', 200); insert into MOVIE_TICKET (ID, MOVIENAME, TOTALTICKETSCOUNT) values ('6', '精武风云', 200); commit;
用户对象包含了用户id和用户名,以及存在一个账户对象的引用,当一个电影票被购买的时候,ticketId所对应的MovieTicket中的id所对应的对象需要相应的被更新。
public class User { private String id; private String name; private Account account; private String ticketId; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Account getAccount() { return account; } public void setAccount(Account account) { this.account = account; } public String getTicketId() { return ticketId; } public void setTicketId(String ticketId) { this.ticketId = ticketId; } }
相应的数据库脚本:
create table USER1 ( ID NUMBER(4), NAME VARCHAR2(50), TICKETID NUMBER(4), ACCOUNTID NUMBER(4) ) ; --填充测试数据: insert into USER1 (ID, NAME, TICKETID, ACCOUNTID) values (1, '华为', 32, 22); insert into USER1 (ID, NAME, TICKETID, ACCOUNTID) values (2, '爱立信', 33, 32); insert into USER1 (ID, NAME, TICKETID, ACCOUNTID) values (3, '中信', 44, 45); insert into USER1 (ID, NAME, TICKETID, ACCOUNTID) values (4, '索尼', 34, 34); commit;
下面是BookingService接口,他将向客户端暴漏用于预定票的"bookTicket"方法:
public interface BookingService { void bookTicket(int userId, int ticketId, int noOfTickets); }
当我们继续深入的写这篇文章的时候,我们会看到大量的程序会使用相同的代码,所以我们写一个下面的实用方法来加载应用程序上下文的实例,并返回一个Jdbc模板
public class Utils { static ApplicationContext context = null; public static JdbcTemplate jdbcTempalte(){ initContext(); DriverManagerDataSource dataSource = (DriverManagerDataSource)context.getBean("mySqlDataSource"); System.out.println("Data source is " + dataSource); JdbcTemplate template = new JdbcTemplate(dataSource); return template; } public static ApplicationContext getContext(){ initContext(); return context; } public static ApplicationContext getContext(String configFilename){ return new ClassPathXmlApplicationContext(configFilename); } private static void initContext(){ if (context == null){ context = new ClassPathXmlApplicationContext("main.xml"); } } }
下面是上面实用类通过spring配置文件加载应用的配置文件内容:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "http://www.springframework.org/dtd/spring-beans-2.0.dtd"> <beans> <bean id="myOralceDataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource" destroy-method="close"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl"/> <property name="username" value="sniper"/> <property name="password" value="123"/> </bean> </beans>
下面的类包涵了购买定影票的各种方法:
public class TicketUtils { @SuppressWarnings("unchecked") public static int getAccountId(JdbcTemplate template, int userId){ String sql = "select * from user1 where id = '" + userId + "'"; List<Object> userList= template.queryForList(sql); int accountId = -1; for (Object userObject : userList){ if (userObject instanceof ListOrderedMap){ ListOrderedMap map = (ListOrderedMap)userObject; Iterator<Object> iterator = map.keySet().iterator(); while (iterator.hasNext()){ Object key = iterator.next(); Object value = map.get(key); if (key.equals("account_id")){ accountId = Integer.parseInt(value.toString()); } System.out.println("Key值是 - " + key + ", Value值是 - " + value); } } } return accountId; } @SuppressWarnings("unchecked") public static float findTicketCost(JdbcTemplate template, int ticketId){ String sql = "select * from movie_ticket where id = '" + ticketId + "'"; List<Object> userList= template.queryForList(sql); float ticketCost = 0; for (Object userObject : userList){ if (userObject instanceof ListOrderedMap){ ListOrderedMap map = (ListOrderedMap)userObject; Iterator<Object> iterator = map.keySet().iterator(); while (iterator.hasNext()){ Object key = iterator.next(); Object value = map.get(key); if (key.equals("price")){ ticketCost = Float.parseFloat(value.toString()); } System.out.println("Key - " + key + ", Value - " + value); } } } return ticketCost; } public static void deductMoneyFromAccount(JdbcTemplate template, int accountId, float amount){ String sql = "update account set amount = (amount - " + amount + ") where id = " + accountId + ""; template.execute(sql); } public static void reduceTicketCount(JdbcTemplate template, int ticketId, int noOfTickets){ String sql = "update movie_ticket set totalTicketsCount = (totalTicketsCount - " + noOfTickets + ") where id = " + ticketId + ""; template.execute(sql); } public static int balanceMoney(JdbcTemplate template, int accountId){ String sql = "select amount from account where id = '" + accountId + "'"; return template.queryForInt(sql); } }
这里值得注意的是所有的方法都被定义为公共的和静态的,并且他们接受一个jdbcTemplate的方法,这样其他的类就可以直接的访问被传进来的JdbcTemplate对象
TicketBookingService.bookticket()方法接受一个USERID,电影票id和被买掉的电影票数量的参数。
那么其中的步骤应该是这样的,第一步通过用户的userid作为参数,调用getAccountId方法得到相应的accountID,然后通过ticketid做为参数,调用findTicketCost() 获得电影票的价格一旦总的价格被计算出,这些金额将从用户account中进行扣除,我们在上边的程序中已经有了用户的accountid以及扣除金额的deductMoneyFromAccount()方法,最后假设预定成功,电影票的总量将被扣除。
下面这部分代码的核心是应用jdbcTemplate的bookTicket() 方法,默认情况下auto-commit 在连接对象中设置为ture,也就意味着不管你insert/update/delete操作的时候,数据库始终提交当前事务然后,在我们的例子中,当银行账户的钱被扣除,电影票削减,必须保证在购票成功的前提下,如果购票失败,那么应用必须确保这项事务的回滚
public class TicketBookingService { private JdbcTemplate template; public void setTemplate(JdbcTemplate template){ this.template = template; } public void bookTicket(int userId, int ticketId, int noOfTickets){ Connection connection = null; try{ connection = template.getDataSource().getConnection(); connection.setAutoCommit(false); int accountId = TicketUtils.getAccountId(template, userId); float ticketCost = TicketUtils.findTicketCost(template, ticketId); float totalCost = (noOfTickets * ticketCost); TicketUtils.deductMoneyFromAccount(template, accountId, totalCost); TicketUtils.reduceTicketCount(template, ticketId, noOfTickets); connection.commit(); }catch (Exception exception){ exception.printStackTrace(); if (connection != null){ try{ connection.rollback(); }catch (Exception e1){ e1.printStackTrace(); } } } } }
注意上边的代码,在方法开始的时候自动提交的连接设置我们显示的设置为false,如果失败,当异常抛出的时候我们要确保利用rollback进行事务的回滚,如果成功的时候commit()持久化相关的数据
测试方法:
public class Main { public static void main(String[] args) { JdbcTemplate template = Utils.jdbcTempalte(); TicketBookingService service = new TicketBookingService(); service.setTemplate(template); service.bookTicket(1, 1, 1); } }
客户端调用方法是简单的传递userid ,ticketid 和电影票的数量输出:
2012-10-23 16:01:22 org.springframework.context.support.AbstractApplicationContext prepareRefresh 信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@171bbc9: display name [org.springframework.context.support.ClassPathXmlApplicationContext@171bbc9]; startup date [Tue Oct 23 16:01:22 CST 2012]; root of context hierarchy 2012-10-23 16:01:22 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions 信息: Loading XML bean definitions from class path resource [main.xml] 2012-10-23 16:01:23 org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory 信息: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@171bbc9]: org.springframework.beans.factory.support.DefaultListableBeanFactory@16bd8ea 2012-10-23 16:01:23 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons 信息: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@16bd8ea: defining beans [myOralceDataSource]; root of factory hierarchy 2012-10-23 16:01:23 org.springframework.jdbc.datasource.DriverManagerDataSource setDriverClassName 信息: Loaded JDBC driver: oracle.jdbc.driver.OracleDriver Data source is org.springframework.jdbc.datasource.DriverManagerDataSource@1deeb40 Key值是 - ID, Value值是 - 1 Key值是 - NAME, Value值是 - 华为 Key值是 - TICKETID, Value值是 - 32 Key值是 - ACCOUNTID, Value值是 - 22 Key - ID, Value - 1 Key - MOVIENAME, Value - 精武风云 Key - TOTALTICKETSCOUNT, Value - 196
Spring事务管理之前的话Code,点击前边的链接可以下载示例代码。
北京市海淀区
邮箱:rafx_z@hotmail.com