数据库:JDBC的简单使用

贼拉简单,纯纯使用方法

Posted by BlackDn on July 29, 2022

“萤火点点,流光难寻;星海横流,岁月成碑。”

数据库:JDBC 的简单使用

前言

这篇文章本来是放到上一篇数据库:SQL 从入门到入门里的
但是那样太长了,看着很不舒服
所以干脆分出来再水一篇了=。=

JDBC 使用

JDBC 即 Java Database Connectivity,是 Java 为关系数据库定义的访问接口
对于MySQL来说,其 JDBC 的驱动就是一个 jar 包,它本身是纯 Java 编写的,因此用户可以引用java.sql包下面的相关接口访问MySQL

数据类型

SQL 定义的数据类型和 Java 本身的有一点区别,常见数据类型对照表如下:

Java 类型 SQL 类型
boolean BIT, BOOL
short SMALLINT
int INTEGER
long BIGINT
float REAL
double FLOAT, DOUBLE
String CHAR, VARCHAR
BigDecimal DECIMAL
java.sql.Date DATE
java.sql.Time TIME

加载驱动

通常我们通过Class.forName(driverClass)来加载驱动,其实就是声明我们使用的是什么数据库:

//加载MySql驱动
Class.forName("com.mysql.jdbc.Driver")
//加载Oracle驱动
Class.forName("oracle.jdbc.driver.OracleDriver")

这条命令能够将CLASSPATH下指定名字的.class文件加载到 JVM 中,然后初始化这个类。以 sql 为例,该方法会让 JVM 到"com.mysql.jdbc"路径下,找到 Driver 类并将其加载到内存中,这个 Driver 类继承NonRegisteringDriver,并实现了java.sql.Driver接口。

public class Driver extends NonRegisteringDriver
  implements java.sql.Driver {
  public Driver() throws SQLException{}
  static {
    try {
      DriverManager.registerDriver(new Driver());
    } catch (SQLException E) {
      throw new RuntimeException("Can't register driver!");
    }
  }
}

这个方法中实际调用DriverManagerregisterDriver()方法注册一个 mysql 的 JDBC 驱动(new Driver()),其提供了若干数据库连接的方法,之后我们就用DriverManager.getConnection()来连接数据库的啥。所以其实逻辑上Class.forName就是创建了一个新的数据库驱动(Driver)对象。
之所以不用new,是因为我们其实用不到 Driver 这个对象,我们也不需要直接调用它的方法,毕竟有 DriverManager 来帮助我们和 Driver 进行交互,所以实际上这个 Driver 对我们来说是隐形的。

既然如此,那我们索性让他隐形,所以现在我们连Class.forName也不用写了。
在 JDK 1.5 之后,那在 JDK1.5 之后,我们不需要显式调用这个方法了,在我们调用DriverManager.getConnectionDriverManager()的时候,系统会自动去 CLASSPATH 下加载合适的驱动。

JDBC 连接数据库并查询

讲道理连接数据库都要指定什么库名,用户名,密码,端口啥的,太麻烦了就跳过这些配置了=。=
总之,我们会通过 java.sql包Connection类来获取一个数据库连接,而Connection对象通常由DriverManager类来获取,其参数就是数据库的配置信息

Connection conn = DriverManager.getConnection(<Uri>, <Username>, <Password>);

然后,ConnectioncreateStatement()方法可以创建一个Statement对象,该对象提供的executeQuery()方法允许我们通过 SQL 语句执行查询,最后用ResultSet来保存执行得到结果。
不过由于Statement对象不能阻止SQL 注入,因此现在多用PreparedStatement,其使用?作为占位符,并且把数据连同 SQL 本身传给数据库,而非字符串,因此可以保证不被注入。
更多关于 SQL 注入的内容可见:SQL Injection 从入门到不精通

PreparedStatement statement = connection.prepareStatement("SELECT `id`, `name` FROM `users` WHERE `id`=?");
statement.setLong(1, id);
ResultSet resultSet = statement.executeQuery();

其中,statement.setLong(1, id)表示PreparedStatement中第一个占位符?的内容是变量id的内容(因为其数据类型是long,在数据库中是BIGINT
假设long id = 1,其替换占位符,则 SQL 语句就变为"SELECT name FROM users WHERE id=1"

SQL 执行完后,可以用ResultSetnext()方法来获取查询到的结果。当然结果可能不止一条,因此有多条数据的话就可以用循环来获取,如果没有数据了,next()方法会返回false
此外,StatmentResultSet都是需要关闭的资源,因此嵌套使用try (resource)确保及时关闭
所以最后整个操作会变成这样:

//use try-statement to close in time
try (Connection connection = DatabaseConnectionProvider.createConnection(configuration)) {
    try (PreparedStatement statement = connection.prepareStatement("SELECT `id`, `name` FROM `users` WHERE `id`=?")) {
        statement.setLong(1, id); //config statement
        ResultSet resultSet = statement.executeQuery();
        while (resultSet.next()) {
            long fitId = resultSet.getLong("id");
            String fitName = resultSet.getString("name");
        }
    }
}

上面的例子中,在获取数据的时候是根据字段名来的,也可以根据 SQL 语句中的字段顺序来获取,比如上面是SELECT id, name,则id为 1,name为 2
因此可以分别写成getLong(1)getString(2)

JDBC 插入数据

和查询类似,我们还是用PreparedStatement执行一条 SQL 语句,不过最后执行的就不是executeQuery()

//use try-statement to close in time
try (Connection connection = DatabaseConnectionProvider.createConnection(configuration)) {
    try (PreparedStatement statement = connection.prepareStatement("INSERT INTO `users` (`name`) VALUES (?)", Statement.RETURN_GENERATED_KEYS)) {
        statement.setString(1, name);
        statement.execute();
        //get result
        ResultSet resultSet = statement.getGeneratedKeys();
        resultSet.next();
        resultSet.getLong(1);
    }
}

PrepareStatement中,除了 SQL 语句,我们还加了一个Statement.RETURN_GENERATED_KEYS),表示获取插入记录的自增主键(这里是id),毕竟在数据库不可视的情况下我们是不知道现在自增到哪了。

