JDBC----底层笔记
今天我来写一篇关于JDBC底层的博客,仔细讲解一下JDBC的底层是怎么样的。
然后再写一篇我们写项目时又是怎么利用别人提供的开源jar包写项目的博客。
一、JDBC的配置:
这个我之前写过一篇关于JDBC配置的博客了,想看请点击这里 : JDBC配置
二、JDBC获取连接操作
首先在获取连接之前我们需要先根据配置文件中的信息找到我们电脑上数据库的信息再进行操作:
public static Connection getConnection() throws IOException, ClassNotFoundException, SQLException { InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties"); Properties pros = new Properties(); pros.load(is); String user = pros.getProperty("user"); String password = pros.getProperty("password"); String url = pros.getProperty("url"); String driverClass = pros.getProperty("driverClass"); //2、加载驱动 Class.forName(driverClass); //3、获取连接 Connection conn = DriverManager.getConnection(url, user, password); return conn; }
首先根据类加载器得到配置文件的输入流,再new一个Properties类(因为我们的配置文件就是properties类型)。
获取配置文件的数据后,加载配置文件中的驱动,再根据本地数据库链接,用户名,密码获取与本地数据库的连接。
三、JDBC资源关闭操作
我们再操作数据库时其实还要根据连接获得数据库的Statement对象(固定语句对象),也要对它进行关闭
public static void closeResource(Connection conn, Statement ps) { try { if (ps != null) { ps.close(); } } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } }
关闭的操作很好实现,但是要注意空指针的判断
我们在操作数据库查询操作的时候还需要ResultSet对象(结果集对象),所以还要再写一个关于结果集对象的关闭操作。
public static void closeResource(Connection conn, Statement ps,ResultSet rs) { try { if (ps != null) { ps.close(); } } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) { conn.close(); } } catch (SQLException e) { e.printStackTrace(); } try { if (rs != null) { rs.close(); } } catch (SQLException e) { e.printStackTrace(); } }
这里还是要注意空指针的问题。
四、JDBC增删改操作
我们在进行增删改操作时主要使用PreparedStatement接口实现(预编译语句对象),它是Statement接口的子接口,具体好处可以去看这篇帖子:
https://www.cnblogs.com/zibange/articles/14118605.html
简而言之它的效率比Statement高,而且还可以防止sql注入问题。效率高,安全性好,还不用拼串,这样的接口谁不爱呢?
public int update(String sql,Object ...args){ //sql中的占位符的个数与可变形参长度一致 Connection conn = null; PreparedStatement ps = null; try { //1、获取数据库的连接 conn = JDBCUtils.getConnection(); //2、编译sql语句,返回PreparedStatement实例 ps = conn.prepareStatement(sql); //3、填充占位符 for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); //小心参数声明 } //4、执行操作 return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }finally { //5、关闭资源 JDBCUtils.closeResource(conn,ps); } return -1; }
返回值是修改的行数,如果修改失败那么返回-1
五、JDBC查询操作
JDBC查询又分为查询一条记录或者查询多条记录
查询一条记录:
public <T> T getInstance(Class<T> clazz,String sql,Object ...args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = JDBCUtils.getConnection(); ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); } rs = ps.executeQuery(); //获取结果集的元数据 ResultSetMetaData rsmd = rs.getMetaData(); //通过ResultSetMetaData,获取结果集的列数 int columnCount = rsmd.getColumnCount(); if(rs.next()){ T t = clazz.newInstance(); //处理结果集一行数据中的每一个列 for(int i=0;i<columnCount;i++) { //获取列值 Object columnValue = rs.getObject(i + 1); //获取每个列的列名 String columnLabel = rsmd.getColumnLabel(i + 1); //给cus对象指定的某个属性赋值为 obj 通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t,columnValue); } return t; } } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtils.closeResource(conn,ps,rs); } return null; }
我们在查询一条记录实际上就是在查询一个对象的数据,那么我们首先要确定这个对象的类型,所以这里使用泛型,并且参数要求传入查询的对象的类型。
for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); }
这里要注意,setObject方法第一个参数应该是i+1
我们利用ResultSet(结果集)得到查询到的每个列的列值,在根据ResultSetMetaData(结果集元数据)获取每个列的列名,
利用反射得到Bean中对应属性的访问权限并赋值。
这里要注意:
String columnLabel = rsmd.getColumnLabel(i + 1);
我们调用的结果集元数据方法是getColumnLabel而不是getColumnName,因为因为取名习惯,数据库中列名可能跟我们Bean对象中属性名不同,
所以在写sql语句的时候就注意给列名取别名,别名要和我们Bean对象中属性名相同。
赋值完成后返回反射产生的对象即可。
下面是查询多条记录:
public <T> List<T> getForList(Class<T> clazz,String sql,Object ...args) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = JDBCUtils.getConnection(); ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); } rs = ps.executeQuery(); //获取结果集的元数据 ResultSetMetaData rsmd = rs.getMetaData(); //通过ResultSetMetaData,获取结果集的列数 int columnCount = rsmd.getColumnCount(); List<T> list = new ArrayList<>(); while(rs.next()){ T t = clazz.newInstance(); //处理结果集一行数据中的每一个列 for(int i=0;i<columnCount;i++) { //获取列值 Object columnValue = rs.getObject(i + 1); //获取每个列的列名 String columnLabel = rsmd.getColumnLabel(i + 1); //给cus对象指定的某个属性赋值为 obj 通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t,columnValue); } list.add(t); } return list; } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtils.closeResource(conn,ps,rs); } return null; }
查询多条数据的代码和查询一条数据的代码大致相同,不同的是我们返回的是一个List对象,
而且判断条件不是:
if(rs.next())
而是:
while(rs.next())
因为我们要查询多条记录
六、JDBC操作的改进:
我们在操作数据库的时候要注意一点:数据库的并发问题:
对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采用必要的隔离机制, 就会导致各种并发问题。 脏读(读脏数据):对于两个事务T1,T2,其中T1读取了已经被T2更新但是还没有提交的字段,之后,若T2 回滚,T1读取的内容就是临时且无效的。 不可重复读:对于两个事务T1,T2,其中T1读取了一个字段,然后T2更新了该字段,之后,T1再次读取 同一个字段,值就不同了。 幻读:对于两个事务 T1,T2,其中T1从一个表中读取了一个字段,然后T2在该表中插入了一些新的行 之后如果T1再次读取同一个表,就会多出几行。 四种隔离级别: 1、READ UNCOMMITTED(读未提交数据) (不可取) 允许事务读取未被其他事务提交的变更,脏读,不可重复读,和幻读问题都会出现。(不可取) 解决:NULL 2、READ COMMITTED (读已提交数据)(Oracle默认) 只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题没有解决。 解决:脏读 3、REPEATABLE READ(可重复读) (MySQL默认) 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行 更新,可以避免脏读和不可重复读,但是幻读问题没有解决。 解决:脏读、不可重复读 4、SERIALIZABLE(串行化) (一般不用) 确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新 和删除操作。所有并发问题都可以解决,但是效率十分低下。 解决:ALL
为了解决这样的问题我们就要改善一下我们的方法,那就是参数也传入数据库的连接,保证不要每次查完一条数据就提交,关闭连接,
我们在完成一个事务之后才可以提交并且关闭数据库。
比如说在这里,如果我们没有将这三个连接绑在一起,那么在完成需要三条DDL语句的事务时,如果产生异常(比如网络异常),那么修改就会被保存,这就出现脏读的现象,
明明这不是我们期望的提交结果但是因为异常还是提交上去了。
通过绑定连接的方式,如果中间出现异常那么这里的操作全部作废,回到上一次提交的状态。
通用的增删改操作 version 2.0
public int update(Connection conn,String sql,Object ...args){ //sql中的占位符的个数与可变形参长度一致 PreparedStatement ps = null; try { //1、编译sql语句,返回PreparedStatement实例 ps = conn.prepareStatement(sql); //2、填充占位符 for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); //小心参数声明 } //3、执行操作 return ps.executeUpdate(); } catch (Exception e) { e.printStackTrace(); }finally { //4、关闭资源 JDBCUtils.closeResource(null,ps); } return -1; }
其实和上面的1.0版本没有太大的区别,只不过我们这里使用的连接不是我们自己造的,而是传入的参数,
在关闭资源的时候也要注意,不要关闭连接,因为事务还没有结束,只有结束的时候才可以关闭连接。
使用样例:
@Test public void testUpdateWithTransaction() { Connection conn = null; try { conn = JDBCUtils.getConnection(); System.out.println(conn.getAutoCommit()); //取消数据的自动提交 conn.setAutoCommit(false); String sql1 = "update user_table set balance = balance - 100 where user = ?"; update(conn, sql1, "AA"); //模拟网络异常 // System.out.println(10 / 0); String sql2 = "update user_table set balance = balance + 100 where user = ?"; update(conn, sql2, "BB"); System.out.println("转账成功"); //提交数据 conn.commit(); } catch (Exception e) { e.printStackTrace(); } finally { try { //恢复状态 //主要针对于数据库连接池时 conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } JDBCUtils.closeResource(conn, null); } }
我们首先要取消数据的自动提交,因为数据库默认每次DDL都会执行自动提交操作,最后也别忘了提交数据。
在最后我们同时要注意设置恢复状态,可能有人觉得没必要,因为连接是我们新建的,我们每次执行事务都要新建一个,不设置就不设置,无所谓的。
但是我们在项目里一般不回去新建连接,因为这样太浪费资源了,如果有很多人访问服务器那么服务器光在创建创建连接就要耗费不少资源,这是非常恐怖的一件事。
我们在项目中一般使用连接池来节省资源,这样我们每次操作都要把自动提交设置回去。
通用的查询一条记录方法 version 2.0
public <T> T getInstance(Connection conn,Class<T> clazz,String sql,Object ...args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); } rs = ps.executeQuery(); //获取结果集的元数据 ResultSetMetaData rsmd = rs.getMetaData(); //通过ResultSetMetaData,获取结果集的列数 int columnCount = rsmd.getColumnCount(); if(rs.next()){ T t = clazz.newInstance(); //处理结果集一行数据中的每一个列 for(int i=0;i<columnCount;i++) { //获取列值 Object columnValue = rs.getObject(i + 1); //获取每个列的列名 String columnLabel = rsmd.getColumnLabel(i + 1); //给cus对象指定的某个属性赋值为 obj 通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t,columnValue); } return t; } } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtils.closeResource(null,ps,rs); } return null; }
通用的查询多条记录方法 version 2.0
public <T> List<T> getForList(Connection conn,Class<T> clazz, String sql, Object ...args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); } rs = ps.executeQuery(); //获取结果集的元数据 ResultSetMetaData rsmd = rs.getMetaData(); //通过ResultSetMetaData,获取结果集的列数 int columnCount = rsmd.getColumnCount(); List<T> list = new ArrayList<>(); while(rs.next()){ T t = clazz.newInstance(); //处理结果集一行数据中的每一个列 for(int i=0;i<columnCount;i++) { //获取列值 Object columnValue = rs.getObject(i + 1); //获取每个列的列名 String columnLabel = rsmd.getColumnLabel(i + 1); //给cus对象指定的某个属性赋值为 obj 通过反射 Field field = clazz.getDeclaredField(columnLabel); field.setAccessible(true); field.set(t,columnValue); } list.add(t); } return list; } catch (Exception e) { e.printStackTrace(); }finally { JDBCUtils.closeResource(null,ps,rs); } return null; }
这里就与上面唯一的区别是conn用的是传入的参数,而且关闭资源的时候不要关掉。
查询特殊值:
public <E> E getValue(Connection conn,String sql,Object ...args) { PreparedStatement ps = null; ResultSet rs = null; try { ps = conn.prepareStatement(sql); for(int i=0;i<args.length;i++) { ps.setObject(i+1,args[i]); } rs = ps.executeQuery(); if(rs.next()){ return (E) rs.getObject(1); } } catch (SQLException e) { e.printStackTrace(); } finally { JDBCUtils.closeResource(null,ps,rs); } return null; }