Learn JDBC-01

前言

很久很久之前,我写过了一个笔记是关于MySQL中的增删改查的基本的操作,其中使用的就是JDBC来连接的数据库从而进行操作数据库。不过当时讲的也是非常的简单的,所以说这里需要来对JDBC的进一步的了解。所谓的的JDBC就是Java DataBase Connectity,使用Java进行数据库的连接的一个东西。不过数据库这种东西虽然是一种统一的SQL语言,但是每一个数据库其实都有很多的不一样的地方,而且每个数据库底层的实现也不是一样的。Java如何实现对这些数据库的连接呢?其实Java的库本质上是没有实现的,JDBC只是Javasun公司写的一个接口。接口里面有很多的抽象的方法和类,以及各种各样的接口。然后有各个数据库的厂商实现JavaJDBC中各种各样的接口,然后提供他们对应的数据库的类的驱动。下面我们需要使用的数据库就是当前最流行的开源的数据库系统MySQL数据库。

数据库的连接

当我们下载MySQL数据库的时候里面就是有一个选项问我们需要使用哪一种驱动,此时一般情况下我们都是使用全选的。然后我们就可以去MySQL的安装的目录下找到Connect For J,里面存放的jar包就是JDBC的驱动。这里的驱动其实和平常意义上说我的驱动其实是一样的,比如说当我们使用莫一款硬盘的时候,电脑会告诉我们查询到未知的设备,正在安装驱动。这里的驱动其实就是厂家提供一个的一套软件,可以配置软件的运行。

加载和注册驱动

MySQL的驱动是com.mysql.cj.jdbc.Driver(之前是com.mysql.jdbc.Driver,后来改了)。我们需要将jar包导入到我们idea的项目中来,然后加载和注册驱动。我们需要使用反射的方式来加载这个驱动。

使用的方式是Class.forName(“com.mysql.jdbc.Driver”);,方法的返回值是一个Class对象。我们也可以通过这样方式加载系统的类,比如如果我们想要得到StringClass对象,之前我们说需要通过对象.getClass或者是类.class的方式,但是我们也可以通过Class.forName("java.lang.String")的方式也是可以得到相同的Class对象的。

加载完驱动之后我们就需要进行注册驱动了。方式是DriverManager.registerDriver(com.mysql.jdbc.Driver),其中DriverManagerjava.sql下面的一个接口。然后各大厂商提供了实现,其实通常情况下我们是不进行注册驱动的,因为各大厂商在写Driver类的时候,在静态的代码块中已经进行注册驱动的操作。比如com.mysql.cj.jdbc.Driver类中就是如下的代码。

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver()); // 已经进行类的加载
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

URL

驱动都已经处理完毕了,不过我们还需要知道这个驱动需要连接到什么地方。我们需要提供给驱动的就是一个URL地址,从而建立数据库的连接。

JDBC URL的标准由三部分组成,各部分间用冒号分隔。

  • jdbc:子协议:子名称 协议:
  • JDBC URL中的协议总是jdbc
  • 子协议:子协议用于标识一个数据库驱动程序
  • 子名称:一种标识数据库的方法。子名称可以依不同的子协议而变化,用子名称的目的是为了定位数据库 提供足够的信息。包含主机名(对应服务端的ip地址),端口号,数据库名

比如说我想要连接我的MySQL需要提供URL的是jdbc:mysql://localhost:3306/sher。其中前面的jdbc:mysql是使用mysql固定的协议。后面的localhost是一个ip地址,就是本机的地址127.0.0.13306MySQL默认的端口号,一般情况下不需要进行修改。后面的sher是我需要连接的database的名字。我们使用数据库的情况下很多都是使用本机,所以中间的那个是可以省略的。于是就可以写成jdbc:mysql:///sher,是不是超级的方便呢。

至于其余的数据库的连接的方式这里就叭介绍了。毕竟我也不使用那些玩意呐。

用户名和密码