这里用的是execute(),其既可以执行查询语句,返回true;又可以执行插入更新删除语句,返回false
事实上,还可以用executeUpdate()方法,该方法返回一个int,表示受影响的行数。插入一条数据则返回 1,两条数据则返回 2。

成功执行后,因为我们还想要获取自增主键的值,所以要用getGeneratedKeys()来获取结果ResultSet。因为我只插入了一条数据,所以我能肯定只返回一条(就是我插入的这条),而getLong(1)就是返回的自增主键的值。
如果一次插入多条记录,那么这个ResultSet对象就会有多行返回值(就要用循环和next()来获取);如果插入时有多列自增,那么ResultSet对象的每一行都会对应多个自增值(就要getLong(1)getLong(2)等)

JDBC 更新数据

有了插入数据的例子,更新就很简答了

//use try-statement to close in time
try (Connection connection = DatabaseConnectionProvider.createConnection(configuration)) {
    try (PreparedStatement statement = connection.prepareStatement("UPDATE `users` SET `name`=? WHERE `id`=?")) {
        statement.setBoolean(1, name);
        statement.setLong(2, id);
        if (statement.executeUpdate() == 1) {
            System.out.println("update successfully");
        }
    }
}

同样使用PreparedStatement执行 SQL 语句,值得注意的是这里出现了多个占位符?,因此在为这些占位符设置参数的时候,从 1 开始从左到右表示对应的占位符。
因为我第一个占位符是name,第二个占位符是id,所以下面分别用 1 和 2 来为其设置具体的值。

JDBC 删除

删除也没啥,改一下 SQL 语句就行了,用execute()executeUpdate()都可以,能够根据具体情况选择。

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("DELETE FROM `users` WHERE `id`=?")) {
        ps.setObject(1, id);
        if (statement.executeUpdate() == 1) {
            System.out.println("delete successfully");
        }
    }
}

JDBC 建表

因为这一段是后来加的,所以放在后面,不过其实很简单,也就是 SQL 语句加上statement.execute()

try (Connection conn = DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASSWORD)) {
    try (PreparedStatement ps = conn.prepareStatement("CREATE TABLE IF NOT EXISTS `users` (\n" +
                "`id`           BIGINT          PRIMARY KEY AUTO_INCREMENT,\n" +
                "`name`         VARCHAR(128)    NOT NULL,\n" +
                ")")) {
        ps.execute();
    }
}

因为我们在 SQL 语句中加上了IF NOT EXISTS,所以不用担心重复创建表,如果已经有了重名的表,这个 SQL 语句就不会执行的。

JDBC 其他操作

判断表是否存在

很多时候我们想在操作前先判断表是否存在,最原始的方法就是catch抛出的异常,再根据异常进行操作。
但是显然这样很麻烦,如果有多个异常,还得去获取异常中的信息。
为此,我们可以使用DatabaseMetaData类的getTables方法,比如我们查看是否存在users表:

DatabaseMetaData metaData = connection.getMetaData();
ResultSet resultSet = metaData.getTables(null, null, "users", new String[]{"TABLE"});
boolean flag = resultSet.next();    //existed: true

getTables()的四个参数分别如下。

参数 作用
String catalog 规定用来寻找表名的目录,通常为 null
String schemaPattern 数据库名,对于 oracle 来说就是用户名,可为 null
String tableNamePattern 用于匹配表名,可以使用通配符来匹配多个表。比如"%"表示任意表名,"T_"表示两个字的表名,第一个字母为 T
String[] types 表的类型(TABLE 或 VIEW),用于缩小检索范围

最后返回一个ResultSet,因为上面这个例子只匹配一个表,所以直接用resultSet.next()获得结果,如果存在则为true,不存在则为false
如果利用通配符匹配多个表,那么结果需要循环调用resultSet.next()

JDBC 事务

事务在之前 SQL 的内容有所提及,具体可见: SQL - 事务
而在 JDBC 中要使用事务,就要用代码的思维来决定一下顺序
首先我们要用setAutoCommit(false)来关闭自动提交,否则 JDBC 会执行一行提交一行;然后我们执行多条 SQL 语句,最后提交事务,重新打开自动提交。
当然还要抓取异常,如果出现了异常就要回滚,从而保证事务的原子性。
于是就有了以下框架:

try {
    connection.setAutoCommit(false);	 // 关闭自动提交
    //insert(); update(); delete();... (执行多条SQL语句)
    connection.commit();    // 提交事务
} catch (SQLException e) {
    connection.rollback();    // 回滚事务
} finally {
    connection.setAutoCommit(true);
    connection.close();
}

参考

  1. 廖雪峰:JDBC 编程
  2. Java 中的数据类型和 SQL 中的数据类型
  3. 详细说明为什么 JDBC 不用写 Class.forName() 加载驱动
  4. JDBC 学习 2:为什么要写 Class.forName(“XXX”)?
  5. SQL Injection 从入门到不精通
  6. execute 与 executeUpdate 的区别
  7. JDBC 如何判断一张表是否存在
  8. DatabaseMetaData 的用法