JDBC入门讲解——可能你还未知道的JDBC特性

什么是JDBC

我们现在可以在图形界面,可以在windows的命令控制台上操作数据库,除了这两种方式,我们还希望能通过java编写程序,让程序来操作数据库。

然而,**各大数据库各有各的方言,各有各的连接方式。**难道我们要为每种数据库编写一个程序来操作?明显是不可能的。为此SUN公司提供了一系列的接口,并且规定了接口的功能,来实现一个程序能操控所有数据库的目的。

比如DriveManager.getConnection()方法是用来获取与数据库的链接的,至于具体怎样实现就由各大数据库供应商来实现,通过这种面向接口思维,我们就不必操心数据库底层实现,只需要关注我们的业务逻辑。

这一系列链接JAVA程序与数据库的接口,就是JDBC (JAVA DATABASE Connectivity).

这一系列接口其实就在两个包中:

  • java.sql
  • javax.sql

JDBC四个核心对象:

有4个对象是JDBC的基本,就像大厦的柱子。所有高级功能底层都是依赖这4个对象

DriverManager:用于注册驱动

DriverManager是java.sql包下为数不多的实现类了。他用来加载数据库提供的驱动。

  • 数据库有MySQL,Oracle,SQL Server等,他们各自有各自的驱动。只有注册了驱动,JAVA的语句才能与数据库连接和通信。

  • 但我们并不是真的需要一个DriverManager对象,我们只是希望在DriverManager中注册对应的java.sql.Driver对象,具体来说就是执行如下代码:

    java.sql.DriverManager.registerDriver(new Driver());
    
  • com.mysql.cj.jdbc 的静态代码块本身就有这个命令:

    /*Driver的部分代码*/
    package com.mysql.cj.jdbc;
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        static {
            try {
                java.sql.DriverManager.registerDriver(new Driver());
            } catch (SQLException E) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }
    

    所以我们一般直接用Class.forName("com.mysql.cj.jdbc.Driver")来代替,因为这个方法在Driver类的静态代码块,所以无论如何都只会执行一次,因为类只会载入一次。

  • 其实即使我们不这样显式地注册,在我们调用getConnection()的时候,他也会自动帮我们注册的,这会在下面讲一下。