有了驱动,还有了对应的URL,但是我们还是需要用户名和密码才可以登陆数据库。不过在此之前,我们需要确认一下我们i电脑中的MySQL服务是否已经打开了。可以打开cmd窗口,输入services.msc打开服务列表。如果没有打开的话,还可以通过使用管理员的方式打开cmd输入net start mysql57打开mysql服务。(我安装的是mysql57,而且一般情况下我的MySQL服务是关闭的,毕竟我也不经常使用这个玩意)

我们可以使用DriverManager.getConnection()方法建立到数据库的连接,方法中我们需要提供的参数有URL 用户名和密码

连接的几种方式

第一种方式

@Test
public void test1() throws Exception {
    Driver driver = null;
    driver = new com.mysql.cj.jdbc.Driver();
    String url = "jdbc:mysql:///sher?serverTimezone=UTC";

    Properties pros = new Properties();
    pros.setProperty("user", "root");
    pros.setProperty("password", "root");

    Connection conn = driver.connect(url, pros);
    System.out.println(conn);
}

直接依赖第三方的API进行了的创建,并且使用第三方类中的方法。不建议使用。

第二种方式

@Test
public void test2() throws Exception{
    Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
    Driver driver = (Driver) clazz.getConstructor().newInstance();

    String url = "jdbc:mysql:///sher?serverTimezone=UTC";

    Properties pros = new Properties();
    pros.setProperty("user", "root");
    pros.setProperty("password", "root");

    Connection conn = driver.connect(url, pros);
    System.out.println(conn);

}

这种方式第一种方式的不同是在于,我们是使用反射的方式来创建实例对象的,不在代码中调用第三方库的API,使用的都是Java中jdbc定义的接口,体现了面向接口编程的思想。

第三种方式

@Test
public void test3() throws Exception{
    Class<?> clazz = Class.forName("com.mysql.cj.jdbc.Driver");
    Driver driver = (Driver) clazz.getConstructor().newInstance();

    String url = "jdbc:mysql:///sher?serverTimezone=UTC";

    DriverManager.registerDriver(driver);
    Connection connection = DriverManager.getConnection(url, "root", "root");
    System.out.println(connection);
}

通过上面我们说的那个DriverManager的方式来获取连接。

第四种方式

@Test
public void test4() throws Exception {
    String url = "jdbc:mysql:///sher?serverTimezone=UTC";
    Connection connection = DriverManager.getConnection(url, "root", "root");
    System.out.println(connection);
}

驱动都可以不用加载了,也不用进行注册了,直接这么写完事了,比起上面复杂的代码是不是简单的飞起。不过这种方式还是有点儿问题的,当我们需要修改用户名,密码或者表名的时候,我们需要修改源代码。而源代码一旦修改就要进行重新的编译,所以上面说的那四种数据我们可以写入到文件中,需要的时候然后读取就行了。

第五种方式

在项目的文件夹下创建一个jdbc.properties的配置文件。

mysqlUser=root
mysqlPasswd=root
mysqlUrl=jdbc:mysql:///sher?serverTimezone=UTC
mysqlDriverClass=com.mysql.cj.jdbc.Driver

当然里面我们还可以写其他的数据库连接的信息。

public static Connection getConnection() throws Exception {
    InputStream is = Demo1.class.getClassLoader().getResourceAsStream("jdbc.properties");
    Properties properties = new Properties();
    properties.load(is);

    String user = properties.getProperty("mysqlUser");
    String passwd = properties.getProperty("mysqlPasswd");
    String url = properties.getProperty("mysqlUrl");
    String driverClass = properties.getProperty("mysqlDriverClass");

    Class.forName(driverClass);
    return DriverManager.getConnection(url, user, passwd);
}

然后使用当前的类的加载器去加载这个文件,(其实我们也是可以使用FileInputStream的),然后使用Properties类来读取文件中的key-value值。之后我们修改配置信息的时候就可以在配置文件中修改而不用重新编译源代码。

