“萤火点点,流光难寻;星海横流,岁月成碑。”
数据库: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!");
}
}
}
这个方法中实际调用DriverManager
的registerDriver()
方法注册一个 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>);
然后,Connection
的createStatement()
方法可以创建一个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 执行完后,可以用ResultSet
的next()
方法来获取查询到的结果。当然结果可能不止一条,因此有多条数据的话就可以用循环来获取,如果没有数据了,next()
方法会返回false
此外,Statment
和ResultSet
都是需要关闭的资源,因此嵌套使用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();
}