Connection:用于获取数据库的连接

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp","root","admin");
  • getConnection() 是一个静态方法,可以直接被DriverManager类名调用,点开这个方法你会发现,无论你调用哪一条重载,他都会把你输入的用户名和密码放到一个Properties中,然后调用如下方法:

    private static Connection getConnection(
            String url, java.util.Properties info, Class<?> caller) throws SQLException {
            /*其他很长很麻烦的代码*/
            ensureDriversInitialized();		//猛击这个方法!
    		/*其他很长很麻烦的代码*/  
        }
    
    private static void ensureDriversInitialized() {
        /*其他很长很麻烦的代码*/
    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
    Iterator<Driver> driversIterator = loadedDrivers.iterator();
    try {
    	while (driversIterator.hasNext()) {
    		driversIterator.next();
    	}
    	} catch (Throwable t) {
            // Do nothing
        }
        /*其他很长很麻烦的代码*/
    }
    

    所以无论调用哪个方法获取Connection,都会做如下几件事:

    • 把JVM能找到的所有继承了jdbc.sql.Driver接口的类全部收集到loadedDrivers 对象中,这是一个容器
    • 把存在loadedDrivers对象中的Driver对象逐一加载。
    • 加载的过程,就会执行Driver对象的静态代码块,而Driver对象的静态代码块会把自己注册到DriverManager
    • 结果就是一旦执行DriverManager.getConnection(),JVM能找到的Driver类都会被注册到DriverManager 中。
  • 所以即使我们不手动加载Class.forName("com.mysql.cj.jdbc.Driver")getConnection()也会把所有Driver对象注册到DriverManager中。

    package test01;
    
    import java.sql.Connection;
    import java.sql.DriverManager;
    import java.sql.SQLException;
    public class JDBCDemo {
        /*
        测试不手动加载能否获取连接。
        */
        public static void main(String[] args) throws SQLException {
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp","root","admin");
            System.out.println("connection = " + connection);
        }
    }
    
    输出结果:
    connection = com.mysql.cj.jdbc.ConnectionImpl@3bf7ca37
    
  • 那我们把所有Driver都注册到DriverManager中,DriverManager怎样知道我想要用哪个Driver呢?答案还是在getConnection()方法中找

    private static Connection getConnection(/*被省略的参数*/){
        //其他很长的代码
    	for (DriverInfo aDriver : registeredDrivers) {
        	if (isDriverAllowed(aDriver.driver, callerCL)) {
    			try {
    				Connection con = aDriver.driver.connect(url, info);
    				if (con != null) {
    					// Success!
    					return (con);
                    }
                } 
            }
    	}
        //其他很长的代码
    }
    

    registeredDrivers对象就是装着所有注册过的Driver对象的容器,他是一个ArrayList 对象,系统会遍历这个容器,看里面的元素,能不能连接传入的url,能够连接就成功获得connection了。

  • 你可能会问,手动加载了是否不再执行ensureDriversInitialized() 那串老长的代码了?就不用逐个Driver测试能否成功连接URL了?经过测试答案是:ensureDriversInitialized() 这串代码,**无论是否手动加载,都至少会执行一次。**这说明,DriverManager永远是装着环境中所有Driver对象,逐个Driver对象跟URL配对看是否能获得Connection

  • 总之结论就是:手动用Class.forName(classPath) 注册驱动跟调用getConnection() 方法注册驱动没什么区别。唯一的区别可能就是:自动注册驱动不会报Class.forName(classPath)找不到类异常

  • Connection对象还能控制自动提交事务的开关,在后面详细说明。

Statement:语句执行对象

  • Statement对象用于操作sql语句,返回对应的结果对象,
  • 可以把它理解为一辆小货车,把我们要执行的命令运送到数据库,再把数据库返回的结果运送到java程序方便我们处理。
public class TestDemo {
    public static void main(String[] args) throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");
        //创建语句执行对象“小货车”
        Statement statement = connection.createStatement();
        System.out.println("statement = " + statement); 
        //statement = com.mysql.cj.jdbc.StatementImpl@2aceadd4
    }
}

ResultSet:查询到的结果集

在命令行环境下,我们通过SELECT * FROM table_name 得到的是一张表,我们无法提取出这张表中的任何数据,不能对表中数据进行任何处理。就只能看一看过过瘾。

这种情况在java环境中却不会发生,就是因为ResultSet对象的强大功能。

JAVA连接数据库的步骤

1、导入包

我们都知道,在第一次调用一个类的构造函数,静态方法,静态变量的时候,JVM会找到这个类,然后把这个类的字节码文件(.class文件)导入方法区。那么,JVM是从哪里找到这些字节码文件的呢?有以下途径

  1. 在JVM自己的classPath中找

    这个classPath也是jre环境的一部分。当JVM运行一个java项目,JVM会先自己的classPath中找到项目所需的字节码文件,这些字节码文件默认是在jdk安装路径下的lib文件夹。比如大家用得最多的java.lang包、java.util包都在这个classPath中。

  2. 在项目自己的classPath中找

    我们自己写的类,自然不会出现在JVM自己的classPath下,那JVM在哪里找呢?在IDEA中,每个模块会有自己的classPath,我们自己写的类就会在模块的classPath中找。IDEA的classPath位置是:工程文件夹/out/production/模块名 下。

  3. 在额外的library中找

    第三方的jar文件,既不在JVM自己的classPath中,也不在我们自己项目的classPath中,那JVM要如何找到他呢?其实我们可以自己指定一个classPath,当JVM在上述两步都没找到对应的字节码文件,就会前往我们指定的classPath中找。

    添加方式也简单:

    • 先把对应的jar包复制到工程文件下的lib文件夹,
    • 在IDEA中右键对应的jar包(jar包就是class文件的集合包),选择add as library。
    • alt + shift + control + s打开项目结构管理器,就能在library标签中看到我们刚刚添加的文件夹1554082072627

