JDBC快速入门

JDBC基本概念

  • 快速入门

 

JDBC基本概念

概念:

  • Java DataBase Connectivity Java 数据库连接,Java语言操作数据库

JDBC本质:

  • 其实是官方(sun公司)定义的一套操作所有关系数据库的规则,即接口。
  • 各个数据库厂商去实现这套接口,提供数据库驱动jar包。
  • 我们可以使用这套接口(JDBC)编程,真正执行的代码是驱动jar包中的实现类。
Person接口

Worker类

Person p = new Worler();

p.eat();

快速入门

步骤:

  1. 导入驱动jar包 mysql-connector-java-5.1.37-bin.jar
  • 1.复制 mysql-connector-java-5.1.37-bin.jar 到项目的libs目录下
  • 2.右键 --> Add As Library
  1. 注册驱动
  2. 获取数据库连对象 Connection
  3. 定义sql
  4. 获取执行sql语句的对象 Statement
  5. 执行sql,接受返回结果
  6. 处理结果
  7. 释放资源
JDBC快速入门

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;

public class JdbcCaiNiao{
    public static void main(String[] args) throws Exception{
        
        //1.导入驱动jar包
        //2.注册驱动
        Class.forName("com.mysql.jdbc.Driver");
        //3.获取数据库连对象
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/db3","root","password");
        //4.定义sql语句
        String sql = "update account set balance = 500 where id = 1";
        //5.获取执行sql语句的对象 Statement
        Statement stmt = conn.createStatement();
        //6.执行sql
        int count = stmt.executeUpdate(sql);
        //7.处理结果
        System.out.println(count);
        //8.释放结果
        stmt.close();
        conn.close();
        
    }
}
-------

简述

  JDBC是什么?JDBC英文名为:Java Data Base Connectivity(Java数据库连接),官方解释它是Java编程语言和广泛的数据库之间独立于数据库的连接标准的Java API,根本上说JDBC是一种规范,它提供的接口,一套完整的,允许便捷式访问底层数据库。可以用JAVA来写不同类型的可执行文件:JAVA应用程序、JAVA Applets、Java Servlet、JSP等,不同的可执行文件都能通过JDBC访问数据库,又兼备存储的优势。简单说它就是JAVA与数据库的连接的桥梁或者插件,用JAVA代码就能操作数据库的增删改查、存储过程、事务等。

JDBC有什么用?我们用JAVA就能连接到数据库;创建SQL或者MYSQL语句;执行SQL或MYSQL的查询数据库;查看和修改结果记录。

我们思考一下?数据库是由不同生产产商决定的,例如Mysql、Oracle、SQL Server,而如果JAVA JDK不可能说提供对不同数据库的实现吧?还有,JAVA具备天生跨平台的优势,它就提供了JDBC的接口API,具体的实现由不同的生产产商决定。这样,数据库生产产商都根据JAVA API去实现各自的应用驱动,这问题就迎刃而解了。

JDBC的工作原理是什么?我将在下一篇文章叙述JDBC运用的设计模式,以及部分JDK源码。

工作原理图(转自百度百科)

常用接口

提供的接口包括:JAVA API:提供对JDBC的管理链接;JAVA Driver API:支持JDBC管理到驱动器连接。

DriverManager:这个类管理数据库驱动程序的列表,查看加载的驱动是否符合JAVA Driver API的规范。

Connection:与数据库中的所有的通信是通过唯一的连接对象。

Statement:把创建的SQL对象,转而存储到数据库当中。

ResultSet:它是一个迭代器,用于检索查询数据。

三、快速入门

操作流程图

数据类型图

数字类型

时间日期类型

字符串类型

实例练习

1、Connection

public class JDBCUtil {
    //Driver类全名
    public static String DRIVER="com.mysql.jdbc.Driver";
    //jdbc协议:子协议://ip:端口号/数据库名
    public static String URL="jdbc:mysql://localhost:3306/test";
    //数据库用户名
    public static String USERNAME="root";
    //数据库密码
    public static String PASSWORD="root";

