JdbcTemplate 是 Spring 对 JDBC 的封装,目的是使JDBC更加易于使用,JdbcTemplate是Spring的一部分。JdbcTemplate 处理了资源的建立和释放,它帮助我们避免一些常见的错误,比如忘了总要关闭连接。他运行核心的JDBC工作流,如Statement的建立和执行,而我们只需要提供SQL语句和提取结果即可。
如果直接使用JDBC的话,需要我们加载数据库驱动、创建连接、释放连接、异常处理等一系列的动作,繁琐且代码看起来不直观。而使用 jdbctemplate 则无需关注加载驱动、释放资源、异常处理等一系列操作,我们只需要提供 sql 语句并且提取最终结果即可,大大方便我们编程开发。此外,Spring提供的JdbcTempate能直接数据对象映射成实体类,不再需要获取ResultSet去获取值、赋值等操作,提高开发效率;
Spring为了简化数据库访问,主要做了以下几点工作:
SQLException
封装为DataAccessException
,这个异常是一个RuntimeException
,并且让我们能区分SQL异常的原因,例如,DuplicateKeyException
表示违反了一个唯一约束;如果同maven导入依赖的话,将spring几个核心的jar引入后,还需要引入:
org.springframework spring-orm 5.3.18
或者将spring-orm-*.*.*.jar 将其导入到环境中。
因为需要连接数据库,因为使用你的阿里的druid,所以在maven中配置
com.alibaba druid 1.2.16
如果不通过maven导入依赖环境的花,还是需要手动导入第三方包。
完整的xml是:
spring_test3 com.xzd 1.0-SNAPSHOT jar 4.0.0 spring_jdbctemplate 8 8 UTF-8 org.springframework spring-core 5.3.20 org.springframework spring-context 5.3.20 org.springframework spring-beans 5.3.20 org.springframework spring-aop 5.3.20 org.springframework spring-expression 5.3.20
org.springframework spring-test 5.3.23 test org.springframework spring-orm 5.3.18 mysql mysql-connector-java 8.0.29 com.alibaba druid 1.2.16 junit junit RELEASE test
现在进行环境的搭建:
首先有数据:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`test_jdbtemplate` /*!40100 DEFAULT CHARACTER SET utf8mb3 */ /*!80016 DEFAULT ENCRYPTION='N' */;USE `test_jdbtemplate`;/*Table structure for table `t_categories` */DROP TABLE IF EXISTS `t_categories`;CREATE TABLE `t_categories` (`categories_flag` int DEFAULT NULL,`categories_name` varchar(20) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;/*Data for the table `t_categories` */insert into `t_categories`(`categories_flag`,`categories_name`) values (1,'射击'),(2,'动作');/*Table structure for table `t_customer` */DROP TABLE IF EXISTS `t_customer`;CREATE TABLE `t_customer` (`customer_id` int DEFAULT NULL,`customer_name` varchar(20) DEFAULT NULL,`customer_phone` int DEFAULT NULL,`customer_money` double(5,2) unsigned DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;/*Data for the table `t_customer` */insert into `t_customer`(`customer_id`,`customer_name`,`customer_phone`,`customer_money`) values (1,'貂蝉',1311111111,100.00),(2,'吕布',1312222222,200.00);/*Table structure for table `t_game` */DROP TABLE IF EXISTS `t_game`;CREATE TABLE `t_game` (`categories_id` int DEFAULT NULL,`game_id` int DEFAULT NULL,`game_name` varchar(20) DEFAULT NULL,`game_details` varchar(30) DEFAULT NULL,`game_price` double(5,2) unsigned DEFAULT NULL,`game_count` int unsigned DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3;/*Data for the table `t_game` */insert into `t_game`(`categories_id`,`game_id`,`game_name`,`game_details`,`game_price`,`game_count`) values (1,1,'使命召唤','最经典的暴雪射击游戏',111.00,30),(2,2,'艾尔登法环','宫崎老贼的善意开局,大树守卫',198.00,20),(2,3,'战神','又是那个光头肌肉男',169.00,15);
首先配置一个,mysql的配置文件
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3307/test_jdbtemplate?serverTimezone=UTC
jdbc.username=root
jdbc.password=root
现在在spring配置文件中进行配置数据连接池的信息:
说的话可能很乱,直接演示:
如果使用之前的导入测试类应该如下写
public class test {@Testpublic void testMethod(){ApplicationContext applicationContext= new ClassPathXmlApplicationContext("spring_jdbc.xml");JdbcTemplate jdbcTemplate= (JdbcTemplate) applicationContext.getBean("jdbcTemplate");System.out.println(jdbcTemplate);}
}
现象看一下整合的方式写法:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring_jdbc.xml")
public class test {@Autowiredprivate JdbcTemplate jdbcTemplate;@Testpublic void testMethod(){System.out.println(jdbcTemplate);}
}
可以看出可以通过注解实现ICO容器的注入。
其中@RunWith可以通过SpringJUnit4ClassRunner.class进行启动IOC容器。
JdbcTemplate主要提供以下五类方法:
先来一个插入
@Testpublic void insertMethod(){String sql="INSERT INTO `test_jdbtemplate`.`t_game` VALUES (?,?,?,?,?,?)";
// 其实插入和删除都是使用update方法jdbcTemplate.update(sql,1,4,"寂静岭","那可是护士啊",82,10);}
然后再来要给删除:
@Testpublic void deleteMethod(){String sql="DELETE FROM `test_jdbtemplate`.`t_game` WHERE `t_game`.`game_id`=?";jdbcTemplate.update(sql,4 );}
插入和删除相对来说简单一些,主要是查询:
先创建一个游戏类:
public class Game {int categoriesId;int gameId;String gamName;String gameDetails;double gamePrice;int gameCount;// 具体有参无参构造方法,set get 方法 toString 方法就不再黏贴了不然太长,用IDE快捷键很快创建出来}
现在得到一个数据的集合:
@Testpublic void selectMethod(){String sql="SELECT * FROM `test_jdbtemplate`.`t_game` ";List gameList = jdbcTemplate.query(sql, new BeanPropertyRowMapper(Game.class));for (Game game: gameList) {System.out.println(game);}
如果只返回一个对象数据:
@Testpublic void selectMethod1() {String sql = "SELECT * FROM `test_jdbtemplate`.`t_game` WHERE `t_game`.`game_id`=? ";Game game = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper(Game.class), 2);System.out.println(game);}
@Testpublic void selectMethod2() {String sql = "SELECT count(*) FROM `test_jdbtemplate`.`t_game`";Integer integer = jdbcTemplate.queryForObject(sql, Integer.class);System.out.println(integer);}
@Testpublic void selectMethod2() {String sql = "SELECT game_name,COUNT(*) FROM `test_jdbtemplate`.`t_game` WHERE game_id =? GROUP BY game_name";Map map = jdbcTemplate.queryForMap(sql,1);System.out.println(map);}
所以常用方法如下:
执行简单的增删改
int update(String sql,Object[] args)
int update(String sql,Objcet... args)
batchUpdate批量增删改
int[] batchUpdate(String[] sql)
int[] batchUpdate(String sql,List
单个简单查询
T queryForObjcet(String sql,Class type)
T queryForObjcet(String sql,Object[] args,Class type)
T queryForObjcet(String sql,Class type,Object... arg)
获取多个
//API
List queryForList(String sql,Class type)
List queryForList(String sql,Object[] args,Class type)
List queryForList(String sql,Class type,Object... arg)
查询复杂对象(封装为Map)
获取单个
Map queryForMap(String sql)
Map queryForMap(String sql,Objcet[] args)
Map queryForMap(String sql,Object... arg)
获取多个
List
查询复杂对象(封装为实体对象)
Spring jdbcTemplate是通过实现org.springframework.jdbc.core.RowMapper
这个接口来完成对entity对象映射。不过一般是通过其实现类BeanPropertyRowMapper
。
获取单个
T queryForObject(String sql,RowMapper mapper)
T queryForObject(String sql,object[] args,RowMapper mapper)
T queryForObject(String sql,RowMapper mapper,Object... arg)
获取多个
List query(String sql,RowMapper mapper)
List query(String sql,Object[] args,RowMapper mapper)
List query(String sql,RowMapper mapper,Object... arg)
事务其实又分两种:编程式事务和声明式事务。
编程式
编程式事务是通过编写程序来管理事务,程序员需要在程序中显式的指定事务的处理过程,由程序员负责事务的开启、提交和回滚等操作。
就是自己开发的时候,写事务提交,事务启动,根据不同的行为异常进行回滚等。简单的说就是自己写代码实现功能。
声明式
Spring声明式事务是通过使用Spring中的AOP技术,将事务管理逻辑从业务逻辑代码中剥离出来,使用注解等形式来声明事务,实现事务管理的功能。
简单的说就是通过配置让框架实现功能。
现象网上进行复制一些两者的区别,以及优缺点:
至于数据库的事务有涉及道的,事务时什么,以及事务的隔离级别等概念,可以看另一篇文章:传送阵
先不开启事务的话,来一个报错的:
首先看一下我的结构:
然后创建service接口,以及实现的类
// 接口public interface GameService {
// Game getGameById();void BuyGame(int customerId,int gameId);
}// 实现接口
@Service
public class GameServiceImpl implements GameService {@Qualifierprivate GameDao gameDao;public GameServiceImpl(GameDao gameDao) {this.gameDao = gameDao;}@Overridepublic void BuyGame(int customerId,int gameId) {Double price =gameDao.findGamePrice(gameId);gameDao.updateGamecount(gameId);gameDao.updateCustomerMoney(customerId,price);}
}
然后创建实现dao接口以及实现类:
// 接口
public interface GameDao {double findGamePrice(int game_id);void updateGamecount(int gameId);void updateCustomerMoney(int customerId,double price);
}// 接口实现类@Repository
public class GameDaoImpl implements GameDao {@ResourceJdbcTemplate jdbcTemplate;@Overridepublic double findGamePrice(int game_id) {String sql="SELECT game_price FROM `test_jdbtemplate`.`t_game` WHERE `game_id`=?";Double price=jdbcTemplate.queryForObject(sql,Double.class,game_id);System.out.println(price);return price;}@Override
// 这里直接默认只会购买一个游戏 毕竟主要时演示事务的效果public void updateGamecount(int gameId) {String sql="UPDATE `test_jdbtemplate`.`t_game` SET `game_count` = game_count - 1 WHERE `game_id` = ?";System.out.println("更新一些游戏表种某款被卖了游戏库存");jdbcTemplate.update(sql,gameId);}@Overridepublic void updateCustomerMoney(int customerId, double price) {String sql="UPDATE `test_jdbtemplate`.`t_customer` SET `customer_money` = customer_money-? WHERE `customer_id` = ?";System.out.println("更新一些会员的余额");jdbcTemplate.update(sql,price,customerId);}
}
首先看一下数据库会员的信息:
然后看下游戏仓库的数据:
然后调用测试类:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring_jdbc.xml")
public class test {
// @Resource(name = "jdbcTemplate")private JdbcTemplate jdbcTemplate;
//@Resource(name = "gameServiceImpl")@Autowiredprivate GameService gameService;@Testpublic void test(){gameService.BuyGame(1,1);}
}
其中数据库种会员的余额,是不可以是带符合的数,所以不够买书会报错,这个还算是可以理解,然后看一下数据库:
游戏数量是减少了,这个就是没有开启事务的一个弊端,这个过程没有执行完毕,应该游戏的数量不会变的,但是变了。还是那句话事务概念不懂的,可以看另一篇文章:传送阵。而本篇主要是演示在Spring种的JdbcTemplate中如何启动事务。
因为需要在bean中添加tx标签:所以需要如下配置:
其中添加了:
xmlns:tx="http://www.springframework.org/schema/tx"以及在 xsi:schemaLocation中添加了 http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
最后看一下实际的配置文件:
如果在是事务的时候不写切面类id,一般建议写上,毕竟好理解,如下:
然后使用注解:@Transactional,其可以ansactional 可以作用在接口、类、类方法。
作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息。
作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息。
作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效
所以说一般常用的情况是在类上或者方法上。现象还是上面的例子,至少在其中的服务实现类上添加这个事务注解:
@Service
@Transactional
public class GameServiceImpl implements GameService {@Qualifierprivate GameDao gameDao;public GameServiceImpl(GameDao gameDao) {this.gameDao = gameDao;}@Overridepublic void BuyGame(int customerId,int gameId) {
// System.out.println("-------"+gameDao);Double price =gameDao.findGamePrice(gameId);gameDao.updateGamecount(gameId);gameDao.updateCustomerMoney(customerId,price);}
}
然后运行测试代码类,也是报如下错:
然后看下数据库中,是否数据变化。
可以看出这个启动的是一个事务,那就是一个报错,然后整个事务进行回滚。
属性 | 描述 |
---|---|
propagation | 代表事务的传播行为,默认值为 Propagation.REQUIRED |
isolation | 事务的隔离级别,默认值为 Isolation.DEFAULT。 |
timeout | 事务的超时时间,默认值为 -1。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 read-only 为 true。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型。一般是编译时不回滚,运行时异常回滚。 |
noRollbackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型也只在运行时其效果。 |
然后说一下其中的某几个属性(因为都是理论直接复制黏贴了):
propagation属性:代表事务的传播行为,默认值为 Propagation.REQUIRED,其他的属性信息如下
isolation 属性:事务的隔离级别,默认值为 Isolation.DEFAULT。其它如下:
具体隔离解决了什么问题,可以看另一篇文章:传送阵。
不过说实话说了这样多的属性,以及分别,不过一般的时候使用的是默认
比如将服务类变成:
@Transactional(readOnly = true)
还有就是前面启动事务,然后更新用户钱的时候报错,然后事务整体回滚,然后可以如下:
@Service
// DataIntegrityViolationException是之前事务混滚的时候报,然后使用如果出现这个异常不回滚,然后运行看一下结果:noRollbackFor值为数组,如果只有一个也可以如下写:noRollbackFor = DataIntegrityViolationException.class。
@Transactional(noRollbackFor = {DataIntegrityViolationException.class})
public class GameServiceImpl implements GameService {@Qualifierprivate GameDao gameDao;public GameServiceImpl(GameDao gameDao) {this.gameDao = gameDao;}@Overridepublic void BuyGame(int customerId,int gameId) {
// System.out.println("-------"+gameDao);Double price =gameDao.findGamePrice(gameId);gameDao.updateGamecount(gameId);gameDao.updateCustomerMoney(customerId,price);}
}
运行的时候依然报错,让事务没有回滚。
其它的也就不再一一演示了
但是涉及到一个事务的传播,这个需要演示一下:
然后创建一个新的需求:
public interface CleanMoney {void cleanmoney(Integer[] gameIds, Integer customerId);
}@Service
@Transactional
public class CleanMoneyImpl implements CleanMoney {@AutowiredGameService gameService;@Overridepublic void cleanmoney(Integer[] gameIds, Integer customerId) {for (Integer gameId : gameIds) {gameService.BuyGame( customerId,gameId);}}
}
另一个服务的代码如下:
@Service
@Transactional
public class GameServiceImpl implements GameService {@Qualifierprivate GameDao gameDao;public GameServiceImpl(GameDao gameDao) {this.gameDao = gameDao;}@Overridepublic void BuyGame(int customerId,int gameId) {
// System.out.println("-------"+gameDao);Double price =gameDao.findGamePrice(gameId);gameDao.updateGamecount(gameId);gameDao.updateCustomerMoney(customerId,price);}
}
然后调用方法:
public class test {private JdbcTemplate jdbcTemplate;@Autowiredprivate CleanMoney cleanMoney;@Test
// 这里吕布有200块 其它游戏都是100多,所以至少可以购买一个游戏public void test(){Integer[] gameIds={1,2};cleanMoney.cleanmoney(gameIds,2);}
}
可见虽然下面也启动的事务,虽然两个服务都有开启了事务,但是还是以CleanMoney上的事务为准的。
但是如果如下设置:
@Service
@Transactional( propagation = Propagation.REQUIRES_NEW)
public class GameServiceImpl implements GameService {@Qualifierprivate GameDao gameDao;public GameServiceImpl(GameDao gameDao) {this.gameDao = gameDao;}@Overridepublic void BuyGame(int customerId,int gameId) {
// System.out.println("-------"+gameDao);Double price =gameDao.findGamePrice(gameId);gameDao.updateGamecount(gameId);gameDao.updateCustomerMoney(customerId,price);}
}
虽然报错,但是可以购买一本书。
@Transactional 应用在非 public 修饰的方法上,就会失效
@Transactional 注解属性 propagation 设置错误,这种失效是由于配置错误。
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
@Transactional 注解属性 rollbackFor 设置错误。一般不会设置这个属性,而是使用默认值。
同一个类中方法调用,导致@Transactional失效。其实这还是由于使用Spring AOP代理造成的,因为只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。
开发中避免不了会对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。这也是经常犯错误的一个地方。
异常被你的 catch“吃了”导致@Transactional失效。
算是一种会影响导致@Transactional失效的可能性那就是数据库引擎不支持事务。
这个就不演示了,而是直接用一个xml配置文件聊了:
不过一般的时候使用注解进行开发的情况会多一些,xml目前了解即可,至少在需要使用的时候会用。