JDBC数据库
基础篇
一.JDBC
1.JDBC的概念
JDBC: Java Database Connectivity,意为Java数据库连接
JDBC是java提供的一组独立于任何数据库管理系统的API
2.JDBC的核心组成
接口规范:
为了项目代码的可移植性,可维护性,sun公司从最初就制定了java程序连接各种数据库的统一接口规范,这样的话,不管是连接哪一种DBMS软件,java可以保持一致性
接口存储在java.sql和javax.sql下
实现规范:
因为各个数据库厂商的DBMS软件各有不同,那么各自的内部如何通过SQL实现增,删,改查等操作管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现
厂商将实现内容和过程封装成jar文件,我们程序员只需要将jar文件引入到项目中集成即可,就可以开发调用实现过程操作数据库了
3.快速入门
public class JDBCQuick { public static void main(String[] args) throws Exception { //注册驱动 Class.forName("com.mysql.cj.jdbc.Driver"); //获取连接 String url = "jdbc:mysql://localhost:3306/atguigu"; String username = "root"; String password = "1211lyshhy"; Connection connection = DriverManager.getConnection(url, username, password); //3.获取数据库操作对象 Statement statement = connection.createStatement(); //4.执行sql,返回结果集 String sql = "SELECT * FROM t_emp"; ResultSet resultSet = statement.executeQuery(sql); //5.处理结果集 while (resultSet.next()){ int id = resultSet.getInt("emp_id"); String name = resultSet.getString("emp_name"); double empSalary = resultSet.getDouble("emp_salary"); int empAge = resultSet.getInt("emp_age"); System.out.println("id = " + id + ", name = " + name + ", empSalary = " + empSalary + ", empAge = " + empAge); } //6.释放资源(先开后关) resultSet.close(); statement.close(); connection.close(); } }
4.核心api的理解
4.1注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
在java中,当使用JDBC连接数据库时,需要加载数据库特定的驱动程序,以便于数据库进行通信.加载驱动程序的目的是为了注册驱动程序,使得JDBC API能够识别并与特定的数据库进行交互.
从JDK6开始,不再需要显示的调用
Class.forname()
来加载JDBC 驱动程序,只要在类路径中集成了对应的jar文件,会自动在初始化时注册驱动程序.
4.2 Connection
Connection接口是JDBC API的重要接口,用于建立与数据库的通信通道。换而言之,Connection对象不为空,则代表一次数据库连接。
在建立连接时,需要指定数据库URL、用户名、密码参数。
URL:jdbc:mysql://localhost:3306/atguigu
jdbc:mysql://IP地址:端口号/数据库名称?参数键值对1&参数键值对2
Connection
接口还负责管理事务,Connection
接口提供了commit
和rollback
方法,用于提交事务和回滚事务。可以创建
Statement
对象,用于执行 SQL 语句并与数据库进行交互。在使用JDBC技术时,必须要先获取Connection对象,在使用完毕后,要释放资源,避免资源占用浪费及泄漏。
4.3 Statement
Statement
接口用于执行 SQL 语句并与数据库进行交互。它是 JDBC API 中的一个重要接口。通过Statement
对象,可以向数据库发送 SQL 语句并获取执行结果。结果可以是一个或多个结果。
增删改:受影响行数单个结果。
查询:单行单列、多行多列、单行多列等结果。
但是
Statement
接口在执行SQL语句时,会产生SQL注入攻击问题
:当使用
Statement
执行动态构建的 SQL 查询时,往往需要将查询条件与 SQL 语句拼接在一起,直接将参数和SQL语句一并生成,让SQL的查询条件始终为true得到结果。
4.4 PreparedStatement
PreparedStatement
是Statement
接口的子接口,用于执行预编译
的 SQL 查询,作用如下:预编译SQL语句:在创建PreparedStatement时,就会预编译SQL语句,也就是SQL语句已经固定。
防止SQL注入:
PreparedStatement
支持参数化查询,将数据作为参数传递到SQL语句中,采用?占位符的方式,将传入的参数用一对单引号包裹起来'',无论传递什么都作为值。有效防止传入关键字或值导致SQL注入问题。性能提升:PreparedStatement是预编译SQL语句,同一SQL语句多次执行的情况下,可以复用,不必每次重新编译和解析。
后续的学习我们都是基于PreparedStatement进行实现,更安全、效率更高!
4.5 ResultSet
ResultSet
是 JDBC API 中的一个接口,用于表示从数据库中执行查询语句所返回的结果集
。它提供了一种用于遍历和访问查询结果的方式。遍历结果:ResultSet可以使用
next()
方法将游标移动到结果集的下一行,逐行遍历数据库查询的结果,返回值为boolean类型,true代表有下一行结果,false则代表没有。获取单列结果:可以通过getXxx的方法获取单列的数据,该方法为重载方法,支持索引和列名进行获取。
5.基于PreparedStatement实现CRUD
5.1 查询单行单列
增删改只有查询语句不一样
public void testinsert2() throws Exception { Connection connection = DriverManager.getConnection ("jdbc:mysql:///atguigu", "root", "1211lyshhy"); PreparedStatement preparedStatement = connection.prepareStatement("insert into t_emp(emp_name,emp_salary,emp_age) values(?,?,?)"); // preparedStatement.setString(1,emp_id); preparedStatement.setString(1, "rose"); preparedStatement.setDouble(2, 345.67); preparedStatement.setInt(3, 28); int rowsInserted = preparedStatement.executeUpdate(); if (rowsInserted > 0) { System.out.println("Update successfully."); } else System.out.println("Update failed."); preparedStatement.close(); connection.close(); }
进阶篇
7.1ORM
在使用JDBC操作数据库时,我们会发现数据都是零散的,明明在数据库中是一行完整的数据,到了Java中变成了一个一个的变量,不利于维护和管理。而我们Java是面向对象的,一个表对应的是一个类,一行数据就对应的是Java中的一个对象,一个列对应的是对象的属性,所以我们要把数据存储在一个载体里,这个载体就是实体类!
ORM(Object Relational Mapping)思想,对象到关系数据库的映射,作用是在编程中,把面向对象的概念跟数据库中表的概念对应起来,以面向对象的角度操作数据库中的数据,即一张表对应一个类,一行数据对应一个对象,一个列对应一个属性!
7.2 主键回显
在数据中,执行新增操作时,主键列为自动增长,可以在表中直观的看到,但是在Java程序中,我们执行完新增后,只能得到受影响行数,无法得知当前新增数据的主键值。在Java程序中获取数据库中插入新数据后的主键值,并赋值给Java对象,此操作为主键回显。
@Test public void testReturnPK() throws Exception { //注册驱动 //获取连接 Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "1211lyshhy"); //获取数据库操作对象,告知preparedstatement,返回新增数据的主键列的值 PreparedStatement preparedStatement = connection.prepareStatement ("insert into t_emp( emp_name, emp_salary, emp_age) values(?,?,?) ", Statement.RETURN_GENERATED_KEYS); //执行sql语句返回结果集 Employee employee = new Employee(null,"jack",123.45,29); preparedStatement.setString(1, employee.getEmpName()); preparedStatement.setDouble(2, employee.getEmpSalary()); preparedStatement.setInt(3,employee.getEmpAge()); int resultSet = preparedStatement.executeUpdate(); ResultSet generatedKeys = null; //处理结果集 if (resultSet > 0) { System.out.println("success"); //返回的主键值,是一个单行单列的结果 generatedKeys = preparedStatement.getGeneratedKeys(); if (generatedKeys.next()) { int emp_id = generatedKeys.getInt(1); System.out.println(emp_id); } }else { System.out.println("fail"); } //关闭资源 if (generatedKeys != null) { generatedKeys.close(); } preparedStatement.close(); connection.close(); }
7.3批量操作
按照上面的代码,如果要连续修改多条数据,增加多条数据,就要写很多代码
由此我们需要一个高效的批量操作
//源代码 @Test public void testMoreInsert() throws Exception { Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu", "root", "1211lyshhy"); PreparedStatement preparedStatement = connection.prepareStatement("insert into t_emp( emp_name, emp_salary, emp_age) values(?,?,?) "); Long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++ ) { preparedStatement.setString(1, "marry"+i); preparedStatement.setDouble(2, 100.0+i); preparedStatement.setInt(3, 20+i); preparedStatement.executeUpdate(); } Long end = System.currentTimeMillis(); System.out.println(end - start); preparedStatement.close(); connection.close(); }花费11秒多
//批量操作 @Test public void testMoreBatch() throws Exception { Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?rewriteBatchedStatements=true", "root", "1211lyshhy"); PreparedStatement preparedStatement = connection.prepareStatement("insert into t_emp( emp_name, emp_salary, emp_age) values(?,?,?) ");//不能在sql语句上加分号 Long start = System.currentTimeMillis(); for (int i = 0; i < 10000; i++ ) { preparedStatement.setString(1, "marry"+i); preparedStatement.setDouble(2, 100.0+i); preparedStatement.setInt(3, 20+i); preparedStatement.addBatch(); } preparedStatement.executeBatch(); Long end = System.currentTimeMillis(); System.out.println(end - start); preparedStatement.close(); connection.close(); }
rewriteBatchedStatements=true
是 MySQL 的一个优化参数,用于启用批量操作的优化。没有这个参数,批量插入可能不会真正被优化。
使用 PreparedStatement
提高性能和安全性,防止 SQL 注入。
使用循环为每条记录设置参数,并通过 addBatch
将 SQL 语句添加到批处理队列。
还可以优化: 可以通过分批次成批
连接池
连接池概述:
连接池就是数据库链接对象的缓冲区,通过配置,有连接池负责连接,管理连接,释放连接等操作
预先创建数据库连接放入链接池,用户请求时,通过池直接获取链接,使用完毕后,将连接放回池中,避免了频繁的创建和销毁,同时解决了创建的效率
当池中无连接可用,且未达到上限时,连接池会创建新连接
池中连接达到上限,用户会请求等待,可以设置超时时间
常见连接池 Druid
Hikari
Druid连接池使用
代码实现
硬编码方式
public class DruidTest { public void testHardCodeDruid() throws Exception { /* * 硬编码: 将连接池的配置信息和Java代码耦合在一起 * 1.创建DruidDataSource连接池对象 * 2.设置连接池的配置信息[必须|非必须] * 3.通过连接池获取连接对象 * 4.回收连接(不是释放连接,而是将连接归还给连接池,给其他线程进行复用) * */ //1.创建DruidDataSource连接池对象 DruidDataSource druidDataSource = new DruidDataSource(); // 2.设置连接池的配置信息[必须|非必须] // 2.1必须设置的配置 druidDataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); druidDataSource.setUrl("jdbc:mysql:///atguigu"); druidDataSource.setUsername("root"); druidDataSource.setPassword("1211lyshhy"); // 2.2非必须设置的配置 druidDataSource.setInitialSize(10);//创建连接对象,初始化10个 druidDataSource.setMaxActive(20);// // 3.通过连接池获取连接对象 DruidPooledConnection connection = druidDataSource.getConnection(); //基于connection进行CRUD //4.回收连接 } }
软编码方式
把配置信息保存在一个配置文件中
@Test public void testResourcesDruid() throws Exception { // 1.创建一个properties集合,用于存储外部配置文件的key和value值 Properties properties = new Properties(); // 2.读取外部配置文件,获取输入流,加载到Properties集合里 InputStream resourceAsStream = DruidTest.class.getClassLoader().getResourceAsStream("db.properties"); properties.load(resourceAsStream); // 3.基于Properties集合构建DruidDataSources连接池 DataSource dataSource = DruidDataSourceFactory.createDataSource(properties); // 4.通过连接池获取连接对象 Connection connection = dataSource.getConnection(); System.out.println(connection); // 5.开发CRUD // 6.回收连接 connection.close(); }
Hikari连接池使用
软编码
Test public void testResourcesHikari() throws Exception { Properties properties = new Properties(); InputStream resourceAsStream = HikariTest.class.getClassLoader().getResourceAsStream("hakari.properties"); properties.load(resourceAsStream); HikariConfig hikariConfig = new HikariConfig(properties); HikariDataSource hikariDataSource = new HikariDataSource(hikariConfig); Connection connection = hikariDataSource.getConnection(); System.out.println(connection); connection.close(); }
硬编码
@Test public void testHardCodeHikari() throws Exception { /* * 硬编码: 将连接池的配置信息和Java代码耦合在一起 * */ HikariDataSource ds = new HikariDataSource(); ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setJdbcUrl("jdbc:mysql://localhost:3306/atguigu"); ds.setUsername("root"); ds.setPassword("1211lyshhy"); ds.setMinimumIdle(10); ds.setMaximumPoolSize(20); Connection connection = ds.getConnection(); System.out.println(connection); connection.close(); }
软编码的好处
把信息写入配置文件中而不是在java代码里
提高灵活性:
通过修改外部配置文件即可调整程序行为,无需重新编译或部署代码。
例如:更改数据库连接信息、切换日志级别等。
增强可维护性:
配置集中管理,便于查找和修改。
代码更简洁,逻辑更清晰,降低维护难度。
支持动态调整:
某些配置可以在程序运行时动态加载,支持热更新,无需重启服务。
例如:通过 Spring 的
@ConfigurationProperties
动态加载配置。
提高复用性:
相同的代码可以在不同的环境下运行,只需切换配置文件即可。
例如:开发环境、测试环境和生产环境使用不同的配置。
方便测试与调试:
测试人员可以通过修改配置文件模拟不同的运行环境和条件。
减少硬编码导致的测试环境不一致问题。
增强可扩展性:
新增功能时,只需扩展配置文件,而无需修改代码。
例如:支持新的支付方式,只需在配置文件中添加对应的支付网关信息。
支持国际化:
通过软编码实现多语言支持(如外置的资源文件),可根据用户语言动态加载。
降低错误风险:
硬编码的内容难以发现和修改,可能导致不可预期的错误。
软编码将配置集中管理,减少因代码改动引入的错误。
高级篇
JDBC工具类封装
/* JDBC工具类(v2.0) * 1.维护一个连接池对象,维护了一个线程绑定的ThreadLocal对象 * 2.对外提供在ThreadLocal中获取连接的方法 * 3.对外提供回收连接的方法,回收过程中,将要回收的连接在threadlocal中移除 */ public class JDBCUtilV2 { private static DataSource dataSource; private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); static { try { Properties properties = new Properties(); InputStream resourceAsStream = JDBCUtil.class.getClassLoader().getResourceAsStream("db.properties"); properties.load(resourceAsStream); dataSource= DruidDataSourceFactory.createDataSource(properties); } catch (Exception e) { throw new RuntimeException(e); } } public static Connection getConnection(){ try { //threadlocal中获取连接 Connection connection = threadLocal.get(); if(connection==null){ connection = dataSource.getConnection();//获取一个连接,存储在local里 threadLocal.set(connection); } return connection; } catch (SQLException e) { throw new RuntimeException(e); } } public static void release(){ try { Connection connection = threadLocal.get(); if(connection!=null){ connection.close(); threadLocal.remove(); } } catch (SQLException e) { throw new RuntimeException(e); } }
DAO封装
public class BaseDao { public int executeUpdate(String sql, Object... params) throws Exception { Connection connection = JDBCUtilV2.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql); if (params != null && params.length > 0) { for (int i = 0; i < params.length; i++) { preparedStatement.setObject(i + 1, params[i]); } } int row = preparedStatement.executeUpdate(); preparedStatement.close(); JDBCUtilV2.release(); return row; } /* 通用的查询: 多行多列,单行多列,单行单列 多行多列: List<Employees> 单行多列: Employee 单行多列: 封装的是一个结果 封装过程: 1.返回的类型: 泛型: 类型不确定,调用者知道,调用时,将此次查询的结果类型告知baseDao 2.返回的结果: 通用 List 可以返回多个结果,也可以返回单个结果 3.结果的封装: 反射,要求调用者告知BaseDao要封装对象的类的对象*/ public <T> List<T> excuteQuery(Class<T> clazz,String sql, Object... params)throws Exception { Connection connection = JDBCUtilV2.getConnection(); PreparedStatement preparedStatement = connection.prepareStatement(sql); if (params != null && params.length > 0) { for (int i = 0; i < params.length; i++) { preparedStatement.setObject(i + 1, params[i]); } } ResultSet resultSet = preparedStatement.executeQuery(); ResultSetMetaData metaData = resultSet.getMetaData(); List<T> list = new ArrayList<>(); while (resultSet.next()) { T t = clazz.newInstance(); for (int i = 1; i <= metaData.getColumnCount(); i++) { Object value = resultSet.getObject(i); String fieldName = metaData.getColumnLabel(i); Field field = clazz.getDeclaredField(fieldName); field.setAccessible(true); field.set(t, value); } list.add(t); } resultSet.close(); preparedStatement.close(); JDBCUtilV2.release(); return list; } public <T> List<T> excuteQueryBean(Class<T> clazz,String sql, Object... params)throws Exception{ List<T> ts = this.excuteQuery(clazz, sql, params); if (ts == null || ts.size() == 0) { return null; } return (List<T>) ts.get(0); } }