• day16 事务


    Author:相忠良
    Email: ugoood@163.com
    起始于:June 8, 2018
    最后更新日期:June 11, 2018

    声明:本笔记依据传智播客方立勋老师 Java Web 的授课视频内容记录而成,中间加入了自己的理解。本笔记目的是强化自己学习所用。若有疏漏或不当之处,请在评论区指出。谢谢。
    涉及的图片,文档写完后,一次性更新。

    1. 事务

    1

    发sql时,把多个sql放在Start transactioncommit之间即可。

    试验准备:

    create table account(
    	id int primary key auto_increment,
    	name varchar(40),
    	money float
    )character set utf8 collate utf8_general_ci;
    
    insert into account(name,money) values('aaa',1000);
    insert into account(name,money) values('bbb',1000);
    insert into account(name,money) values('ccc',1000);
    

    现在,a向b转账100元,操作如下:

    start transaction;
    update account set money=money-100 where name='aaa';
    

    关掉连接,重新登录数据库查看,aaa 账户的 money 还是 1000。
    只有下面这样才行:

    start transaction;
    update account set money=money-100 where name='aaa';
    update account set money=money+100 where name='bbb';
    commit;
    

    执行到 commit,上面2条sql才算真正执行,而不是回滚,这就是事务(控制多条sql作为整体执行)。

    rollback 可以手动回滚,而不是异常时,事务在数据库中自动回滚。

    当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
    JDBC控制事务语句:

    • Connection.setAutoCommit(false); 相当于 start transaction
    • Connection.rollback(); rollback
    • Connection.commit(); commit

    程序中控制事务的例子如下:

    public class Demo1 {
    	/**	 
    	 a--->b 100
    	 */
    	public static void main(String[] args) throws SQLException {
    
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		try{
    			conn = JdbcUtils.getConnection();
    			conn.setAutoCommit(false);    //start transaction;
    
    			String sql1 = "update account set money=money-100 where name='aaa'";
    			String sql2 = "update account set money=money+100 where name='bbb'";
    
    			st = conn.prepareStatement(sql1);
    			st.executeUpdate();
    
    			int x = 1/0; // <-- 产生异常
    
    			st = conn.prepareStatement(sql2);
    			st.executeUpdate();
    
    			conn.commit(); // commit
    		}finally{
    			JdbcUtils.release(conn, st, rs);
    		}
    	}
    }
    

    1.1 事务回滚点

    手动回滚,按下面例子,只想从第二条sql开始回滚,方法就是:

    1. 设置回滚点Savepoint
    2. 手动设置 commit。

    例子如下:

    public class Demo2 {
    	public static void main(String[] args) throws SQLException {
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		Savepoint sp = null; // 回滚点对象
    		try {
    			conn = JdbcUtils.getConnection();
    			conn.setAutoCommit(false); // start transaction;
    
    			String sql1 = "update account set money=money-100 where name='aaa'";
    			String sql2 = "update account set money=money+100 where name='bbb'";
    			String sql3 = "update account set money=money+100 where name='ccc'";
    
    			st = conn.prepareStatement(sql1);
    			st.executeUpdate();
    
    			sp = conn.setSavepoint(); // <-- 2. 设置回滚点
    
    			st = conn.prepareStatement(sql2);
    			st.executeUpdate();
    
    			int x = 1 / 0; 		// <-- 1. 产生异常
    
    			st = conn.prepareStatement(sql3);
    			st.executeUpdate();
    
    			conn.commit(); // commit
    		} catch (Exception e) {
    			e.printStackTrace();
    			conn.rollback(sp); 	// <-- 3. 回滚
    			conn.commit(); 		// <-- 4. 手动回滚后,一定要记得提交事务
    		} finally {
    			JdbcUtils.release(conn, st, rs);
    		}
    	}
    }
    

    1.2 事务四大特性 ACID

    若一个数据库号称支持事务,那它必然支持 ACID;反过来说,若某数据库支持 ACID,那这个数据库也是支持事务的。

    • 原子性(Atomicity) 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生;
    • 一致性(Consistency) 事务前后数据的完整性必须保持一致;
    • 隔离性(Isolation) 事务的隔离性是多个用户 并发访问 数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离;
    • 持久性(Durability) 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

    1.2.1 隔离性 - 脏读 - 不可重复读 - 虚读(幻读)

    脏读:指一个事务读取了另外一个事务未提交的数据。(最危险)

    故事:这是非常危险的,假设 A 向 B 转帐 100 元,对应 sql 语句如下所示:

    1. update account set money=money+100 while name='b';
    2. update account set money=money-100 while name='a';

    当第 1 条 sql 执行完,第 2 条还没执行(A 未提交时),如果此时 B 查询自己的帐户,就会发现自己多了 100 元钱。如果 A 等 B 走后再回滚,B 就会损失 100 元。

    下面介绍的不可重复读和幻读,有些情况下是没问题的,但有时会有问题。

    不可重复读:在一个事务内读取表中的某一行数据,多次读取结果不同。 也指读表中同一条数据,结果不同。

    故事:中国人民银行生成开启生成报表这个事务,报送克强总理1000亿RMB,在报送近平主席前,生成报表这个事务未结束期间,有客户存了200亿RMB并该客户完成了他的事务,现在又生成近平主席的报表显示为1200亿。问题出现了:两位领导要打架的。困惑就是:哪次查询时是准确的呢? 这就是不可重复读所产生的问题。

    虚读(幻读):是指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。 也指所读的表的记录数在变化。

    故事:人口普查系统正生成报表,开启了一个事务。该系统在这个事务中需生成多个报表。可能发生这样的事:生成第一个报表,显示中国有10亿人,但生成第二个报表期间,有人往数据库中插入了数据,统计结果显示有11亿人。困惑来了:到底以哪个为准呢?这就是幻读产生的问题。

    1.3 事务的隔离级别

    根据上节介绍的,若无隔离性,数据库可能出现的三种问题,针对问题的解决,提出了事务隔离级别。隔离级别的提出,主要在解决问题的基础上,尽可能的不过多损失数据库性能。

    数据库共定义了四种隔离级别:

    • Serializable:可避免脏读、不可重复读、虚读情况的发生。(串行化)
    • Repeatable read:可避免脏读、不可重复读情况的发生。(可重复读)
    • Read committed:可避免脏读情况发生(读已提交)。
    • Read uncommitted:最低级别,以上情况均无法保证。(读未提交)

    事务隔离性的设置语句:

    • set transaction isolation level 设置事务隔离级别
    • select @@tx_isolation 查询当前事务隔离级别

    方立勋老师开启了2个mysql客户端,进行了模拟。模拟过程这里不表述了。

    编程序时,获得的 connection:

    • 若是 mysql 的链接,默认隔离级别是repeatable read,mysql完全支持上述4种隔离级别;
    • 若是 oracle 的链接,默认隔离级别是 read committed,且oracle只支持Serializable和Read committed这两种隔离级别。

    编程中,用JDBC设置隔离级别: conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);

    示例代码:

    public class Demo3 {
    	public static void main(String[] args) throws SQLException, InterruptedException {
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		Savepoint sp = null;
    		try{
    			conn = JdbcUtils.getConnection();   //mysql repeatable read
    			conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
    			conn.setAutoCommit(false);    //start transaction;
    
    			String sql = "select * from account";
    			conn.prepareStatement(sql).executeQuery();
    
    			Thread.sleep(1000*20);
    
    			conn.commit();
    		}finally{
    			JdbcUtils_DBCP.release(conn, st, rs);
    		}
    	}
    }
    

    2. 数据库连接池

    下图展示了无数据库连接池时的缺点:
    2

    下图是有连接池的情形:
    3

    有连接池后,数据库就不必为每个用户创建连接,而仅仅在一开始生成一些连接(假如20个),并将这些连接放入连接池,其他用户只从池中拿连接,用完后还到池中。(这个故事主要考虑,数据库自己创建1个连接需消耗很多资源,10万用户申请,就创建10万次连接,数据库本身做本职工作就很繁忙,再去频繁地创建若此多的链接,数据库极有可能被累死!我们要做的是尽量减轻数据库服务器的负担。)

    故事: 我们希望执行`conn.close();`时,连接还回连接池,但事实是conn是mysql提供的链接,执行close方法时,那个连接将还给mysql,而不是连接池。 当发现对象的方法不够我们用时,我们需增强那个方法。办法有: 1. 写一个Connection子类,覆盖close方法,增强close方法; 2. 用包装设计模式; 3. 用动态代理。

    通常子类的方式不可行,原因是很难将父类对象信息导入子类对象中,除非父类对象封装的信息极少。

    包装设计模式步骤(我自己的经验,想象一下BufferedReader的用法,就是用构造函数接收被包装对象):

    1. 定义一个类,实现与被增强对象相同的接口;
    2. 在类中定义一个变量,记住被增强对象;
    3. 定义一个构造函数,接收被增强对象;
    4. 覆盖想增强的方法;
    5. 对于不想增强的方法,直接调用目标对象(被增强对象)的方法。

    包装模式例子:

    class MyConnection implements Connection{ // step 1
      private Connection conn; // step 2
      public MyConnection(Connection conn){ // step 3
        this.conn = conn;
      }
      public void close(){ // step 4
        list.add(this.conn);
      }
    
      // step 5
      @Override
      public void commit() throws SQLException{
        this.conn.commit(); // 调用的是 mysql 提供的 commit 方法
      }
    
      @Override
      public void clearWarnings() throws SQLException{
        this.conn.clearWarnings(); // 调用的是 mysql 提供的 clearWarnings 方法
      }
    
      /*
        ...
        ...
        后面不想增强的方法均照 step 5 处理,极有可能代码量超大,这也是包装模式处理此类问题的缺点
      */
    }
    

    使用经包装(装饰)后的conn对象:

    MyConnection my = new MyConnection(conn);
    

    当我们用my这个链接对象时,它的close方法就是我们自己写的方法了。

    下面代码时动态代理方式(这里仅做个记录):

    proxyConn = (Connection) Proxy.newProxyInstance(this.getClass()
        .getClassLoader(), conn.getClass().getInterfaces(),
        new InvocationHandler() {
          // 此处为内部类,当close方法被调用时将conn还回池中,其它方法直接执行
          public Object invoke(Object proxy, Method method,
              Object[] args) throws Throwable {
            if (method.getName().equals("close")) {
              pool.addLast(conn);
              return null;
            }
            return method.invoke(conn, args);
          }
        });
    

    3. 常用开源数据库连接池(DataSource 接口的开源实现)

    数据源 = 数据库连接池

    常见开源数据库连接池有:

    • DBCP 数据库连接池
    • C3P0 数据库连接池
    • Apache Tomcat 内置的连接池(用的是 apache DBCP)

    3.1 Apache DBCP 数据源

    若想用 Apache DBCP,应用程序应增加如下 2 个 jar 文件:

    • Commons-dbcp.jar:连接池的实现
    • Commons-pool.jar:连接池实现的依赖库

    下面是 dbcp-1.2.2 开发包中的 dbcpconfig.properties文件(实验时,需将该文件 copy 到 src 目录下),其作用同以前我们自己写的 db.properties 一样,是存放配置 dbcp 连接哪种数据库、url、用户名、密码等信息的一种配置文件。如下:

    #连接设置
    driverClassName=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/jdbc
    username=root
    password=
    
    #<!-- 初始化连接 -->
    initialSize=10
    
    #最大连接数量
    maxActive=50
    
    #<!-- 最大空闲连接 -->
    maxIdle=20
    
    #<!-- 最小空闲连接 -->
    minIdle=5
    
    #<!-- 超时等待时间以毫秒为单位 6000毫秒/1000等于60秒 -->
    maxWait=60000
    
    
    #JDBC驱动建立连接时附带的连接属性属性的格式必须为这样:[属性名=property;]
    #注意:"user" 与 "password" 两个属性会被明确地传递,因此这里不需要包含他们。
    connectionProperties=useUnicode=true;characterEncoding=utf8
    
    #指定由连接池所创建的连接的自动提交(auto-commit)状态。
    defaultAutoCommit=true
    
    #driver default 指定由连接池所创建的连接的只读(read-only)状态。
    #如果没有设置该值,则“setReadOnly”方法将不被调用。(某些驱动并不支持只读模式,如:Informix)
    defaultReadOnly=
    
    #driver default 指定由连接池所创建的连接的事务级别(TransactionIsolation)。
    #可用值为下列之一:(详情可见javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
    defaultTransactionIsolation=READ_COMMITTED
    

    重新设置 JdbcUtils.java,用连接池的方式:

    package cn.wk.utils;
    
    import java.io.InputStream;
    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Statement;
    import java.util.Properties;
    
    import javax.sql.DataSource;
    
    import org.apache.commons.dbcp.BasicDataSourceFactory;
    
    public class JdbcUtils_DBCP {
    
    	private static DataSource ds = null;
    
    	static {
    		try {
    			// 读配置文件 dbcpconfig.properties
    			InputStream in = JdbcUtils_DBCP.class.getClassLoader()
    					.getResourceAsStream("dbcpconfig.properties");
    			Properties prop = new Properties();
    			prop.load(in);
    
    			BasicDataSourceFactory factory = new BasicDataSourceFactory();
    			ds = factory.createDataSource(prop);
    		} catch (Exception e) {
    			throw new ExceptionInInitializerError(e); // 异常转换成错误
    		}
    	}
    
    	public static Connection getConnection() throws SQLException {
    		return ds.getConnection(); // dbcp conn.close() commit()
    	}
    
    	public static void release(Connection conn, Statement st, ResultSet rs) {
    		// 模板代码
    		if (rs != null) {
    			try {
    				rs.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			rs = null;
    		}
    		if (st != null) {
    			try {
    				st.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			st = null;
    		}
    		if (conn != null) {
    			try {
    				conn.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    3.2 C3P0 数据源 (Spring 内置数据源)

    C3P0 的jar包在c3p0-0.9.2-pre1中,导入如下2个jar包:

    • c3p0-0.9.2-pre1.jar
    • mchange-commons-0.2.jar

    C3P0数据源配置文件名为c3p0-config.xml,可放在src目录下,C3P0自己会找到它。

    c3p0-config.xml例子如下:

    <c3p0-config>
    	<default-config>
    		<property name="driverClass">com.mysql.jdbc.Driver</property>
    		<property name="jdbcUrl">jdbc:mysql://localhost:3306/day16</property>
    		<property name="user">root</property>
    		<property name="password">root</property>		
    		<property name="initialPoolSize">10</property>
    		<property name="maxIdleTime">30</property>
    		<property name="maxPoolSize">20</property>
    		<property name="minPoolSize">5</property>
    		<property name="maxStatements">200</property>
    	</default-config>
    
    	<named-config name="mysql">
    		<property name="acquireIncrement">50</property>
    		<property name="initialPoolSize">100</property>
    		<property name="minPoolSize">50</property>
    		<property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching -->
    		<property name="maxStatements">0</property>
    		<property name="maxStatementsPerConnection">5</property>
    	</named-config>
    
    	<named-config name="oracle">
    		<property name="acquireIncrement">50</property>
    		<property name="initialPoolSize">100</property>
    		<property name="minPoolSize">50</property>
    		<property name="maxPoolSize">1000</property><!-- intergalactoApp adopts a different approach to configuring statement caching -->
    		<property name="maxStatements">0</property>
    		<property name="maxStatementsPerConnection">5</property>
    	</named-config>
    </c3p0-config>
    

    最上面的<default-config>是默认配置,使用方法如下:

    ComboPooledDataSource ds =  new ComboPooledDataSource();
    

    若想用<named-config name="oracle">的配置,使用方法如下:

    ComboPooledDataSource ds =  new ComboPooledDataSource("oracle");
    

    看起来非常方便。

    完整的 C3P0 连接建立代码JdbcUtils_C3P0如下:

    package cn.wk.utils;
    
    import java.sql.Connection;
    import java.sql.ResultSet;
    import java.sql.SQLException;
    import java.sql.Statement;
    
    import com.mchange.v2.c3p0.ComboPooledDataSource;
    
    public class JdbcUtils_C3P0 {
    
    	private static ComboPooledDataSource ds = null;
    	static {
    		try {
    			ds = new ComboPooledDataSource();
    		} catch (Exception e) {
    			throw new ExceptionInInitializerError(e); // 异常转换成错误
    		}
    	}
    
    	public static Connection getConnection() throws SQLException {
    		return ds.getConnection();
    	}
    
    	public static void release(Connection conn, Statement st, ResultSet rs) {
    		// 模板代码
    		if (rs != null) {
    			try {
    				rs.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			rs = null;
    		}
    		if (st != null) {
    			try {
    				st.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			st = null;
    		}
    		if (conn != null) {
    			try {
    				conn.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    }
    

    测试代码:

    public class Demo4 {
    	public static void main(String[] args) throws SQLException,
    			InterruptedException {
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		try {
    			conn = JdbcUtils_C3P0.getConnection();
    			System.out.println(conn.getClass().getName());
    		} finally {
    			JdbcUtils_C3P0.release(conn, st, rs);
    		}
    	}
    }
    

    4. 编写自己的 JDBC 框架

    4.1 元数据 - DataBaseMetaData

    元数据:数据库、表、列的定义信息。
    Connection.getDatabaseMetaData()

    DataBaseMetaData对象

    • getURL():返回一个String类对象,代表数据库的URL。
    • getUserName():返回连接当前数据库管理系统的用户名。
    • getDatabaseProductName():返回数据库的产品名称。
    • getDatabaseProductVersion():返回数据库的版本号。
    • getDriverName():返回驱动驱动程序的名称。
    • getDriverVersion():返回驱动程序的版本号。
    • isReadOnly():返回一个boolean值,指示数据库是否只允许读操作。

    ParameterMetaData对象,获取 sql 语句参数的元数据。

    以上2个元数据对象例子如下:

    public class Demo5 {
    	public static void main(String[] args) throws SQLException {
    
    		Connection conn = JdbcUtils_C3P0.getConnection();
    
    		// 获取数据库的元数据
    		DatabaseMetaData meta = conn.getMetaData();		
    		System.out.println(meta.getDatabaseProductName());
    
    		// 获取参数元数据
    		String sql = "insert into user(id,name) values(?,?)";
    		PreparedStatement st = conn.prepareStatement(sql);
    		ParameterMetaData para_meta = st.getParameterMetaData();		
    		System.out.println(para_meta.getParameterCount());
    		System.out.println(para_meta.getParameterType(1)); // mysql不支持获得类型,抛异常
    	}
    }
    

    ResultSetMetaData对象(重要,后面案例用到),结果集元数据:

    • getColumnCount() 返回resultset对象的列数
    • getColumnName(int column) 获得指定列的名称
    • getColumnTypeName(int column) 获得指定列的类型

    4.2 做自己的 jdbc 框架

    准备:
    模拟环境,先弄一个cn.wk.domain.Account的javabean:

    package cn.wk.domain;
    
    public class Account {
    	private int id;
    	private String name;
    	private double money;
    	public int getId() {return id;}
    	public void setId(int id) {this.id = id;}
    	public String getName() {return name;}
    	public void setName(String name) {this.name = name;}
    	public double getMoney() {return money;}
    	public void setMoney(double money) {this.money = money;}
    }
    

    dao 层方法大致代码:
    注意到:crud 变化的是 sql 和 st.set 其余代码均相同

    public void add(Account a) throws SQLException{
      Connection conn = null;
      PreparedStatement st = null;
      ResultSet rs = null;
      try {
        conn = JdbcUtils_DBCP.getConnection();
        String sql = "(?,?,?)";
        st.setInt(1, a.getId());
        st.setString(2, a.getName());
        st.setDouble(3, a.getMoney());
    
        st.executeUpdate();
      } finally {
        JdbcUtils_DBCP.release(conn, st, rs);
      }
    }
    
    public void delete(int id) throws SQLException{
      Connection conn = null;
      PreparedStatement st = null;
      ResultSet rs = null;
      try {
        conn = JdbcUtils_DBCP.getConnection();
        String sql = "delete from where id=?";
        st.setInt(1, id);			
    
        st.executeUpdate();
      } finally {
        JdbcUtils_DBCP.release(conn, st, rs);
      }
    }
    

    现在要做优化,抽出相同的部分。

    重写了cn.wk.utils.JdbcUtils,重点在该工具类的release方法的后面, 涉及到以下知识点:
    自己 = 框架编写者

    1. 自己的框架提供个接口(处理器),供用户填写;
    2. 自己写好了常用的处理器实现;
    3. 使用元数据;
    4. 使用反射技术,根据结果集字段名向对应的bean的域中写入数据。
    package cn.wk.utils;
    
    import java.io.InputStream;
    import java.lang.reflect.Field;
    import java.sql.Connection;
    import java.sql.PreparedStatement;
    import java.sql.ResultSet;
    import java.sql.ResultSetMetaData;
    import java.sql.SQLException;
    import java.sql.Statement;
    import java.util.Properties;
    
    import javax.sql.DataSource;
    
    import org.apache.commons.dbcp.BasicDataSourceFactory;
    
    public class JdbcUtils {
    
    	private static DataSource ds = null;
    
    	static {
    		try {
    			// 读配置文件 dbcpconfig.properties
    			InputStream in = JdbcUtils.class.getClassLoader()
    					.getResourceAsStream("dbcpconfig.properties");
    			Properties prop = new Properties();
    			prop.load(in);
    
    			BasicDataSourceFactory factory = new BasicDataSourceFactory();
    			ds = factory.createDataSource(prop);
    		} catch (Exception e) {
    			throw new ExceptionInInitializerError(e); // 异常转换成错误
    		}
    	}
    
    	public static Connection getConnection() throws SQLException {
    		return ds.getConnection(); // dbcp conn.close() commit()
    	}
    
    	public static void release(Connection conn, Statement st, ResultSet rs) {
    		// 模板代码
    		if (rs != null) {
    			try {
    				rs.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			rs = null;
    		}
    		if (st != null) {
    			try {
    				st.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    			st = null;
    		}
    		if (conn != null) {
    			try {
    				conn.close();
    			} catch (Exception e) {
    				e.printStackTrace();
    			}
    		}
    	}
    
    	/* 抽取 增删改 的公共代码 */
    
    	// add delete update 都调用下面方法,变化的部分 sql , params
    
    	// String sql="insert into account(id,name,money) values(?,?,?)";
    	// object[]{1,"aaa","1000"}
    	public static void update(String sql, Object params[]) throws SQLException {
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		try {
    			conn = getConnection();
    			st = conn.prepareStatement(sql);
    			for (int i = 0; i < params.length; i++)
    				st.setObject(i + 1, params[i]);
    			st.executeUpdate();
    		} finally {
    			release(conn, st, rs);
    		}
    	}
    
    	// 想替换掉所有 查询
    	public static Object query(String sql, Object params[],
    			ResultSetHandler handler) throws SQLException {
    
    		Connection conn = null;
    		PreparedStatement st = null;
    		ResultSet rs = null;
    		try {
    			conn = getConnection();
    			st = conn.prepareStatement(sql);
    			for (int i = 0; i < params.length; i++)
    				st.setObject(i + 1, params[i]);
    			rs = st.executeQuery(); // 接下来, 框架制作者不知道该怎样处理 rs
    			// 方法: 对外暴露个接口,让调用者实现那个接口(handler),我们用客户所实现的接口处理 rs
    			// 调用用户传来的 handler
    			return handler.handler(rs);
    		} finally {
    			release(conn, st, rs);
    		}
    	}
    }
    
    // 设计一个接口,对外暴露
    interface ResultSetHandler {
    	public Object handler(ResultSet rs); // 让用户实现这个方法
    }
    
    // 框架作者根据现实情况,提前写好一些处理器
    class BeanHandler implements ResultSetHandler {
    	// 不知道 bean 是啥, 就定义一个变量接收,且用构造函数提供对外访问方式
    	private Class clazz;
    
    	public BeanHandler(Class clazz) {
    		this.clazz = clazz;
    	}
    
    	@Override
    	public Object handler(ResultSet rs) {
    		try {
    			if (!rs.next())
    				return null;
    
    			// 创建出要封装结果集的 bean
    			Object bean = this.clazz.newInstance();
    
    			// 通过元数据技术获知 rs 里有啥
    			ResultSetMetaData meta = rs.getMetaData();
    			int colNum = meta.getColumnCount();
    			for (int i = 0; i < colNum; i++) {
    				String name = meta.getColumnName(i + 1); // 结果集每列列名 id
    				Object value = rs.getObject(name); 		 // 1
    
    				// 通过 name,反射出 bean 上与 name对应的属性
    				Field f = bean.getClass().getDeclaredField(name);
    				f.setAccessible(true); // 强制访问私有元素
    				f.set(bean, value);
    			}
    			return bean;
    		} catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    	}
    }
    
    // 返回包含 bean 的 list 集合
    class BeanListHandler implements ResultSetHandler {
    	private Class clazz;
    
    	public BeanListHandler(Class clazz) {
    		this.clazz = clazz;
    	}
    
    	@Override
    	public Object handler(ResultSet rs) {
    		List list = new ArrayList();
    		try {
    			ResultSetMetaData meta = rs.getMetaData();
    			int count = meta.getColumnCount();
    			while (rs.next()) {
    				Object bean = this.clazz.newInstance();
    				for (int i = 0; i < count; i++) {
    					String name = meta.getColumnName(i + 1);
    					Object value = rs.getObject(name);
    
    					Field f = bean.getClass().getDeclaredField(name); // 反射获取域
    					f.setAccessible(true);
    					f.set(bean, value);
    				}
    				list.add(bean);
    			}
    		} catch (Exception e) {
    			throw new RuntimeException(e);
    		}
    		return list;
    	}
    }
    

    模拟使用该框架的 dao 代码:

    package cn.wk.utils;
    import java.sql.SQLException;
    import org.junit.Test;
    import cn.wk.domain.Account;
    
    // 假设这是 Dao
    // 注意到:crud 变化的是 sql 和 st.set 其余代码均相同
    public class Demo7 {
    	@Test
    	public void test() throws SQLException {
        List<?> list = getAll();
    		System.out.println(list.size());
    	}
    	public void add(Account a) throws SQLException {
    		String sql = "insert into account(name,money) values(?,?)";
    		Object params[] = { a.getName(), a.getMoney() };
    		JdbcUtils.update(sql, params);
    	}
    	public void delete(int id) throws SQLException {
    		String sql = "delete from account where id=?";
    		Object params[] = { id };
    		JdbcUtils.update(sql, params);
    	}
    	public void update(Account a) throws SQLException {
    		String sql = "update account set name=?, money=? where id=?";
    		Object params[] = { a.getName(), a.getMoney(), a.getId() };
    		JdbcUtils.update(sql, params);
    	}
    	public Account find(int id) throws SQLException {
    		String sql = "select * from account where id=?";
    		Object params[] = { id };
    		return (Account) JdbcUtils.query(sql, params, new BeanHandler(
    				Account.class));
    	}
      public List getAll() throws SQLException {
      String sql = "select * from account";
      Object params[] = {};
      return (List) JdbcUtils.query(sql, params, new BeanListHandler(
          Account.class));
      }
    }
    
  • 相关阅读:
    在虚拟机上安装Docker并运行镜像下
    分红包思想
    从微信授权到JWT认证——玩转token之路
    .Net(C#)数据库访问连接方式
    Asp.Net导出Excel表格之二(HttpContext.Current.Response)
    我的ip_本机ip_本地ip_本机ip地址_公网ip_ip地址查询
    干货版“测试小品”欢乐场景
    家用宽带搭建Hmailserver邮箱服务器
    【microPython与esp8266】之一——呼吸灯与PWM
    截取长文本,显示省略号(text-overflow:ellipsis)
  • 原文地址:https://www.cnblogs.com/ZhongliangXiang/p/9170404.html
Copyright © 2020-2023  润新知