Day34
三大范式及反范式设计
第一范式:
存在问题:
1.存在非常严重的数据冗余(重复)
2.数据添加存在问题
3.数据删除存在问题第二范式:
解决了一部分数据冗余,但仍然存在较严重的数据冗余问题,数据添加和删除问题依然存在。
第三范式:
数据冗余问题解决,数据添加和删除问题也解决,但是由于分了很多表,大大降低了查询效率。
反范式设计:
多表关联在查询时会消耗较多的时间,使用反范式设计会存在数据的冗余,但是减少了表的存在,也就是说牺牲了空间换取更多的时间
封装DBUtil(上)
编写一个工具类DBUtil,完成获取连接对象和发送sql指令对象以及关闭资源的功能。
注意:1.通过配置文件将driverName,url,username,password等参数传入。
2.工具类里面需要考虑哪些地方要处理异常,哪些地方抛出异常(涉及到具体业务的时候抛出)
package com.qf.utils; import java.io.IOException; import java.sql.*; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static{ Properties properties = new Properties(); try { properties.load(DBUtil.class.getClassLoader().getResourceAsStream("DBConfig.properties")); } catch (IOException e) { throw new RuntimeException(e); } String driverName=properties.getProperty("diverName"); url= properties.getProperty("url"); username= properties.getProperty("username"); password=properties.getProperty("password"); } public static Connection getConnection() throws SQLException { Connection connection = DriverManager.getConnection(url,username,password); return connection; } public static void close(Connection connection, Statement statement, ResultSet resultSet){ if(resultSet!=null){ try { resultSet.close(); } catch (SQLException e) { throw new RuntimeException(e); } } if (statement!=null){ try { statement.close(); } catch (SQLException e) { throw new RuntimeException(e); } } if(connection!=null){ try { connection.close(); } catch (SQLException e) { throw new RuntimeException(e); } } } }
配置文件:
driverName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/2042javaee?characterEncoding=utf8&serverTimezone=UTC username=root password=123456
使用工具类:
package com.qf.jdbc01; import com.qf.utils.DBUtil; import org.junit.Test; import java.sql.*; /** * 知识点:JDBC */ public class Test01 { //增 @Test public void test01() throws ClassNotFoundException, SQLException { //获取连接对象 Connection connection = DBUtil.getConnection(); //获取发送sql指令对象 Statement statement = connection.createStatement(); //发送sql指令 String sql="insert into student(name,sex,salary,age,course) values('喜羊羊','男',5000,28,'Java');"; int num = statement.executeUpdate(sql); System.out.println("对"+num+"行产生了影响"); //关闭资源 DBUtil.close(connection,statement,null); } //删 @Test public void test02() throws ClassNotFoundException, SQLException { //获取连接对象 Connection connection = DBUtil.getConnection(); //获取发送sql指令对象 Statement statement = connection.createStatement(); //发送sql指令 String sql="delete from student where id>8;"; int num = statement.executeUpdate(sql); System.out.println("对"+num+"行产生了影响"); //关闭资源 DBUtil.close(connection,statement,null); } //改 @Test public void test03() throws ClassNotFoundException, SQLException { //获取连接对象 Connection connection = DBUtil.getConnection(); //获取发送sql指令对象 Statement statement = connection.createStatement(); //发送sql指令 String sql="update student set salary=30000 where id=1;"; int num = statement.executeUpdate(sql); System.out.println("对"+num+"行产生了影响"); //关闭资源 DBUtil.close(connection,statement,null); } //查 @Test public void test04() throws ClassNotFoundException, SQLException { //获取连接对象 Connection connection = DBUtil.getConnection(); //获取发送sql指令对象 Statement statement = connection.createStatement(); //发送sql指令 String sql="select * from student;"; ResultSet resultSet = statement.executeQuery(sql); //遍历结果集 while(resultSet.next()){ int id = resultSet.getInt("id"); String name = resultSet.getString("name"); String sex = resultSet.getString("sex"); double salary = resultSet.getDouble("salary"); String course = resultSet.getString("course"); System.out.println(id+"--"+name+"--"+sex+"--"+salary+"--"+course); } //关闭资源 DBUtil.close(connection,statement,resultSet); } }
SQL注入问题
知识点:SQL注入问题
需求:模拟登录功能
出现原因:MySQL不知道哪些是sql命令,哪些是数据
package com.qf.jdbc02; import com.qf.utils.DBUtil; import java.sql.Connection; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Scanner; public class Test01 { /** * 知识点:SQL注入问题 * * 需求:模拟登录功能 * * 出现原因:MySQL不知道哪些是sql命令,哪些是数据 */ public static void main(String[] args) throws SQLException { Connection connection = DBUtil.getConnection(); Statement statement = connection.createStatement(); Scanner scan = new Scanner(System.in); System.out.println("请输入账号:"); String usernameVal = scan.nextLine(); System.out.println("请输入密码:"); String passwordVal = scan.nextLine(); //select * from user where username='' or 1=1 #' and password='123456789' String sql = "select * from user where username='" + usernameVal + "' and password='" + passwordVal + "';"; ResultSet resultSet = statement.executeQuery(sql); if(resultSet.next()){ System.out.println("登录成功"); String username = resultSet.getString("username"); String password = resultSet.getString("password"); String nickName = resultSet.getString("nick_name"); String sex = resultSet.getString("sex"); int age = resultSet.getInt("age"); System.out.println(username + " -- " + password + " -- " + nickName + " -- " + sex + " -- " + age); }else{ System.out.println("登录失败"); } DBUtil.close(connection,statement,resultSet); } }
注:当账号中输入’ or 1=1 #时,由于拼接的逻辑会变成账号输入为空(false)或者1=1(true)并且注释掉密码,然后返回值。出现原因:MySQL不知道哪些是sql命令,哪些是数据。
此类问题为SQL注入问题
解决方法:使用prepareStatement()方法告诉MySQL哪些是sql命令,哪些是数据。
package com.qf.jdbc02; import com.qf.utils.DBUtil; import java.sql.*; import java.util.Scanner; public class Test02 { /** * 知识点:SQL注入问题 * * 需求:模拟登录功能 * * 出现原因:MySQL不知道哪些是sql命令,哪些是数据 * 解决方案:告诉MySQL哪些是sql命令,哪些是数据 */ public static void main(String[] args) throws SQLException { Connection connection = DBUtil.getConnection(); //获取PreparedStatement对象 String sql = "select * from user where username=? and password=?"; PreparedStatement statement = connection.prepareStatement(sql); //设置参数 Scanner scan = new Scanner(System.in); System.out.println("请输入账号:"); String usernameVal = scan.nextLine(); System.out.println("请输入密码:"); String passwordVal = scan.nextLine(); statement.setString(1,usernameVal); statement.setString(2,passwordVal); ResultSet resultSet = statement.executeQuery(); if(resultSet.next()){ System.out.println("登录成功"); String username = resultSet.getString("username"); String password = resultSet.getString("password"); String nickName = resultSet.getString("nick_name"); String sex = resultSet.getString("sex"); int age = resultSet.getInt("age"); System.out.println(username + " -- " + password + " -- " + nickName + " -- " + sex + " -- " + age); }else{ System.out.println("登录失败"); } DBUtil.close(connection,statement,resultSet); } }
注:由于已经传入了sql命令,并且已经用set方法传入了参数,所以后面提交命令的时候不用再次传入sql。
主键回填
主键回填(Key Backfilling)是指在数据库插入操作后,自动生成的主键(通常是自增的主键值)被返回并填充到应用程序中相应的对象或变量中的过程。这在许多应用场景中非常有用,因为插入一条新记录后,我们往往需要立即知道这条记录的唯一标识符(主键)以便进行后续的操作,如更新、删除或关联到其他数据。
在 JDBC 中,通过使用 PreparedStatement和 RETURN_GENERATED_KEYS 常量,可以实现主键回填。
public static int commonInsert(String sql,Object... params) throws SQLException { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { connection = getConnection(); statement = connection.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS); paramsHandler(statement,params); statement.executeUpdate(); resultSet = statement.getGeneratedKeys(); int primaryKey = 0; if(resultSet.next()){ primaryKey = resultSet.getInt(1); } return primaryKey; } finally { close(connection,statement,resultSet); } }
封装DBUtil(中)
动态地将查询结果填充到指定类型的对象中,适用于许多不同类型的查询和数据对象。
public static <T> List<T> commonQuery(Class<T> clazz,String sql,Object... params) throws SQLException, InstantiationException, IllegalAccessException { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { connection = getConnection(); statement = connection.prepareStatement(sql); paramsHandler(statement,params); resultSet = statement.executeQuery(); //获取表信息对象 ResultSetMetaData metaData = resultSet.getMetaData(); //获取字段个数 int fieldCount = metaData.getColumnCount(); List<T> list = new ArrayList<>(); while(resultSet.next()){ //创建对象 T t = clazz.newInstance(); for (int i = 1; i <= fieldCount ; i++) { //获取字段名 String fieldName = metaData.getColumnName(i); //获取该数据行上对应字段的数据 Object val = resultSet.getObject(fieldName); //利用反射设置对象里对应的属性 setField(t,fieldName,val); } //将对象添加到List集合中 list.add(t); } return list; } finally { close(connection,statement,resultSet); } }
其中获取属性和设置属性方法为:
/** * 获取当前类及其父类的属性对象 * @param clazz class对象 * @param name 属性名 * @return 属性对象 */ private static Field getField(Class<?> clazz,String name){ for(Class<?> c = clazz;c != null;c = c.getSuperclass()){ try { Field field = c.getDeclaredField(name); return field; } catch (NoSuchFieldException e) { } catch (SecurityException e) { } } return null; } /** * 设置对象中的属性 * @param obj 对象 * @param name 属性名 * @param value 属性值 */ private static void setField(Object obj,String name,Object value){ Field field = getField(obj.getClass(), name); if(field != null){ field.setAccessible(true); try { field.set(obj, value); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } }
事务
事务是数据库执行的工作单位,一条SQL语句默认认为是一个事务,事务是自动管理提交的。若想多条SQL语句同时实行完毕,就需要将多条SQL语句放在同一个事务里
# 开启事务 START TRANSACTION; UPDATE bank SET money=money-200 WHERE name='aaa'; UPDATE bank SET money=money+200 WHERE name='bbb'; # 提交事务:将事务提交给数据库管理系统 # COMMIT; # 回滚事务:将数据回滚到开启事务的状态 ROLLBACK;
JDBC中操作:
public class Test01 { /** * 知识点:事务 * * 需求:模拟银行用户转账 */ public static void main(String[] args) { Connection connection = null; PreparedStatement statement1 = null; PreparedStatement statement2 = null; try { connection = DBUtil.getConnection(); //开启事务 connection.setAutoCommit(false); String sql1 = "UPDATE bank SET money=money-200 WHERE name='aaa';"; statement1 = connection.prepareStatement(sql1); statement1.executeUpdate(); System.out.println(10/0); String sql2 = "UPDATE bank SET money=money+200 WHERE name='bbb';"; statement2 = connection.prepareStatement(sql2); statement2.executeUpdate(); //提交事务 connection.commit(); } catch (Exception e) { //回滚事务 //通过在进行数据库操作(如回滚、提交或关闭)之前检查 connection 对象是否为 null,可以避免空指针异常 if(connection != null){ try { connection.rollback(); } catch (SQLException ex) { throw new RuntimeException(ex); } } System.out.println("发生异常,回滚事务了"); } finally { DBUtil.close(null,statement2,null); DBUtil.close(connection,statement1,null); } } }
但是转账逻辑中应该是同一个连接,并且多个转账事件同时发生时应该有自己的连接。因此要对DBUtil进行修改。
封装DBUtil(下)
使用ThreadLocal,确保每个线程有自己的连接。
package com.qf.utils; import java.io.IOException; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static{ Properties properties = new Properties(); try { properties.load(DBUtil.class.getClassLoader().getResourceAsStream("DBConfig.properties")); } catch (IOException e) { throw new RuntimeException(e); } String driverName = properties.getProperty("driverName"); url = properties.getProperty("url"); username = properties.getProperty("username"); password = properties.getProperty("password"); try { Class.forName(driverName); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } local = new ThreadLocal<>(); } private static ThreadLocal<Connection> local; /** * 开启事务 */ public static void startTransaction() throws SQLException { Connection connection = getConnection(); connection.setAutoCommit(false); } /** * 提交事务 */ public static void commit() throws SQLException { Connection connection = local.get(); if(connection != null){ connection.commit(); connection = null; } } /** * 回滚事务 */ public static void rollback() throws SQLException { Connection connection = local.get(); if(connection != null){ connection.rollback(); connection = null; } } /** * 获取连接对象 */ public static Connection getConnection() throws SQLException { Connection connection = local.get(); if(connection == null){ connection = DriverManager.getConnection(url, username, password); local.set(connection); } return connection; } /** * 关闭资源 */ public static void close(Connection connection, Statement statement, ResultSet resultSet) { if(resultSet != null){ try { resultSet.close(); } catch (SQLException e) { throw new RuntimeException(e); } } if(statement != null){ try { statement.close(); } catch (SQLException e) { throw new RuntimeException(e); } } if(connection != null){ try { if(connection.getAutoCommit()) {//自动提交事务的情况下 connection.close(); local.set(null); } } catch (SQLException e) { throw new RuntimeException(e); } } } /** * 更新数据(添加、删除、修改) */ public static int commonUpdate(String sql,Object... params) throws SQLException { Connection connection = null; PreparedStatement statement = null; try { connection = getConnection(); statement = connection.prepareStatement(sql); paramsHandler(statement,params); int num = statement.executeUpdate(); return num; } finally { close(connection,statement,null); } } /** * 主键回填 */ public static int commonInsert(String sql,Object... params) throws SQLException { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { connection = getConnection(); statement = connection.prepareStatement(sql,PreparedStatement.RETURN_GENERATED_KEYS); paramsHandler(statement,params); statement.executeUpdate(); resultSet = statement.getGeneratedKeys(); int primaryKey = 0; if(resultSet.next()){ primaryKey = resultSet.getInt(1); } return primaryKey; } finally { close(connection,statement,resultSet); } } public static <T> List<T> commonQuery(Class<T> clazz,String sql,Object... params) throws SQLException, InstantiationException, IllegalAccessException { Connection connection = null; PreparedStatement statement = null; ResultSet resultSet = null; try { connection = getConnection(); statement = connection.prepareStatement(sql); paramsHandler(statement,params); resultSet = statement.executeQuery(); //获取表信息对象 ResultSetMetaData metaData = resultSet.getMetaData(); //获取字段个数 int fieldCount = metaData.getColumnCount(); List<T> list = new ArrayList<>(); while(resultSet.next()){ //创建对象 T t = clazz.newInstance(); for (int i = 1; i <= fieldCount ; i++) { //获取字段名 String fieldName = metaData.getColumnName(i); //获取该数据行上对应字段的数据 Object val = resultSet.getObject(fieldName); //利用反射设置对象里对应的属性 setField(t,fieldName,val); } //将对象添加到List集合中 list.add(t); } return list; } finally { close(connection,statement,resultSet); } } /** * 处理sql命令中的参数 */ private static void paramsHandler(PreparedStatement statement,Object... params) throws SQLException { for (int i = 0; i < params.length; i++) { statement.setObject(i+1,params[i]); } } /** * 获取当前类及其父类的属性对象 * @param clazz class对象 * @param name 属性名 * @return 属性对象 */ private static Field getField(Class<?> clazz,String name){ for(Class<?> c = clazz;c != null;c = c.getSuperclass()){ try { Field field = c.getDeclaredField(name); return field; } catch (NoSuchFieldException e) { } catch (SecurityException e) { } } return null; } /** * 设置对象中的属性 * @param obj 对象 * @param name 属性名 * @param value 属性值 */ private static void setField(Object obj,String name,Object value){ Field field = getField(obj.getClass(), name); if(field != null){ field.setAccessible(true); try { field.set(obj, value); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } } }
场景
package com.qf.jdbc04; import com.qf.utils.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class Test02 { /** * 知识点:事务 * * 需求:模拟银行用户转账 */ public static void main(String[] args) { //转账的场景 // try { // DBUtil.startTransaction(); // // subtraction("aaa",200); // // //System.out.println(10/0); // // addition("bbb",200); // // DBUtil.commit(); // } catch (Exception e) { // try { // DBUtil.rollback(); // } catch (SQLException ex) { // throw new RuntimeException(ex); // } // } //单独存取钱的场景 try { subtraction("aaa",500); } catch (SQLException e) { throw new RuntimeException(e); } try { addition("bbb",1000); } catch (SQLException e) { throw new RuntimeException(e); } } public static void addition(String name,float money) throws SQLException { Connection connection = null; PreparedStatement statement = null; try { connection = DBUtil.getConnection(); System.out.println(connection); String sql = "UPDATE bank SET money=money+? WHERE name=?;"; statement = connection.prepareStatement(sql); statement.setFloat(1,money); statement.setString(2,name); statement.executeUpdate(); } finally { DBUtil.close(connection,statement,null); } } public static void subtraction(String name,float money) throws SQLException { Connection connection = null; PreparedStatement statement = null; try { connection = DBUtil.getConnection(); System.out.println(connection); String sql = "UPDATE bank SET money=money-? WHERE name=?;"; statement = connection.prepareStatement(sql); statement.setFloat(1,money); statement.setString(2,name); statement.executeUpdate(); } finally { DBUtil.close(connection,statement,null); } } }
事务的特点
事务的特性:ACID
原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持久性( Durability )
原子性:事务是数据库的逻辑工作单位,事务中包含的各操作要么都完成,要么都不完成
一致性:事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。
**隔离性:**一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。
持久性:指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
事务的隔离级别:
属于事务的。都已开启了事务为前提。
不考虑事务的隔离级别,会出现以下的情况
l 脏读:一个线程中的事务读到了另外一个线程中未提交的数据。
l 不可重复读:一个线程中的事务读到了另外一个线程中已经提交的update的数据。
l 虚读:一个线程中的事务读到了另外一个线程中已经提交的insert的数据。
要想避免以上现象,通过更改事务的隔离级别来避免:
l READ UNCOMMITTED 脏读、不可重复读、虚读有可能发生。
l READ COMMITTED 避免脏读的发生,不可重复读、虚读有可能发生。
l REPEATABLE READ 避免脏读、不可重复读的发生,虚读有可能发生。
l SERIALIZABLE 避免脏读、不可重复读、虚读的发生。
set transaction isolation level READ UNCOMMITTED;
级别依次升高,效率依次降低。
MySQL:默认REPEATABLE READ
ORACLE:默认READ COMMITTED
JDBC中设置隔离级别:
//设置事务的隔离界别 connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
批处理
在jdbc的url中添加rewriteBatchedStatements=true参数,可以提高批处理执行效率。
url=jdbc:mysql://localhost:3306/2042javaee?characterEncoding=utf8&serverTimezone=UTC&rewriteBatchedStatements=true
知识点:批处理
理解:批量处理数据需求:批量执行多条sql语句,sql语句不相同
public static void main(String[] args) throws SQLException { Connection connection = DBUtil.getConnection(); Statement statement = connection.createStatement(); //把多条SQL语句添加到Batch包中 String sql1 = "INSERT INTO student(name,sex,age,salary,course) VALUES('aaa','女',24,8000,'HTML');"; String sql2 = "update student set salary=99999 where id=1"; statement.addBatch(sql1); statement.addBatch(sql2); //发送Batch包 statement.executeBatch(); DBUtil.close(connection,statement,null); }
需求:批量插入100条记录,sql语句相同,只是参数值不同
public static void main(String[] args) throws SQLException { Connection connection = DBUtil.getConnection(); String sql = "INSERT INTO student(name,sex,age,salary,course) VALUES(?,'男',24,8000,'HTML')"; PreparedStatement statement = connection.prepareStatement(sql); for (int i = 1; i <= 100; i++) { statement.setString(1,"xxx" + i); statement.addBatch(); } statement.executeBatch(); DBUtil.close(connection,statement,null); }
当数据量过大时,采用批处理 + 事务,减少提交次数
public static void main(String[] args) throws SQLException { Connection connection = DBUtil.getConnection(); connection.setAutoCommit(false); String sql = "INSERT INTO student(name,sex,age,salary,course) VALUES(?,'男',24,8000,'HTML')"; PreparedStatement statement = connection.prepareStatement(sql); for (int i = 1; i <= 10000; i++) { statement.setString(1,"yyy" + i); statement.addBatch(); if(i%1000 == 0){ statement.executeBatch(); statement.clearBatch();//清空数据包中的sql命令 } } connection.commit(); DBUtil.close(connection,statement,null); }