上面的第五种方式基本上就是我们连接数据库的最基本的操作了。不过需要注意的事,上面的方法中其实是会抛出很多的异常的,但是处于简单的处理,我就直接将异常抛出了,当异常信息需要处理的时候,我们还是需要使用try-catch-finnaly来处理一下异常的。

对数据库的基本的CRUD的操作

所谓的CRUD就是Create Read Update Delete也就是增删改查的缩写。这四种操作也就是数据库操作的最基本的操作。不过即使我们是使用Java来操作数据库的,其实我们还是要写SQL语言的,也就是说Java只是提供了一个连接的环境,事实上我们操作数据库还是使用select update insert delete这种SQL语言。不过之前的文章中我们已经对这中基本的操作已经做了了解,这里简单的内容就不说了。就比如说PreparedStatementStatement的区别。使用PreparedStatement可以避免SQL注入的问题,以及PreparedStatement进行了预编译的操作,可以极大的提升批量操作的性能。不过这里暂且不来讨论这些问题了。

其实所谓的增删改都是一样的操作,我们调用的时候都是使用executeUpdate,其实这三个操作基本上都是一样的,可以将其封装成为一个方法,至于查询可以封装成为另一个方法。不过在此之前,我们需要先来将数据库的连接与关闭的方法进行封装。

连接数据库获取连接

public static Connection getConnection() throws Exception {
    InputStream is = Demo1.class.getClassLoader().getResourceAsStream("jdbc.properties");
    Properties properties = new Properties();
    properties.load(is);

    String user = properties.getProperty("mysqlUser");
    String passwd = properties.getProperty("mysqlPasswd");
    String url = properties.getProperty("mysqlUrl");
    String driverClass = properties.getProperty("mysqlDriverClass");

    return DriverManager.getConnection(url, user, passwd);
}

和上面的连接的数据库操作是一样的。

关闭数据库以及其他资源