    private static Connection connection=null;

    /**
     * 获取JDBC连接
     * @return
     */
    public  static Connection getConnection(){
        try {
            //加载驱动程序:它通过反射创建一个driver对象。
            Class.forName(DRIVER);

            //获得数据连接对象。
            // 在返回connection对象之前,DriverManager它内部会先校验驱动对象driver信息对不对,我们只要知道内部过程即可。
            connection= DriverManager.getConnection(URL,USERNAME,PASSWORD);
            return connection;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 通过读取文件连接
     * @param fileName
     * @return
     * @throws SQLException
     */
    public   Connection getConnectionByLoadSettingFile(String fileName) throws SQLException {
        /*
            文件里面的内容:跟上面的常量一模一样
            jdbc.driver=com.mysql.jdbc.Driver
            jdbc.url=jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf-8
            jdbc.username=root
            jdbc.password=root
        */
        Properties props=new Properties();
        try {
            //我的properties文件是放在src根目录下的
            InputStream in=DBUtil.class.getResourceAsStream("/"+fileName);
            if(null==in)
                System.out.println("找不到文件:"+fileName);
            props.load(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String driver=props.getProperty("jdbc.driver");
        if(null!=driver)
            System.setProperty("jdbc.drivers",driver);
        String url=props.getProperty("jdbc.url");
        String username=props.getProperty("jdbc.username");
        String password=props.getProperty("jdbc.password");
        connection=DriverManager.getConnection(url,username,password);
        return connection;
    }
}

2、Statement

public class MyClient {
    public static void main(String [] args) throws SQLException {
        Connection connection=null;
        Statement statement=null;

        connection=JDBCUtil.getConnection();
        statement=connection.createStatement();
        //需要在自己的数据库当中建立一张user表
        String sql="insert into user(loginName,userName,password,sex)values('tom123','tom','123456',1)";
        statement.executeUpdate(sql);
    }
}

3、PareparedStatement

public class MyClient {
    public static void main(String [] args) throws SQLException {
        Connection connection=null;
        PreparedStatement pStatement=null;

        connection=JDBCUtil.getConnection();
        String sql="insert into user(loginName,userName,password,sex)values(?,?,?,?)";
        //预编译
        pStatement=connection.prepareStatement(sql);
        //前面的索引对应上面的问号,传递参数。
        pStatement.setString(1,"tom123");
        pStatement.setString(2,"tom");
        pStatement.setString(3,"123456");
        pStatement.setInt(4,1);
        pStatement.executeUpdate();
    }
}

4、ResultSet

public class MyClient {
    public static void main(String [] args) throws SQLException {
        Connection connection=null;
        Statement statement=null;
        ResultSet resultSet;

        connection=JDBCUtil.getConnection();
        String sql="select * from user";
        statement=connection.createStatement();
        //resultSet就是一个迭代器,里面的方法跟迭代器几乎一致。
        resultSet=statement.executeQuery(sql);
        while (resultSet.next()){
            String loginName=resultSet.getString("loginName");
            String userName=resultSet.getString("userName");
            String password=resultSet.getString("password");
            int sex=resultSet.getInt("sex");
            System.out.println(loginName+"-"+userName+"-"+password+"-"+sex);
        }
    }
}

四、可滚动和可更新的结果集

相关参数

//了解数据集可滚动更新:查看ResultSet接口的几个参数

  /** 结果集不能滚动(默认值)*/
    int TYPE_FORWARD_ONLY = 1003;

    /** 结果集可以滚动,但对数据库变化不敏感*/
    int TYPE_SCROLL_INSENSITIVE = 1004;

    /**结果集可以滚动,且对数据库变化敏感*/
    int TYPE_SCROLL_SENSITIVE = 1005;
  
  /**结果集不能用于更新数据库(默认值)*/
    int CONCUR_READ_ONLY = 1007;

    /**结果集可以用于更新数据库*/
    int CONCUR_UPDATABLE = 1008;

可滚动可更新

注意:可滚动简单说就是设置结果集可更新resultSet目前的游标值。可更新就是可以更新结果集里面的增删改查。可更新简单说,就是获取数据集ResultSet以后改动更加灵活。

public class Client {
    public static void main(String [] args){
        Connection connection=null;
        PreparedStatement pStatement=null;
        Statement statement=null;
        ResultSet resultSet=null;
        try {
            connection=DBUtil.getInstance().getConnection();
            //第一个参数设置是否可以滚动,第二个参数设置是否可更新
            statement=connection.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,ResultSet.CONCUR_UPDATABLE);
            String sql="select * from user";
            ResultSet rs=statement.executeQuery(sql);

            /**可滚动的几个方法
                rs.previous();
                rs.next();
                rs.getRow();
                rs.absolute(0);
             **/

            //往数据集里面插入数据同时更新到数据:从表的最后开始插入。
            rs.moveToInsertRow();//把游标移动到插入行,默认在最后一行。
            rs.updateString("loginName","小白脸");
            rs.updateString("userName","大猩猩");
            rs.updateString("password","123");
            rs.updateInt("sex",100);
            rs.insertRow();
            rs.moveToCurrentRow();//把游标移动最后一个位置

            //删除第十行数据
            rs.absolute(10);
            rs.deleteRow();

            while(rs.next()){
                System.out.println(rs.getString(2));
                //把数据集里的数据中的性别全部更新为0
                rs.updateInt("sex",0);
                rs.updateRow();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

五、事务

问题思考

事务是什么?我们将一组语句构成一个事务。当所有语句都是顺利执行以后,事务可以被提交。否则,如果其中某个语句遇到错误,那么事务将被回滚,就好像任何语句都没有被执行一样。

实际用例。假设我们需要将钱从一个银行账号转移到另外一个账号。此时,一个非常重要的问题就是我们必须同时将钱从一个账号取出并且存入另一个账号。如果在将钱存入其他账号之前系统发生崩溃,那么我们必须撤销取款操作。

事务有什么特性或者说有什么作用?

  • 原子性:最小的单元,如果一个是失败了,则一切的操作将全部失败。
  • 一致性:如果事务出现错误,则回到最原始的状态
  • 隔离性:多个事务之间无法访问,只有当事务完成后才可以看到结果
  • 持久性:当一个系统崩溃时,一个事务依然可以提交,当事务完成后,操作结果保存在磁盘中,不会被回滚

保存点与批量更新

保存点与事务的接口源码

public interface Connection  extends Wrapper, AutoCloseable {
    /** 设置提交方式:自动还是手动*/
    void setAutoCommit(boolean autoCommit) throws SQLException;
    
    boolean getAutoCommit() throws SQLException;

    /**提交事务*/
    void commit() throws SQLException;

   /**事务回滚*/
    void rollback() throws SQLException;
    
    /**设置保存点*/
    Savepoint setSavepoint() throws SQLException;

    Savepoint setSavepoint(String name) throws SQLException;
    
    /**回滚到保存点*/
    void rollback(Savepoint savepoint) throws SQLException;
    
    /**释放保存点资源*/
    void releaseSavepoint(Savepoint savepoint) throws SQLException;
}

public interface Statement  extends Wrapper, AutoCloseable {
    /**加入到批量处理队列*/
    void addBatch( String sql ) throws SQLException;
    
    void clearBatch() throws SQLException;

    /**执行批量处理队列*/
    int[] executeBatch() throws SQLException;
}

什么是保存点?使用保存点可以更细粒度地控制回滚操作,而不用每次都退回到初始点。

什么又是批量更新?批量更新包括批量增删改,当我们一次性要插入很多条数据的时候,假设我们每次提交一次又获取数据库连接一次,然后又关闭数据库连接,而且数据库连接是一个耗时操作,这样会大大降低性能,后续文章我会对这部分内容进行详细叙述。而批量更新呢,则先把数据放入一个队列里,并没有真正存入数据库中,当调用commit()方法的时候,队列的数据的操作一次性收集和提交。

public class Client {
    public static void main(String [] args) throws SQLException {
        long time=System.currentTimeMillis();
        Connection connection=null;
        PreparedStatement pStatement=null;
        boolean autoCommit=false;
        Savepoint savepoint=null;
        try {
            connection=JDBCUtil.getConnection();
            autoCommit=connection.getAutoCommit();
            connection.setAutoCommit(false);
            String sql="insert into user(loginName,userName,password,sex)values(?,?,?,?)";
            pStatement=connection.prepareStatement(sql);
            //设置保存点
            savepoint=connection.setSavepoint("savePoint");
            for(int i=0;i<1000;i++){
                pStatement.setString(1,"tony"+i);
                pStatement.setString(2,"user"+i);
                pStatement.setString(3,i+"");
                pStatement.setInt(4,i);
                //添加到队列
                pStatement.addBatch();
            }
            //批量执行
            pStatement.executeBatch();
            connection.commit();

        } catch (SQLException e) {
            e.printStackTrace();
            //回滚到保存点
            connection.rollback(savepoint);
        }finally {
            //把事务提交设置为最初设置
            connection.setAutoCommit(autoCommit);
        }
        long temp=System.currentTimeMillis()-time;
        System.out.println(temp+"ms");
    }
}

六、思考与总结

思考

  • 问题一:我们都知道获取JDBC连接是一个耗时操作。而我们查看教程的时候,提倡我们获取数据库连接,操作完毕以后要记得关闭,这样固然是正确的。但是,如果一个简单的操作就不停开启连接断开连接,这样会对性能大打折扣。
  • 问题二:JDBC的工作原理?还有它底部运用什么设计模式,让它能够自适应不同数据库产商的驱动呢?
  • 问题三:事务提交和普通提交的性能到底有多大的差别?

总结

解决一:有一个概念叫做连接池,就是数据库连接这个耗时操作交个一个容器去管理。至于数据库什么时候连接什么时候被关闭,有几个数据库连接对象?这些完全托管给连接池,而不需要客户端去考虑,目前一个比较成熟的是c3p0的连接池。现在模拟一个单例的数据库连接,比较单例数据库连接与随开随关的性能比较。

工具类

public class DBUtil {
    public static String DRIVER="com.mysql.jdbc.Driver";
    public static String URL="jdbc:mysql://localhost:3306/test";
    public static String USERNAME="root";
    public static String PASSWORD="root";

    private  Connection connection=null;
    private DBUtil(){
    }
    /**
     * 获得DB工具类的对象,这种获取对象的方式慢慢被jdk推荐使用。
     */
    public static  DBUtil getInstance(){
        return DBUtilClassInstance.dbUtil;
    }

    /**
     * 采用内部类单例模式:天然线程安全,延迟加载,调用效率高。若不了解,参考我的文章设计模式-单例模式
     */
    private static class DBUtilClassInstance{
        private  static  DBUtil dbUtil= new DBUtil();
    }

    /**
     * 获取JDBC连接
     * @return
     */
    public  Connection getConnection(){
        try {
            if(null!=connection && !connection.isClosed()){
                return connection;
            }
            Class.forName(DRIVER);
            System.out.println("驱动程序加载成功!");
            connection=DriverManager.getConnection(URL,USERNAME,PASSWORD);
            return connection;
        } catch (Exception e) {
            System.out.println("未找到驱动程序!");
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 通过读取文件连接
     * @param fileName
     * @return
     * @throws SQLException
     */
    public   Connection getConnectionByLoadSettingFile(String fileName) throws SQLException {
        if(null!=connection && !connection.isClosed()){
            return connection;
        }
        Properties props=new Properties();
        try {
            InputStream in=DBUtil.class.getResourceAsStream("/"+fileName);
            if(null==in)
                System.out.println("找不到文件:"+fileName);
            props.load(in);
        } catch (Exception e) {
            e.printStackTrace();
        }
        String driver=props.getProperty("jdbc.driver");
        if(null!=driver)
            System.setProperty("jdbc.drivers",driver);
        String url=props.getProperty("jdbc.url");
        String username=props.getProperty("jdbc.username");
        String password=props.getProperty("jdbc.password");
        connection=DriverManager.getConnection(url,username,password);
        return connection;
    }

性能比较

public class Client {
    public static void main(String [] args){
        long time=System.currentTimeMillis();
      for(int i=0;i<100;i++){
          User user=new User("loginName"+i,"userName"+i,"password"+i,i);
          myThread thread=new myThread(user);
          thread.run();
      }
      System.out.println(System.currentTimeMillis()-time+"ms");
    }
}

class myThread implements Runnable{

    private User user;

    public myThread(User user){
        this.user=user;
    }

    public void run() {
        PreparedStatement pStatement=null;
        Connection connection=null;
        try {
            connection=DBUtil.getInstance().getConnectionByLoadSettingFile("db.properties");
            String sql="insert into user(loginName,userName,password,sex)value(?,?,?,?)";
            pStatement=connection.prepareStatement(sql);
            pStatement.setString(1,user.getLoginName());
            pStatement.setString(2,user.getUserName());
            pStatement.setString(3,user.getPassword());
            pStatement.setInt(4,user.getSex());
            pStatement.executeUpdate();
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            //注释掉这个方法就是代表数据库连接用完就随即关闭。
            DBUtil.getInstance().closePreparedStatement(pStatement);
            DBUtil.getInstance().closeConnection(connection);
        }
    }
}

测试结果

随开随关:7155ms。单例模式,最后一个数据库操作完毕再关闭:6211ms。

连接池的原理参考:http://www.cnblogs.com/doudouxiaoye/p/5708854.html

解决二:在后续文章再介绍JDBC运用的设计模式以及部分源码分析,敬请期待。

解决三:事务提交耗时测试(代码参考事务批量处理部分,不再重复写)。

测试结果:单例模式,最后一个数据操作完成关闭批量更新耗时:69863ms。事务方式批量更新耗时:682ms。

-----------

JDBC 快速入门

JDBC英文全称是Java Database Connectivity,也就是Java数据库连接。这是一个Java连接SQL数据库的标准,包含了常用的API,让我们能方便的连接盒管理SQL数据库。每个数据库厂商都会提供相应的JDBC驱动程序,实现相应的接口。这样我们就能以统一的方式,操作不同的数据库了。

建立连接

要使用JDBC,首先要做的事情就是建立一个数据库连接,这是一个java.sql.Connection对象,提供了很多功能。详细的使用方法可以参考JavaDoc。要创建一个Connection对象,我们需要使用以下语句:

Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

这个方法会抛出SQL异常,所以在使用的时候需要包裹在try-catch块中或者向上一级抛出异常。

DriverManager的getConnection方法会接受三个参数,URL代表JDBC连接字符串,还有两个参数是用户名和密码。每个数据库都有自己的连接字符串,这里列举几个常用的。DriverManager能够自动查找和加载驱动程序类,但是有时候(比如使用Hibernate)可能需要手动指定驱动程序类,这时候就需要知道驱动程序类的名称。

数据库连接字符串驱动程序类
MySQLjdbc:mysql://HOST/DATABASEcom.mysql.jdbc.Driver
Postgresqljdbc:postgresql://HOST/DATABASEorg.postgresql.Driver
SQL Serverjdbc:microsoft:sqlserver://HOST:1433;DatabaseName=DATABASEcom.microsoft.jdbc.sqlserver.SQLServerDriver

下面我们以MySQL数据库为例。默认情况下MySQL的连接字符串应该是这样:jdbc:mysql://localhost:3306/jdbclearn,jdbclearn为我们所使用数据库的名称。

使用Statement

有了数据库连接之后,我们就可以执行SQL语句了。执行SQL语句需要创建一个Statement对象。可以用以下语句创建Statement:

Statement statement = connection.createStatement()

有了Statement对象,就可以调用它的方法来具体执行SQL语句了。根据功能可以将SQL语句分为两种,查询和更新。查询语句是对数据库的查询,不涉及数据的更改。更新语句包括插入、更新、删除等操作,会修改数据库的状态。

执行更新

执行更新需要调用Statement的executeUpdate方法,接受一个SQL更新字符串。这个方法实际上还会返回一个整数,表示受到影响的行数,不过一般情况下我们用不到。

下面的语句简单的执行了一条SQL插入语句。

statement.executeUpdate("INSERT INTO user(username,password) VALUES('yitian','123456')");

执行查询

另一类语句就是查询语句了。执行查询语句需要调用Statement的executeQuery方法,这个方法接受一个查询字符串,会返回一个ResultSet对象,也就是查询的结果集。这个对象会包含所有的查询结果和一个游标。下面的例子执行一个SQL查询,将结果放到相应的实体类中,然后得到一个List。

List<User> users = new ArrayList<>();
try (Statement statement = connection.createStatement()) {
    try (ResultSet rs = statement.executeQuery("SELECT *FROM user")) {

        while (rs.next()) {
            User user = new User();
            user.setId(rs.getInt(1));
            user.setUsername(rs.getString(2));
            user.setPassword(rs.getString(3));
            user.setNickname(rs.getString(4));
            user.setBirthday(rs.getDate(5));
            users.add(user);
        }
    }
}

next方法会将游标移动到下一个数据,如果没有下一个数据,就会返回false。在刚获取到结果集的时候,游标默认在第一个数据之前,所以我们可以将next方法放到循环中,来获取所有数据。结果集对象提供了一组get方法,用来获取结果。对于大多数数据库类型都有对应的Java类型,我们调用对应的方法就可以获取到数据了。这些get方法可以接受列名或者是列编号,如果使用编号的话性能会更好一点,因为不需要查询列名。需要注意列编号以1开始,不要和以零开始的数组相混淆

由于数据库连接、语句和结果集对象都实现了AutoCloseable接口,所以我们可以将其放入到自动资源清理语句中。

预编译的语句

普通的Statement虽然灵活,可以执行任意的SQL语句,但是它有几个缺点,第一,每次执行查询都需要将语句传入数据库中,不够高效;第二,如果要查询的语句很长,包含多个参数,需要拼接大量字符串,费时费力;第三,和第二点相关,如果用户输入的数据是virus';drop table user;这样的用户名,可能会扰乱SQL语句,甚至清除数据库,这就是所谓的SQL注入。要避免以上问题很简单,就是使用预编译的语句,也就是PreparedStatement对象。

使用方法很简单,和普通的语句类似,只不过换成了PreparedStatement,然后在创建预编译语句的时候需要在创建时指定SQL字符串,参数使用问号?代替。然后用一组set方法将参数传入,然后才能执行语句。

try (PreparedStatement statement
             = connection.prepareStatement("INSERT INTO user(username,password,nickname,birthday) VALUES(?,?,?,?)")) {
    statement.setString(1, "test2");
    statement.setString(2, "12345678");
    statement.setString(3, "张三");
    statement.setDate(4, new Date(new java.util.Date().getTime()));
    int rows = statement.executeUpdate();
    assertThat(rows, is(1));
}

上面普通的语句有什么缺点,预编译的语句就有什么优点。所以如果没有什么特殊要求,最好在项目中全部使用预编译的语句。

结果集

执行查询之后JDBC会返回一个结果集对象,结果集对象包含了我们获取查询结果的很多方法。最常用的方法就是前面的做法,在循环中调用结果集的next方法,然后获取每一行内容。

结果集的常用方法如下:

方法名作用
absolute(int i)将游标移动到结果集的第i行
afterLast()将游标移动到结果集的最后一行的后面
beforeFirst()将游标移动到结果集第一行的前面
first()将游标移动到第一行
last()将游标移动到最后一行
getXXX(int columnIndex)一组get方法,按列序号获取当前行的数据
getXXX(String columnLabel)一组get方法,按列名称获取当前行的数据
deleteRow()删除当前行的数据,也会从地从数据库中删除
updateXXX一组update方法,用来更新结果集的,和get方法一样,存在按照列名和列序号两种方式更新数据
updateRow()将更新之后的行写入结果集和底层数据库

默认情况下结果集只支持一次遍历,也就是说游标在遍历到下一条数据之后,就无法后退了。我们也可以打开结果集的遍历和编辑功能。要打开结果集的遍历和编辑功能,需要在创建语句对象的时候同时指定结果集的标志。然后就可以使用上面列举出的各种方法对结果集进行遍历和编辑、删除操作了。

try (Statement statement
             = connection.createStatement(
        ResultSet.TYPE_SCROLL_INSENSITIVE,
        ResultSet.CONCUR_UPDATABLE)) {
    statement.executeUpdate("INSERT INTO user(username,password) VALUES('test3','112233')");
    try (ResultSet rs = statement.executeQuery("SELECT *FROM user WHERE username='test3'")) {
        rs.absolute(1);
        rs.updateString("password", "987654321");
        rs.updateRow();
    }
    try (ResultSet rs = statement.executeQuery("SELECT *FROM user WHERE username='test3'")) {
        rs.absolute(1);
        assertThat(rs.getString("password"), equalTo("987654321"));
    }

}

元数据

利用元数据可以获取关于JDBC的更多信息我们可以在数据库连接、结果集等对象上调用getMetaData方法,获取相应的元数据对象。

下面是一个数据库元数据的例子,我们可以使用元数据获取数据库连接的详细属性。

DatabaseMetaData metaData = connection.getMetaData();
logger.info("DriverName:{}", metaData.getDriverName());
logger.info("DriverVersion:{}", metaData.getDriverVersion());

利用结果集元数据,我们可以获取结果集的详细信息。下面利用元数据获取了结果集各列的列名。

try (PreparedStatement statement
             = connection.prepareStatement("SELECT *FROM user")) {
    try (ResultSet rs = statement.executeQuery()) {
        ResultSetMetaData metaData = rs.getMetaData();
        for (int i = 0; i < metaData.getColumnCount(); ++i) {
            System.out.print(String.format("%s\t", metaData.getColumnName(i + 1)));
        }
        System.out.println();
        while (rs.next()) {
            for (int i = 0; i < metaData.getColumnCount(); ++i) {
                System.out.print(String.format("%s\t", rs.getString(i + 1)));
            }
            System.out.println();
        }

    }
}

列集

ResultSet是对查询结果的一个抽象,但是结果集有一些局限性。所以出现了一个功能更强的接口就是列集RowSet,它继承自结果集,所以具备结果集的所有特性,同时还增加了一些功能。

下面就是一个使用JdbcRowSet的小例子。更多列集的使用方法请参考相关文档和博客。

RowSetFactory factory = RowSetProvider.newFactory();
try (RowSet rs = factory.createJdbcRowSet()) {
    rs.setUrl(JdbcUtil.URL);
    rs.setUsername(JdbcUtil.USERNAME);
    rs.setPassword(JdbcUtil.PASSWORD);
    rs.setCommand("select *from user");
    rs.execute();

    ResultSetMetaData metaData = rs.getMetaData();
    for (int i = 0; i < metaData.getColumnCount(); ++i) {
        System.out.print(String.format("%s\t", metaData.getColumnName(i + 1)));
    }
    System.out.println();
    while (rs.next()) {
        for (int i = 0; i < metaData.getColumnCount(); ++i) {
            System.out.print(String.format("%s\t", rs.getString(i + 1)));
        }
        System.out.println();
    }
}

相应的我写了一个小项目来演示上面的这些例子。项目托管在Github上,地址在这里。有兴趣的同学可以看看。

存储过程

在JDBC中也可以执行存储过程。我们以MySQL存储过程为例。下面是两个存储过程。

CREATE PROCEDURE find_all_blogs_of(IN user_id INT)
  BEGIN
    SELECT
      id,
      username,
      password,
      nickname,
      birthday
    FROM user
    WHERE id = user_id;
  END;

CREATE PROCEDURE get_total_user_count(OUT count INT)
  BEGIN
    SELECT count(id)
    FROM user
    INTO count;
  END;

执行存储过程需要使用CallableStatement。当存储过程需要IN参数的时候,像普通查询参数那样使用setInt这样的方法设置即可。如果存储过程是查询数据的,可以直接使用结果集返回。

CallableStatement statement = connection.prepareCall("CALL find_all_blogs_of(?)");
statement.setInt(1, 1);
ResultSet rs = statement.executeQuery();

如果存储过程使用OUT参数返回结果,那么情况稍微有些复杂。我们需要使用registerOutParameter方法注册一个输出参数。然后在存储过程执行之后获取该参数的值。

CallableStatement statement = connection.prepareCall("CALL get_total_user_count(?)");
statement.registerOutParameter(1, Types.INTEGER);
statement.execute();
int count = statement.getInt(1);

数据源

前面我们使用DriverManager来获取连接对象。但是在实际环境中最好使用数据原来实现相同的功能。JDBC定义了一个DataSource接口,所有的JDBC驱动都实现了该接口。除了JDBC驱动之外,还有一些类库页实现了该接口,提供了方便的数据源功能。以MySQL为例,我们来设置一个MysqlConnectionPoolDataSource数据源。

MysqlConnectionPoolDataSource dataSource = new MysqlConnectionPoolDataSource();
dataSource.setUrl(JdbcUtil.URL);
dataSource.setUser(JdbcUtil.USERNAME);
dataSource.setPassword(JdbcUtil.PASSWORD);
dataSource.setUseSSL(false);

有了数据源,我们就可以调用数据源的getConnection方法获取连接对象了。如果查看MySQL的源代码或者文档会发现,MysqlConnectionPoolDataSource还提供了大量set方法设置数据源的各种属性,因此数据源应该是创建数据库连接的首选方式。

事务管理

前面我们都是在执行了SQL语句之后,立刻获得了结果。我们还可以使用JDBC的事务管理功能。首先需要调用Connection.setAutoCommit(false)将自动提交关闭,然后使用Connection.commit和Connection.rollback提交或回滚事务。

DataSource dataSource = DataSourceUtils.getDataSource();
Connection conn = dataSource.getConnection();
conn.setAutoCommit(false);
PreparedStatement selectOne = conn.prepareStatement("SELECT count(id) FROM user WHERE username =?");
PreparedStatement insertOne = conn.prepareStatement("INSERT INTO user(username,password) VALUES(?,?)");
//成功插入
String username1 = "zhang3";
insertOne.setString(1, username1);
insertOne.setString(2, "123456");
insertOne.executeUpdate();
conn.commit();

selectOne.setString(1, username1);
ResultSet rs = selectOne.executeQuery();
rs.first();
assertThat(rs.getInt(1), is(1));

//插入失败
String username2 = "li4";
insertOne.setString(1, username2);
insertOne.setString(2, "123456");
insertOne.executeUpdate();
conn.rollback();

selectOne.setString(1, username2);
rs = selectOne.executeQuery();
rs.first();
assertThat(rs.getInt(1), is(0));

参考资料

http://alvinalexander.com/java/jdbc-connection-string-mysql-postgresql-sqlserver

来源:https://www.javatt.com/p/9392