我们以后要导入其他第三方包,也可以用这种方法,思路也是一样的。

为什么要把jar包复制到工程文件夹下呢?因为如果我们把jar包放在其他地方,那classPath就会指向硬盘中的其他地方,一旦工程要交给别人处理,classPath指向的地方就没有对应的jar包。程序就蒙圈了。

所以我们一般把jar包放到工程中,就会连同jar包一起复制,程序就不会蒙圈。

2、创建连接

正常流程下,一般第二步都是加载驱动,但因为DriverManager.getConnection() 其实会自动帮我们加载驱动,我会省略这一步。但如果你在某些情况下连接不上数据库,可能就是没有手动加载驱动的原因

Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp","root","admin");

getConnection() 方法接受三个参数,第一个是URL,URL由以下几部分组成:

协议名 : 子协议名 ?/ 主机名 : 端口名 / 资源名 ? [参数名1=参数值1&参数名2=参数值2]

url的作用就是

  • 用协议指定的方法
  • 告诉对方应用程序(通过主机名和端口名指定对方应用程序是谁)
  • 本程序需要用到哪些资源

我们上网其实也是给服务器传入这种url,来获取自己想要的资源的。

​ 所以DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp","root","admin") 这个语句的意思就是通过jdbc协议的mysql子协议,告诉localhost这台主机,在监控3306端口的那个程序,我需要db_temp这个资源,让他看着办。

​ 于是监控着3306端口的那个程序,也就是mysql,就对这个url进行处理,最后给我们一些参数,让我们创建了connection对象。

getConnection() 会自动帮我们把用户名和密码都被作为参数放到url中去,最后传给mysql的只有一条url,我们也可以自己把用户名和密码写进url

public class JDBCDemo {
    public static void main(String[] args) throws SQLException {
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp?user=root&password=admin");
        System.out.println("connection = " + connection);
        //connection = com.mysql.cj.jdbc.ConnectionImpl@79079097
    }
}

建议还是分开指定,我们自己知道底层是这个形式就行了。

如果主机地址和端口分别是"localhost"和"3306",则可以省略的,mysql自己会为我们加上,不过建议不要省。因为真正开发时很容易忘记改回来。

每一次调用getConnection()方法都会返回一个新的connection,多次调用getConnection()方法等于打开了多个命令窗口。

3、创建语句执行对象:Statement

Statement statement = connection.createStatement();

Statment对象有3个方法:

  • boolean statement.execute(String sql) 啥语句都执行,如果执行的是查询语句,返回true,否则返回false。比较少用,因为不能拿到返回的ResultSet。
  • ReasultSet statement.executeQuery(String sql) 只执行查询,会返回一个表
  • int statement.executeUpdate(String sql) 执行查询之外的语句,返回影响的行数。

假设在db_temp 数据库中有account表如下:

