JDBC API的设计目的是使简单的事情保持简单。这意味着JDBC简化了日常数据库任务
如何使用JDBC进行java应用程序与数据库交互
Java应用程序通过JDBC与数据库进行交互来管理数据,需要四步:创建连接、创建Statement、执行SQL语句查询、处理结果
- 连接数据库
- 创建Statement对象
- 发送查询或者更新语句到数据库
- 检索并处理从数据库接收到的结果,以响应您的查询
public void connectToAndQueryDatabase(String username, String password) {
// 创建连接
Connection con = DriverManager.getConnection(
"jdbc:myDriver:myDatabase",
username,
password);
// 发送查询语句
Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery("SELECT a, b, c FROM Table1");
// 处理收到的结果
while (rs.next()) {
int x = rs.getInt("a");
String s = rs.getString("b");
float f = rs.getFloat("c");
}
}
PrepareStatement的使用
JDBC API提供了三种不同类型的Statement:
- Statement:用于实现不带参数的简单SQL语句
- PreparedStatement:用于预编译可能包含输入参数的SQL语句,并且提供了占位符参数的功能,可以防止SQL注入
- CallableStatement:用于执行可能同时包含输入和输出参数的存储过程
在日常开发中,对于SQL语句的执行(不包含存储过程)一般都是使用PreparedStatement进行操作。
@Test
public void test() throws SQLException {
// 创建连接
Connection connection = DriverManager.getConnection("jdbc:mysql://IP:PORT/mybatisdemo?useUnicode=true&characterEncoding=UTF8&autoReconnect=true&useSSL=false&useServerPrepStmts=true&cachePrepStmts=true", "用户", "密码");
// 首次预编译,将发送预编译请求到MySQL服务端
PreparedStatement preparedStatement = connection.prepareStatement("select * from blog where name = ?");
preparedStatement.setString(1, "woshishui or '1' = '1'");
// 执行查询
preparedStatement.execute();
// 处理结果
ResultSet resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
Blog blog = new Blog();
blog.setId(id);
blog.setName(name);
System.out.println(blog);
}
// 关闭Statement,以便进行Statement的缓存
preparedStatement.close();
// 此时,由于之前已经执行过预编译了,直接从缓存中拿到预编译好的对象
preparedStatement = connection.prepareStatement("select * from blog where name = ?");
preparedStatement.setString(1, "woshishen");
// 执行查询
preparedStatement.execute();
// 处理结果
resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
long id = resultSet.getLong(1);
String name = resultSet.getString(2);
Blog blog = new Blog();
blog.setId(id);
blog.setName(name);
System.out.println(blog);
}
}
上面我们提到过PrepareStatement可以支持SQL预编译、防SQL注入,那么预编译发生在什么时候?转义又发生在什么时候呢?
我们通过wireshark抓包可以看出,当执行 connection.prepareStatement 时,客户端会向服务端发送预编译SQL的请求命令,之后由服务端进行预编译处理后,返回一个 Statement ID ,之后客户端将通过传递这个 Statement ID 和参数组来执行SQL。
而防止SQL注入的方法,是通过对参数进行转义来实现。具体可以通过打开MySQL的 general_log 可以看出。如果开启了 useServerPrepStmts=true 那么由服务端来执行转义,否则客户端直接转义了。
总结
PrepareStatement的预编译和防止SQL注入涉及到两个参数:
- useServerPrepStmts=true
- cachePrepStmts=true
如果建立连接时,没有使用这两个参数的话,默认情况下都为false。即不开启服务端预编译,以及不缓存预编译SQL(都没有进行服务端的预编译请求了,自然就没有缓存的步骤了)。
如果只使用了 useServerPrepStmts=true 那么每次调用 Connection.prepareStatement 时,都将向服务端发送预编译SQL的请求,之后通过传递 Statement ID 和参数组进行SQL调用。因此打开了 useServerPrepStmts=true 那么建议同时打开 cachePrepStmts=true 以便提高效率,避免同一个SQL多次执行进行了多次预编译操作。
只有调用 PrepareStatement.close() 才会对Statement对象进行缓存,否则下一次调用 Connection.prepareStatement 时将重新发起预编译请求,造成不必要的消耗