public static void closeResource(Connection conn, PreparedStatement ps, ResultSet rs) {
    if (conn != null) {
        try {
            conn.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (ps != null) {
        try {
            ps.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    if (rs != null) {
        try {
            rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

增删改的操作

public static int update(Connection conn, String sql, Object... objs) throws Exception {
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }
    int res = ps.executeUpdate();
    closeResource(null, ps, null);
    return res;
}

其中这个函数的返回值是受到影响的数据条数。这里我们将连接是作为参数传递进来的,如果每个方法中都来创建一个连接,那么效率都会特别的差,而且关闭的时候我们也没有关闭传递进来的连接。尤其到了后面事务操作的时候,我们就能清楚的明白为什么要这么做了。

查询操作

接下来就是最复杂的查询的操作,首先我们要思考一点,我们需要什么样的返回值。我们需要的返回的结果不应该是一个个字符串,我们需要的是一个对象或者是一个map(因为数据库中的数据是以键值对的形式来储存的)。首先我们来考虑一下返回值是一个对象的情况。这里我们需要操作的数据中有两个列idusername。我们如果需要返回一个对象,当然不可能查询的时候现造。我们需要对要进行操作的表写一个对应的类。

返回对象

public class Student {
    private int id;
    private String username;

    public Student(int id, String username) {
        this.id = id;
        this.username = username;
    }

    public Student() {
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", username='" + username + '\'' +
                '}';
    }
}

这里我们就写一个这个表对应的一个对象Student,那么有了这个结构了我们就可以通过反射的方式创建对象并返回了。

public static <T> T getInstance(Connection conn, Class<T> clazz, String sql, Object... objs) 
    throws Exception {
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }

    ResultSet rs = ps.executeQuery();
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount(); // 获取有多少列
    T t = null;
    if (rs.next()) {
        t = clazz.getConstructor().newInstance();
        for (int i = 0; i < columnCount; i++) {
            Object o = rs.getObject(i + 1);
            String columnLabel = metaData.getColumnLabel(i + 1); // 获取列名
            // 通过反射的方式来创建对象。
            Field field = clazz.getDeclaredField(columnLabel);
            field.setAccessible(true);
            field.set(t, o);
        }
    }
    closeResource(null, ps, rs);
    return t;
}

Class对象传入方法中,通过反射的方式来创建对象。前面的操作是和增删改的操作是一致的。但是后面我们需要处理ResultSet这个东西。不过之前我们只是从ResultSet中获取某一个属性的值是什么,比如说getInt(id),但是这里我们并不知道这里面的数据的名字是什么。不过API提供了一个结构给我们解决了这个问题。使用ResultSetMetaData可以获取ResultSet中列的类型和属性信息的对象。

不过需要注意的是,这个方法只可以返回一个值,就算是查询到了多个结果也只会返回结果的第一个。如果想要返回多个值的话,我们需要将返回值修改为List<Student>

public static <T> List<T> getInstances(Connection conn, Class<T> clazz, String sql, Object... objs) throws Exception {
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }

    ResultSet rs = ps.executeQuery();
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    List<T> list = new ArrayList<>();
    while (rs.next()) {
        T t = clazz.getConstructor().newInstance();
        for (int i = 0; i < columnCount; i++) {
            Object o = rs.getObject(i + 1);
            String columnLabel = metaData.getColumnLabel(i + 1);
            Field field = clazz.getDeclaredField(columnLabel);
            field.setAccessible(true);
            field.set(t, o);
        }
        list.add(t);
    }
    return list;
}

返回Map

这个方法和上面的获取一个的基本上是一模一样的。下面我们需要说的是返回一个Map,其实Map也是非常好的,我们不需要创建对应的对象。可以直接获取键值对然后放入Map中。

public static Map<String, Object> getMapInstance(Connection conn, String sql, Object... objs) throws Exception{
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }

    ResultSet rs = ps.executeQuery();
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    if (rs.next()) {
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < columnCount; i++) {
            Object o = rs.getObject(i + 1);
            String label = metaData.getColumnLabel(i + 1);
            map.put(label, o);
        }
        return map;
    }
    return null;
}

同样的,如果我们想要返回多条数据的话,我们需要返回者的类型是List<Map<String, Object>。其余的基本上都是一样的。

public static List<Map<String, Object>> getMapInstances(Connection conn, String sql, Object... objs) throws Exception{
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }

    ResultSet rs = ps.executeQuery();
    ResultSetMetaData metaData = rs.getMetaData();
    int columnCount = metaData.getColumnCount();
    List<Map<String, Object>> list = new ArrayList<>();
    while (rs.next()) {
        Map<String, Object> map = new HashMap<>();
        for (int i = 0; i < columnCount; i++) {
            Object o = rs.getObject(i + 1);
            String label = metaData.getColumnLabel(i + 1);
            map.put(label, o);
        }
        list.add(map);
    }
    return list;
}

其实还有一种查询的情况,不是查询这种数据,而是这种类型的查询。比如说select count(*) from test或者是select max(id) from test这种类型的查询,返回的都是一个值,这种类型的查询上面的方法是不适合的。我们需要使用特殊的方法进行查询。

查询特殊值

public static Object getValue(Connection conn, String sql, Object... objs) throws Exception{
    PreparedStatement ps = conn.prepareStatement(sql);

    for (int i = 0; i < objs.length; i++) {
        ps.setObject(i + 1, objs[i]);
    }

    ResultSet rs = ps.executeQuery();
    if (rs.next()) {
        Object object = rs.getObject(1);
        return object;
    }
    closeResource(null, ps, rs);
    return null;
}

其实这种方式的查询倒是最简单的,因为我们已经知道了只是返回一个数据了,那么我们处理的时候只需要返回那一个参数就行了。这就是对这种特殊值查询的处理。

总结

上面我们先是了解了数据库的连接的操作,然后对JDBC的增删改查的操作进行了基本的封装,也算是对JDBC的一个基本的了解了。


一枚小菜鸡