balance(INT)name(VARCHAR)id(INT)create_date(DATE)cost(DOUBLE)
500张三12018-01-0310.2
1500李四22018-11-2310.2
2000王五42019-08-0210.2
public class JDBC_3 {
    public static void main(String[] args) throws SQLException {
        // 获取连接
        Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");

        //获取语句执行对象
        Statement statement = connection.createStatement();

        // 使用statement对象执行命令
        // 创建一个新的用户,叫user01,可以在任意ip地址登录
        String user01 = String.format("create user '%s'@'%s'", "user01","%");
        statement.execute(user01);

        //新增一行记录,周杰伦,存款是500,开户日期是2019年3月1日
        int i = statement.executeUpdate(
            "insert into account (name,balance,create_date) values 
            ('周杰伦',500,'2019-3-1');");
        System.out.println("i = " + i);

        //查询表
        ResultSet resultSet = statement.executeQuery("select * from account");
        System.out.println(resultSet);
    }

同一个connection多次调用createStatement() 会返回多个不同的statement对象。

public class JDBC_4 {
    public static void main(String[] args) throws SQLException {
        Connection conn_1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");
        Statement stm_1 = conn_1.createStatement();
        Statement stm_2 = conn_1.createStatement();
        System.out.println("stm_1 = " + stm_1);	
        //stm_1 = com.mysql.cj.jdbc.StatementImpl@2aceadd4
        System.out.println("stm_2 = " + stm_2);
        //stm_2 = com.mysql.cj.jdbc.StatementImpl@24aed80c
    }
}

这个特性不知道之后会不会有重要作用,先记住吧,有再回来改。

4、获取返回表的信息:ResultSet

执行完statement.executeQuery(String sql) 会返回resultSet,这个结果集会有一个一开始指在第一行数据前面的游标resultSet对象有以下实例方法帮助我们获得表的信息

  • boolean next() 方法会让游标下移一行,并返回当前行是否有数据。没有数据返回false

  • int result.getInt(columnIdnex / columnName) 获取返回表当前行指定列的整数,他还有String,Byte,Date之类的变体。

    • 注意,如果用getInt()去处理一个类型是String的列,那么就会报错,

    • 如果用getString()去处理其他诸如int,double,date这类型的列,则会把当前数值变成String,再返回,有点像Scanner的next()方法

    • getBytes()会拿到数据库底层储存信息的原编码,就是信息在mysql底层是以什么形式储存的就返回什么

      Retrieves the value of the designated column in the current row of this ResultSet object as a byte array in the Java programming language. The bytes represent the raw values returned by the driver.

    • getObject() 会返回对应的对象,不管是什么都能返回,可惜是个Object但,我们可以通过metadata.getColumnClass()来获得类然后强转。

    • 注意列数的开始索引是1,而不是0

      public class JDBC_5 {
          public static void main(String[] args) throws SQLException {
              Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");
              Statement statement = connection.createStatement();
              ResultSet resultSet = statement.executeQuery("select * from account");
              while (resultSet.next()) {
                  // 获取当前行第1列的数值:
                  int anInt = resultSet.getInt(1);
                  System.out.print(anInt + ",");
                  
                  //获取当前行第2列的字符串,如果是数字,会先把数字转成对应的字符串返回。
                  String string = resultSet.getString(2);
                  System.out.print("string = " + string + ",");
                  
                  // 获取当前行第1列的原始数组
                  byte[] bytes = resultSet.getBytes(1);
                  System.out.print("bytes[] = " );
                  for (byte aByte : bytes) {
                      System.out.printf("%d ",aByte);
                  }
                  System.out.println();
              }
          }
      }
      
      其中一行数据的返回值:
      500,string = 张三,bytes[] = 53 48 48 
      
    • 其实从上面的案例可以看出,MySQL底层是把所有数据都转换成字符串,然后以utf-8的编码储存的。

    • 虽然MySQL底层是把数据转成字符串储存,但是每次getObject() 得到的都是确切对应的类。而不是String,非常神奇的。

  • ResultSetMetaData resultSet.getMetaData() 获取列表的元数据。这是一个描述列表列属性的对象。他有以下方法:

    • int metaData.getColumnCount(); 返回有多少列。
    • int metaData.getColumnDisplaySize(i + 1 ); 返回列能储存多长的数据,比如varchar(20)会返回20,int默认是得到11。
    • String catalogName = metaData.getCatalogName(i + 1); 返回列所属的数据库。跨表查询的时候可能用得上
    • String metaData.getTableName(i + 1); 见名知意的,返回列所属的表名。
    • String metaData.getColumnClassName(i + 1); 返回列类型的全限定名,如
      • VARCHAR(20)会返回java.lang.String
      • INT会返回java.lang.Integer
      • DATE会返回java.sql.Date (注意不是Util包下的那个Date,是sql包下的Date)
      • 这个方法可以获取类名,就可以通过反射获得对象。好用的。
    • String metaData.getColumnTypeName(i + 1); 获得数据库的类型,就VARCHAR,INT那种
    • String metaData.getColumnLabel(i + 1); / String metaData.getColumnName(i + 1);都是获得列的名称,不知道怎样设置label。
    public class JDBC_6 {
        public static void main(String[] args) throws SQLException {
    
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");
            Statement statement = connection.createStatement();
            ResultSet resultSet = statement.executeQuery("select * from account");
            ResultSetMetaData metaData = resultSet.getMetaData();
            int columnCount = metaData.getColumnCount();
            for (int i = 0; i < columnCount; i++) {
                System.out.println("row in " + (i + 1));
    
                int columnDisplaySize = metaData.getColumnDisplaySize(i + 1);
                System.out.println("columnDisplaySize = " + columnDisplaySize);
                
                String catalogName = metaData.getCatalogName(i + 1);
                System.out.println("catalogName = " + catalogName);
                
                String columnClassName = metaData.getColumnClassName(i + 1);
                System.out.println("columnClassName = " + columnClassName);
                
                String columnLabel = metaData.getColumnLabel(i + 1);
                System.out.println("columnLabel = " + columnLabel);
                
                String columnName = metaData.getColumnName(i + 1);
                System.out.println("columnName = " + columnName);
                
                String columnTypeName = metaData.getColumnTypeName(i + 1);
                System.out.println("columnTypeName = " + columnTypeName);
                
                int columnType = metaData.getColumnType(i + 1);
                System.out.println("columnType = " + columnType);
                
                String tableName = metaData.getTableName(i + 1);
                System.out.println("tableName = " + tableName);
                
                System.out.println("------------------------");
            }
        }
    }
    输出结果一部分;
    row in 1
    columnDisplaySize = 11
    catalogName = db_temp
    columnClassName = java.lang.Integer
    columnLabel = balance
    columnName = balance
    columnTypeName = INT
    columnType = 4
    tableName = account
    ------------------------
    

    建议自己逐个试一下这些方法加深印象。

  • 可以预想到,通过MetaData和类型信息的互相作用,我们可以做到功能异常强大,同时又非常容易使用的增删改查方法。

5、关闭ResultSet,Statement,Connection

就像我们用完数据库以后,要关闭命令窗口一样,java用完数据库也是要关闭数据库连接的。

像关闭其他流的数据库一样:后来先关

resultSet.close();
statement.close();
connection.close();

**resultSet.close()**的注释:

A ResultSet object is automatically closed by the Statement object that generated it when that Statement object is closed, re-executed, or is used to retrieve the next result from a sequence of multiple results.

Calling the method close on a ResultSet object that is already closed is a no-op.

一个resultSet会在以下情况下自动关闭:

  • 生成这个ResultSetStatement对象关闭了。
  • 生成这个ResultSetStatement对象重新执行了同一句语句。
  • 生成这个ResultSetStatement对象执行了不同的语句
  • 另外,重复关闭同一个resultSet也不会问题。

总而言之就是:**Statement会照顾好这个resultSet了,我们不用管他的。**即使以后用到PrepareStatment,也是如此。

**statment.close()**的注释:

It is generally good practice to release resources as soon as you are finished with them to avoid tying up database resources.

statment对象用完就关是个好习惯,可以防止占用数据库资源。重复关闭statment对象没啥效果,但也不会报错

**connection.close()**的注释

Calling the method close on a Connection object that is already closed is a no-op.
It is strongly recommended that an application explicitly commits or rolls back an active transaction prior to calling the close method. If the close method is called and there is an active transaction, the results are implementation-defined.

我们应该在一个事务明确地commit或者rollback以后再关connection,如果在事务进行时关闭了connection,会发生什么事由不同的实现来定义。

总之就是事务完结以后再关connection

**结论:**我们只需要关闭statment对象和connection对象就好了

使用JAVA来开启事务

在命令窗口或者图形化窗口中,即使我们不关闭自动提交:

set autocommit =0;

只需要

start transaction;

数据库也会在commit或者rollback之前暂停自动提交,但是JAVA并没有start transaction这个命令,所以我们只能把autocommit设为false,以阻止数据库的自动提交。

connection.setautocommit(false)

这样就不会再自动提交了。此外还有几个方法配合

  • connection.commit(); 提交事务,一旦成功执行到这行代码,就意味着注册表被永久修改
  • connection.rollback(); 回滚事务,所有在connection.setautocommit(false)之后执行的DML操作全部无效。连内存都没有痕迹!
  • connection.rollback(Savepoint); 部分事务回滚,可以保留部分未提交的事务,界限是生成savePoint的语句,生成语句之前的事务即使未提交都还可以保留在内存中,生成语句之后的事务如果未提交则全部无效。
public class JDBC_7 {
    public static void main(String[] args) {
        try {
            Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/db_temp", "root", "admin");
            Statement statement = connection.createStatement();
            connection.setAutoCommit(false);
            // statement.executeUpdate("update account set balance = 3000 where id=1;");
            // Savepoint savepoint = connection.setSavepoint();
            try {
                // 对account表执行两部操作:把用户1的余额改为2000
                statement.executeUpdate("update account set balance = 2000 where id=1;");
                //  把用户3的余额改为1000
                statement.executeUpdate("update account set balance = 1000 where id=3;");
                //  出错了!
                int b = 10 / 0;
                connection.commit();
            } catch (Exception e) {
                //任何错误都会导致回滚
                System.out.println("出错了!回滚");
                //connection.rollback(savepoint);
                connection.rollback();
                // 查看回滚后的数据
                showRS(statement.executeQuery("select * from account"));
            } finally {
                statement.close();
                connection.close();
            }
        } catch (SQLException e) {
            e.printStackTrace();
            System.out.println("无法开启数据库");
        }
    }

    private static void showRS(ResultSet resultSet) {
        try {
            int columnCount = resultSet.getMetaData().getColumnCount();
            while (resultSet.next()) {
                for (int i = 0; i < columnCount; i++) {
                    Object object = resultSet.getObject(i + 1);
                    System.out.print(object + " , ");
                }
                System.out.println();
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

输出结果:
出错了!回滚
500 , 张三 , 1 , 2018-01-03 , 10.2 , 
1500 , 李四 , 2 , 2018-11-23 , 10.2 , 
2000 , 王五 , 4 , 2019-08-02 , 10.2 , 
500 , 周杰伦 , 14 , 2019-03-01 , 10.2 , 
// 没有改变数据库数据
  • 注意,connection.setSavepoint(); 方法只能在setAutoCommit(false)之后调用。

如果把statement.executeUpdate("update account set balance = 3000 where id=1;"); 前的注释符号去掉,生成SavePoint来测试,就能看到生成SavePoint 之前的操作能够保留:

出错了!回滚
3000 , 张三 , 1 , 2018-01-03 , 10.2 , 
1500 , 李四 , 2 , 2018-11-23 , 10.2 , 
2000 , 王五 , 4 , 2019-08-02 , 10.2 , 
500 , 周杰伦 , 14 , 2019-03-01 , 10.2 , 

但是这些改变并没有commit,因此没有影响到数据库,如果现在查看account表,会看到如下内容:

500	张三	1	2018-01-03	10.2
1500李四	2	2018-11-23	10.2
2000王五	4	2019-08-02	10.2
500	周杰伦	14	2019-03-01	10.2
-- 用户1的余额还是500,因为数据并没有影响到数据库。

所有代码已经上传到[github](https://github.com/huheman/Blog/tree/master/jdbc_sample)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值