day16 事务 - 数据库链接池 - 编写本身的jdbc框架

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

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

1. 事务

1

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

试验准备:sql

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。
只有下面这样才行:apache

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));
  }
}
相关文章
相关标签/搜索