现在想对如下表进行转账操作,AA给BB转100,代码如下:
public class TransactionTest { // 未考虑事务的情况 @Test public void test() throws Exception { String sql1 = "update user_table set balance = balance - 100 where user = ?"; update(sql1, "AA"); String sql2 = "update user_table set balance = balance + 100 where user = ?"; update(sql2, "BB"); } public void update(String sql, Object ... args) { Connection connection = null; PreparedStatement ps = null; try { connection = JDBCUtils.getConnection(); ps = connection.prepareStatement(sql); for (int i = 0; i < args.length; i++) { ps.setObject(i+1, args[i]); } ps.execute(); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(connection, ps); } } }结果:
这样看起来目的也达到了,但如果在AA转出钱之后程序出现了异常就会导致BB收不到钱()下面代码模拟了异常:
@Test public void test() throws Exception { String sql1 = "update user_table set balance = balance - 100 where user = ?"; update(sql1, "AA"); // 异常出现 System.out.println(1/0); String sql2 = "update user_table set balance = balance + 100 where user = ?"; update(sql2, "BB"); }我们再来查看数据库,显然钱对不上了:
所以,要引入事务
也就是说,我们希望故障发生时这些数据能回滚到初始状态(即二人钱都是1000),想让AA转钱,BB收钱之后再把数据提交,而不是AA转钱,BB收钱之间提交,因为这时即使回滚也回不到他们两人都是1000的状态了(此时AA:900,BB:1000)。
回滚只能回到最近一次提交的状态。
因此,要想避免自动提交就要避免上述三点:
因为DML操作居多,所以不用太在意 直接设置即可 在AA转出之后,先不要关闭连接,而是接着用这个连接去操作BB当异常出现时,可以看到数据库并没有更改:
https://www.bilibili.com/video/BV1eJ411c7rf?p=40
https://www.bilibili.com/video/BV1eJ411c7rf?p=41
通常只需保证避免脏读就行。
在Cusotmer查询的时候,传了Customer.class这个参数,但我们操作Customer表,再传这个参数就有些多余了,我们希望能不传参也能实现查询的功能:
@Override public Customer getCustomerById(Connection connection, int id) { String sql = "select id, name, email, birth from customers where id = ?"; Customer customer = queryMulti(connection, Customer.class, sql, id); return customer; }该如何做呢?
先给BaseDAO加上泛型:
然后让Impl继承父类并指明泛型:
再把之前的泛型方法修改:
我们的目的是不同的类继承BaseDAO就传不同的泛型,在不同的类(如Customer、Order等)调用update等方法之前,clazz就已经获得了类型,因此在BaseDAO中设置了属性,并希望在它的子类(也就是Customer、Order等)创建对象时就把这个clazz的类型确定下来,这要如何做呢?:
在这个时机赋值有如下几种方法:
显式赋值,但不可行,因为一行代码搞不定代码块赋值,可行,但要是非静态构造器,可行这里选用代码块。
使用反射
在new子类对象的时候,会调用super()加载父类结构,因而会加载父类的构造器和代码块,所以在BaseDAO中添加如下代码块:
{ Type genericSuperclass = this.getClass().getGenericSuperclass(); System.out.println(genericSuperclass); ParameterizedType paramType = (ParameterizedType) genericSuperclass; System.out.println(paramType); Type[] TypeArguments = paramType.getActualTypeArguments();// 获取父类的泛型参数 System.out.println(TypeArguments); clazz = (Class<T>) TypeArguments[0]; // 泛型的第一个参数 当new Customer对象时,它为Customer System.out.println("clazz: " + clazz); }当程序运行时:
@Test public void testSelect() { Connection connection = null; try { connection = JDBCUtils.getConnection(); Customer customer = CustomerTest.getCustomerById(connection, 20); System.out.println(customer); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(connection, null); } }结果:
https://www.bilibili.com/video/BV1eJ411c7rf?p=45
使用这个需要把相应的驱动导入并build path。
这种方式要求我们在src下新建一个文件:c3p0-config.xml,文件名一定要是这个。
然后在里面写配置信息,例如:
<?xml version="1.0" encoding="UTF-8" ?> <c3p0-config> <named-config name="hc3p0"> <!-- 连接所需的4个信息--> <property name="driverClass">com.mysql.jdbc.Driver</property> <property name="jdbcUrl">jdbc:mysql://localhost:3306/test</property> <property name="user">root</property> <property name="password">root</property> <!-- 数据库连接池管理的信息--> <!-- 当连接池中的连接数不够时c3p0向服务器一次性申请的连接数--> <property name="acquireIncrement">5</property> <!-- 数据库连接池中初始化的连接数--> <property name="initialPoolSize">10</property> <!-- c3p0数据库连接池维护的最少连接数--> <property name="minPoolSize">10</property> <!-- c3p0数据库连接池维护的最多连接数--> <property name="maxPoolSize">100</property> <!-- 维护的最多Statement个数--> <property name="maxStatements">50</property> <!-- 每个连接中可以最多使用的Statement的个数--> <property name="maxStatementsPerConnection">2</property> </named-config> </c3p0-config> // 方式二 使用配置文件 @Test public void test2() throws SQLException { ComboPooledDataSource cpds = new ComboPooledDataSource("hc3p0"); // 这里的参数要和上面的xml文件<named-config name="hc3p0">保持一致 Connection conn = cpds.getConnection(); System.out.println(conn); }结果:
现在就可以使用连接池的连接来执行之前的操作了:
将之前用的JDBCUtils拷贝一份到c3p0包下,然后对获取连接的操作做如下更改:
public static Connection getConnection() throws Exception{ Connection conn = cpds.getConnection(); System.out.println("使用了连接池"); return conn; }之后测试功能,这里就简写了,没有关闭连接:
@Test public void testSelectC3p0() throws Exception { Connection connection = JDBCUtils.getConnection(); Customer customer = CustomerTest.getCustomerById(connection, 20); System.out.println(customer); }结果:
同样,需要把驱动导入并build path:
需要写properties文件:
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql:///test username=root password=root initialSize=10 @Test public void test2() throws Exception { Properties pros = new Properties(); // 方式 1 InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("dbcp.properties"); // 方式 2 // FileInputStream is = new FileInputStream(new File("src/dbcp.properties")); pros.load(is); DataSource source = BasicDataSourceFactory.createDataSource(pros); Connection conn = source.getConnection(); System.out.println(conn); } 结果: jdbc:mysql:///test, UserName=root@localhost, MySQL Connector Javahttps://www.bilibili.com/video/BV1eJ411c7rf?p=49
老规矩,先加载驱动:
这里就不演示方法1了,直接上读取配置文件的代码:
driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql:///test username=root password=root initialSize=8 @Test public void test1() throws Exception { Properties pros = new Properties(); pros.load(ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties")); DataSource source = DruidDataSourceFactory.createDataSource(pros); Connection conn = source.getConnection(); System.out.println(conn); } 结果: com.mysql.jdbc.JDBC4Connection@243c4f91连接池解决的是获取数据库的连接,而增删改查(CURD)是很常见的操作,因此Java将这些操作做了封装,只需调用相应的方法就可以了。
说明:
这里的连接池随便选哪个都行,这里使用的是dbcp在插入中文的时候数据库里出现了??,解决方案是在properties文件中的url后面加上characterEncoding=utf-8,即:url=jdbc:mysql:///test?characterEncoding=utf-8其实这个update的源码和我们之前写的源码核心部分是差不多的,只是人家的代码健壮性好一些update方法的返回值也是被影响的行数我们能发现这个QueryRunner主要有有无参的和有参的两种构造器,有参的需要传一个DataSource接口的实现类,下面来看看都有什么区别:
无参构造器:就像上面的insert操作一样,无参构造器是这样的:QueryRunner runner = new QueryRunner();这时如果要调用update或者query方法需要把Connection传进去,并且执行完之后不会帮你把Connection关闭,你得自己关闭,但是结果集和Statement会帮你关闭,通常用这种方式来完成和事务相关的操作;有参构造器:QueryRunner runner = new QueryRunner(DataSource);这时如果要调用update或者query方法就不需要再把Connection传进去,因为DataSource就能获取到Connection,并且执行完之后会帮你把Connection、结果集和Statement一并关闭,这种方式可以用来完成一次操作就结束的业务。为了进行select操作,我们发现了几个重载的query方法:
现在推荐使用的就那么几个,这里选择上图蓝色行的那个。
分析:
第一个参数是Connection的实现类的对象,这个不论是我们自己写的还是调用连接池都能获得第二个参数就是sql语句第四个参数是与sql配套的可变参数第三个参数点进去一看是个接口: 于是查看其实现类,有好几个!:由于查询的时候需求是不同的,根据不同的需求来选用不同的实现类,如果想让他返回一个对象,就可以用BeanHandler,注意它的构造器要求传一个你想把数据保存到那个表的类的class:
BeanHandler beanHandler = new BeanHandler<>(Customer.class); // select测试 public void test3() { Connection conn = null; try { QueryRunner runner = new QueryRunner(); conn = JDBCUtils.getConnection(); String sql = "select name, email from cUstomers where id = ?"; BeanHandler<Customer> beanHandler = new BeanHandler<>(Customer.class); Customer customer = runner.query(conn, sql, beanHandler, 10); System.out.println(customer); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn, null); } } 结果: Customer{id=0, name='周杰伦', email='zhoujl@sina.com', birth=null}以上是返回一条记录,那么返回多条记录也是同理:
可以使用BeanListHandler。
@Test public void test4() { Connection conn = null; try { QueryRunner runner = new QueryRunner(); conn = JDBCUtils.getConnection(); String sql = "select name, email from cUstomers where id < ?"; BeanListHandler<Customer> beanListHandler = new BeanListHandler<>(Customer.class); List<Customer> customerList = runner.query(conn, sql, beanListHandler, 10); customerList.forEach(s -> System.out.println(s)); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn, null); } } 结果: Customer{id=0, name='汪峰', email='wf@126.com', birth=null} Customer{id=0, name='王菲', email='wangf@163.com', birth=null} Customer{id=0, name='林志玲', email='linzl@gmail.com', birth=null} Customer{id=0, name='汤唯', email='tangw@sina.com', birth=null} Customer{id=0, name='成龙', email='Jackey@gmai.com', birth=null} Customer{id=0, name='迪丽热巴', email='reba@163.com', birth=null} Customer{id=0, name='刘亦菲', email='liuyifei@qq.com', birth=null} Customer{id=0, name='陈道明', email='bdf@126.com', birth=null}其实这个BeanHandler内部的实现和我们之前写的差不多,见下图:
以这个作为query的参数来保存查询结果是将属性和值以键值对的形式存储的:
// select测试 使用MapHandler @Test public void test5() { Connection conn = null; try { QueryRunner runner = new QueryRunner(); conn = JDBCUtils.getConnection(); String sql = "select name, email from cUstomers where id = ?"; MapHandler handler = new MapHandler(); Map<String, Object> query = runner.query(conn, sql, handler, 10); System.out.println(query); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn, null); } } 结果: {name=周杰伦, email=zhoujl@sina.com}有这种需求的时候可以使用ScalarHandler :
// select 特殊表达式,如count等 @Test public void test6() { Connection conn = null; try { QueryRunner runner = new QueryRunner(); conn = JDBCUtils.getConnection(); String sql = "select count(*) from cUstomers"; ScalarHandler handler = new ScalarHandler(); long rs = (long) runner.query(conn, sql, handler); System.out.println(rs); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn, null); } } 结果: 16如果已提供的ResultSetHandler的实现类无法满足现有的要求,那么可以自己定义一个实现类。
我们自定义这样一个实现类,将count查到的结果*10(没有实际意义,只是演示一下):
// 自定义ResultSetHandler的实现类 @Test public void test7() { Connection conn = null; try { QueryRunner runner = new QueryRunner(); conn = JDBCUtils.getConnection(); String sql = "select count(*) from cUstomers"; ResultSetHandler<Long> handler = new ResultSetHandler<>(){ @Override public Long handle(ResultSet rs) throws SQLException { long count = 0; if (rs.next()) count = (long) rs.getObject(1); return count*10; } }; long rs = (long) runner.query(conn, sql, handler); System.out.println(rs); } catch (Exception e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(conn, null); } } 结果: 160在org.apache.commons.dbutils.DbUtils;下可以关闭资源:
public static void closeResource(Connection conn, PreparedStatement ps, ResultSet result) { DbUtils.closeQuietly(conn); DbUtils.closeQuietly(ps); DbUtils.closeQuietly(result); }这个就比较简单了,不必多说。