不足之处请多多指教,感谢大家
JDBC 定义
JDBC是Java Database Connectivity的简称,它定义了一套访问数据库的规范和接口。但它自身不参与数据库访问的实现。因此对于目前存在的数据库(譬如Mysql、Oracle)来说,要么数据库制造商本身提供这些规范与接口的实现,要么社区提供这些实现。
Java 程序只依赖于 JDBC API,通过 DriverManager 来获取驱动,并且针对不同的数据库可以使用不同的驱动
图示:
JDBC核心五大对象
粗略的画个图
之前不太明白Class.forName(“com.mysql.jdbc.Driver”)怎么注册驱动。我们可以先看一下Driver类的源码
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!");
}
}
}
通过源码可以看出,把Driver类加载到内存里面是,静态代码块就立马执行注册驱动方法了。
JDBC封装类:
JDBC连接数据库的操作还是比较繁琐的,将这些操作封装起来,更利于我们的开发
思路:
1.将数据库驱动,连接数据库的url,数据库的账号,密码这些可能会做出修改的参数,从代码中抽离出来,写到配置文件中。有利于后期的维护
2.注册驱动只需要注册一次就可以了,配置文件的读取也只需要读取一次。所以这些操作放到静态代码块中更合理
3.将获取连接对象,获取PreparedStatement对象关闭资源这些常用的操作,分别封装起来,以后就只需要调用对应的方法就可以了
4.executeUpdate,executeQuery这两个方法执行的返回结果肯定不是我们想要的,我们需要把这些结果作为属性传入我们准备的实体类中。所以需要把它封装,经过处理返回我们需要的返回值
代码演示:
package com.feisi.util;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
/**
* jdbc的工具类
*/
public class JDBCUtil {
static Properties props = new Properties();
static{
InputStream in = JDBCUtil.class.getClassLoader().getResourceAsStream("dbconfig.properties");
try {
props.load(in);
Class.forName(props.getProperty("driverClassName"));
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
//得到连接的方法
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(props.getProperty("url"),props.getProperty("username"),props.getProperty("password"));
}
//关闭资源的方法
public static void close(ResultSet rs, PreparedStatement pstmt,Connection conn ){
try {
if(rs != null) rs.close();
if(pstmt != null) pstmt.close();
if(conn != null) conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
/**
* 执行增删改的sql
*/
public static int executeUpdate(String sql,Object...params){
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = JDBCUtil.getConnection();
pstmt = createPreparedStatement(conn,sql,params);
return pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
}finally {
JDBCUtil.close(null,pstmt,conn);
}
return 0;
}
private static PreparedStatement createPreparedStatement(Connection conn,String sql,Object...params) throws SQLException {
conn = JDBCUtil.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql);
//给?赋值 params本质数组
if( params != null && params.length > 0 ){
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i+1,params[i]);
}
}
return pstmt;
}
/**
* 执行查询的sql
* 可变参数约束:
* 1. 位于所有参数后面
* 2. 一个方法只能有一个可变参数
* 所有可变参数都可以使用数组替换
*/
/**
* 优化版本
*/
public static <T> List<T> executeQuery(String sql,Class<T> clazz, Object... params){
//使用jdbc 查询tb_user表
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = JDBCUtil.getConnection();
//创建PreparedStatement
pstmt = createPreparedStatement(conn,sql,params);
//执行
rs = pstmt.executeQuery();
System.out.println(rs.toString());
//解决ResultSet 反射
List<T> list = new ArrayList<>();
while(rs.next()){
//rs.getXxx()
//newInstance() 调用无参构造方法
T t = clazz.newInstance();
//给t的属性赋值
//获取T的属性
Field[] fields = clazz.getDeclaredFields();
//获取数据库中的元数据
ResultSetMetaData metaData = rs.getMetaData();
//获取字段查到的字段个数
int columnCount = metaData.getColumnCount();
//遍历结果集中所有的元数据
for (int i = 1; i <= columnCount; i++) {
//获取字段名
String columnLabel = metaData.getColumnLabel(i);
//遍历属性集合对象
for (Field field : fields) {
//将属性对象设置为暴力访问
field.setAccessible(true);
//获取对象的属性名
String name = field.getName();
//查找属性名字和字段名相同的属性,忽略大小写
if (name.equalsIgnoreCase(columnLabel)){
//获取数据库中字段对应的数据
Object object = rs.getObject(columnLabel);
//赋值给对象的属性
field.set(t,object);
}
}
}
//将对象添加到集合中
list.add(t);
}
return list;
}catch (Exception e) {
e.printStackTrace();
}finally{
JDBCUtil.close(rs,pstmt,conn);
}
return null;
}
}
执行SQL语句的过程
(图片来源于网络)
JDBC深入研究
1.关于PreparedStatement和Statement的区别
Statement执行过程:发客户端送sql语句到数据库服务器,数据库服务器会先分析语法,在编译SQL语句,最后再执行SQL语句。
通过上面的图片,可以看出。当执行的语句大体相同,只是后面参数不同时。重复的对SQL语句进行编译和语法分析会浪费很多时间。
并且当sql语句是:SELECT * FROM tab_user WHERE username=‘a’ or ‘1’=‘1’ and password=‘a’ or ‘1’=‘1’。这种就会存在很大的问题–SQL攻击。
为了解决上面的问题,Java提供了Statement的子类PreparedStatement类
PreparedStatement:可以进行预编译
1.MySQL执行预编译分为三步
- 1.执行预编译语句
- 2.设置变量
- 3.执行语句
通过预编译,可以提供一个sql语句模板,这样就不用每次都对语句进行分析,编译了,可以节省很多时间
2.我们一般用的PreparedStatement并没有用到预编译功能的(原因后面分析),只是用到了防止sql注入攻击的功能。
PreparedStatement里面会对敏感词进行转义,所有就不会出现SQL攻击的问题。这个操作是在PreparedStatement类中实现的,和服务器无关。
注意:
1.JDBC MySQL驱动5.0.5以后的版本默认PreparedStatement是关闭预编译功能的,,需要我们手动开启。而之前的JDBC MySQL驱动版本默认是开启预编译功能的。并且MySQL服务器4.1版本之前是不支持预编译的。
2.开启预编译的方式:设置MySQL连接URL参数:useServerPrepStmts=true
PreparedStatement 的预编译
首先我们知道它默认是关闭预编译的,如果不开启的话,将不会进行预编译。
代码:
public class ceshi {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
//加载驱动
Class.forName("com.mysql.jdbc.Driver");
//&useServerPrepStmts=true&cachePrepStmts=true 开启预编译和缓存
//获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ceshi?useUnicode=true&characterEncoding=utf8&useSSL=false","root","root");
String sql = "select * from subjecttype where subjectID = ?";
//预编译
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//给参数赋值
preparedStatement.setObject(1,1);
//发送SQL语句,获取结果集
ResultSet resultSet1 = preparedStatement.executeQuery();
preparedStatement.setObject(1,2);
//再发送一条SQL语句
ResultSet resultSet2 = preparedStatement.executeQuery();
if (resultSet1!=null) resultSet1.close();
if (resultSet2!=null) resultSet2.close();
if (preparedStatement!=null) preparedStatement.close();
if (connection!=null) connection.close();
}
}
查看日志文件可知:平时使用PreparedStatement 只是进行了防治SQL攻击,并没有进行预编译
PreparedStatement 预编译后的函数不会缓存
这也就意味这,当使用不同的PreparedStatement对象来执行相同的SQL语句时,还是会出现编译两次的现象。
如果希望缓存编译后函数的key,那么就要设置URL参数cachePrepStmts=true:
代码:
public class ceshi {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.jdbc.Driver");
//&useServerPrepStmts=true&cachePrepStmts=true 开启预编译和缓存
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ceshi?useUnicode=true&characterEncoding=utf8&useSSL=false&useServerPrepStmts=true&cachePrepStmts=true","root","root");
//对象一
String sql = "select * from subjecttype where subjectID = ?";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
preparedStatement.setObject(1,1);
ResultSet resultSet1 = preparedStatement.executeQuery();
if (resultSet1!=null) resultSet1.close();
if (preparedStatement!=null) preparedStatement.close();
//对象二
PreparedStatement preparedStatement2 = connection.prepareStatement(sql);
preparedStatement2.setObject(1,2);
ResultSet resultSet2 = preparedStatement2.executeQuery();
if (resultSet2!=null) resultSet2.close();
if (preparedStatement!=null) preparedStatement.close();
if (connection!=null) connection.close();
}
}
查看日志文件:
注意:
1.每次使用PreparedStatement对象后都要关闭该PreparedStatement对象流,否则预编译后的函数key是不会缓存的。
2.不同connection之间,预编译的结果是独立的,是无法共享的,一个connection无法得到另外一个connection的预编译缓存结果
2.PreparedStatementd的性能真的比Statement效率高很多吗?
直接上代码测试
完成向数据库中插入一万条记录的功能(INSERT into subjecttype VALUES(1,1))
public class ceshi {
public static void main(String[] args) throws Exception{
// PreparedStatementTest2();
StatementTest();
}
public static void StatementTest() throws Exception{
Class.forName("com.mysql.jdbc.Driver");
//&useServerPrepStmts=true&cachePrepStmts=true 开启预编译和缓存 //数据库批处理&rewriteBatchedStatements=true
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ceshi?useUnicode=true&characterEncoding=utf8&useSSL=false&useServerPrepStmts=true&rewriteBatchedStatements=true","root","root");
String sql = "INSERT into subjecttype VALUES(1,";
Statement preparedStatement = connection.createStatement();
long l = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
String sql2 = sql+i+")";
preparedStatement.addBatch(sql2);
}
preparedStatement.executeBatch();
long l1 = System.currentTimeMillis();
long l2 = l1 - l;
System.out.println("预编译需要花费的时间"+l2);
if (preparedStatement!=null) preparedStatement.close();
if (connection!=null) connection.close();
}
public static void PreparedStatementTest2() throws Exception{
Class.forName("com.mysql.jdbc.Driver");
//&useServerPrepStmts=true&cachePrepStmts=true 开启预编译和缓存 //数据库批处理&rewriteBatchedStatements=true
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/ceshi?useUnicode=true&characterEncoding=utf8&useSSL=false&useServerPrepStmts=true&rewriteBatchedStatements=true&cachePrepStmts=true","root","root");
String sql = "INSERT into subjecttype VALUES(1,?)";
//获取PreparedStatement对象
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//记录插入数据前的时间
long l = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
//给SQL设置不同的参数
preparedStatement.setObject(1,i);
//批处理
preparedStatement.addBatch();
}
//提交批处理操作
preparedStatement.executeBatch();
//记录执行后的时间
long l1 = System.currentTimeMillis();
//相减得出花费的时间
long l2 = l1 - l;
System.out.println("预编译需要花费的时间"+l2);
//关闭资源
if (preparedStatement!=null) preparedStatement.close();
if (connection!=null) connection.close();
}
}
当不开启预编译功能时,做5次测试,单位毫秒
第一次:39957 第二次:40020 第三次:39251 第四次:36505 第五次:39411
这个结果是开启了预编译(需要使用批处理方法来提交sql语句,并且需要在URL后面设置参数&rewriteBatchedStatements=true,因为数据库默认不开启批处理操作)
平均值:39313
第一次:259 第二次:266 第三次:242 第四次:268 第五次:260
经过测试:PreparedStatementd的性能真的比Statement效率高很多。
1.Statement的批处理:
优点:可以向数据库发送多条不同的SQL语句。
缺点:SQL语句没有预编译。
2.PreparedStatement的批处理:
优点:PreparedStatment批量操作时与数据库的通信次数远少于Statment。
缺点:只能应用在SQL语句相同,但参数不同的批处理中