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)可能需要手动指定驱动程序类,这时候就需要知道驱动程序类的名称。
数据库 | 连接字符串 | 驱动程序类 |
---|---|---|
MySQL | jdbc:mysql://HOST/DATABASE | com.mysql.jdbc.Driver |
Postgresql | jdbc:postgresql://HOST/DATABASE | org.postgresql.Driver |
SQL Server | jdbc:microsoft:sqlserver://HOST:1433;DatabaseName=DATABASE | com.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