文章目录
推荐后学
第一章:概述
1.1 JavaWeb技术
-
B/S Brower/Server
-
C/s Client/Server
-
目前前端三大技术。html,css,js,框架:Vue,react。
1.2 JDBC概述
-
JDBC 直接访问数据库
-
JDO 技术(Java Data Object)
-
第三方 O/R 工具,如 Hibernate, Mybatis 等
-
JDBC 是 java 访问数据库的基石,JDO, Hibernate 等只是更好的封装了 JDBC。
-
JDBC(Java Database Connectivity)是一个独立于特定数据库管理系统(DBMS)、通用的 SQL 数据库存取和操作的 公共接口(一组 API),定义了用来访问数据库的标准 Java 类库,使用这个类库可以以一种标准的方法、方便地访 问数据库资源 JDBC 为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题。 JDBC 的目标是使 Java 程序员使用 JDBC 可以连接任何提供了 JDBC 驱动程序的数据库系统,这样就使得程序员无需 对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。
有 JDBC,那么 Java 程序访问数据库时是这样的:
https://i.loli.net/2021/06/21/hwHcgGe3IkQWLyZ.png
如果没有JDBC,每种数据库都有一种规范,都给开发者造成很大麻烦,JDBC便定义了这种规范接口,让各自厂商实现自己的驱动,而开发者只是面向该接口去编程就可以了。
1.3 JDBC程序编写步骤
https://i.loli.net/2021/06/21/tqGvP2DzWUZyabL.png
第二章:获取数据库连接
2.1 Driver(驱动)接口实现
- 五种方式获取驱动,即与数据库连接,请详细看代码注释
package com.hyb.JDBC;
import com.mysql.cj.jdbc.Driver;
import org.junit.Test;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;
/**
* @program: ConectionTest
* @description:
* @author: Huang Yubin
* @create: 2021-06-21 19:48
**/
public class ConnectionTest {
// 方式一,用Driver接口
@Test
public void testConnection1() throws SQLException {
// 获取jar包下的驱动类,要是没有jar包,请上网查找如何导入,应用
Driver driver = new Driver();
// 获取你数据库的URL,jdbc:mysql:协议,localhost ip地址,3306 端口号,你的想要操作数据库的名称
String url="jdbc:mysql://localhost:3306/mysqldata";
// 获取一个Properties,来获取用户和密码
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","15717747056HYB");
// 进行连接
Connection connect = driver.connect(url, info);
System.out.println(connect);
// 获得数据库的toString
// com.mysql.cj.jdbc.ConnectionImpl@38364841
}
// 方式二,对方式一的迭代
// 为什么要迭代?
// Java具有可移植性,我们可以看到在本类的开头
// import com.mysql.cj.jdbc.Driver;
// 这说明我们在用方式一连接数据库的时候需要导包,这也就限制了我们的移植性,
// 所以我们要解决这一矛盾,可以需要用到反射去获取import com.mysql.cj.jdbc.Driver;
@Test
public void testConnection2() throws Exception{
// 利用反射获取import com.mysql.cj.jdbc.Driver;
Class<?> classConnection = Class.forName("com.mysql.cj.jdbc.Driver");
// 利用反射获取运行时类的对象
Driver driver= (Driver) classConnection.newInstance();
// 然后其他步骤都一样
String url="jdbc:mysql://localhost:3306/mysqldata";
// 获取一个Properties,来获取用户和密码
Properties info=new Properties();
info.setProperty("user","root");
info.setProperty("password","15717747056HYB");
// 进行连接
Connection connect = driver.connect(url, info);
System.out.println(connect);
// com.mysql.cj.jdbc.ConnectionImpl@63a65a25
}
// 加入 DriverManager
@Test
public void testConnection3() throws Exception {
// 利用反射获取import com.mysql.cj.jdbc.Driver;
Class<?> classConnection = Class.forName("com.mysql.cj.jdbc.Driver");
// 获取对象
Driver driver = (Driver) classConnection.newInstance();
// 注册驱动
DriverManager.registerDriver(driver);
String url="jdbc:mysql://localhost:3306/mysqldata";
String u="root";
String p="15717747056HYB";
Connection connection = DriverManager.getConnection(url, u, p);
System.out.println(connection);
// com.mysql.cj.jdbc.ConnectionImpl@54c562f7
}
// 方式四:对方式三的优化
@Test
public void testConnection4() throws Exception {
// 利用反射获取import com.mysql.cj.jdbc.Driver;
// 删除Class<?> classConnection =
Class.forName("com.mysql.cj.jdbc.Driver");
// 如下操作可以被注释掉:
// 获取对象
// Driver driver = (Driver) classConnection.newInstance();
// 注册驱动
// DriverManager.registerDriver(driver);
// 为什么可以?
// 在mysql的实现类的众,声明了如下操作
// static {
// try {
// DriverManager.registerDriver(new Driver());
// } catch (SQLException var1) {
// throw new RuntimeException("Can't register driver!");
// }
// }
// 它会自动帮你创建
String url="jdbc:mysql://localhost:3306/mysqldata";
String u="root";
String p="15717747056HYB";
Connection connection = DriverManager.getConnection(url, u, p);
System.out.println(connection);
// com.mysql.cj.jdbc.ConnectionImpl@54c562f7
}
// 最终版,我们都知道,程序一般都不要暴露我们的信息,所以我们要将自己的数据库信息放到一个配置文件里再读取
@Test
public void finalConnection() throws Exception{
// 我们要用类的加载器来获取配置文件,所以要在src下建立Properties格式的文件
// user=root
// password=15717747056HYB
// url=jdbc:mysql://localhost:3306/mysqldata
// driverClass=com.mysql.cj.jdbc.Driver
// 尽量不要有空格,除非你的数据本身就有空格
InputStream o = ConnectionTest.class.getClassLoader().getResourceAsStream("myTest.properties");
Properties p = new Properties();
p.load(o);
String user = p.getProperty("user");
String password = p.getProperty("password");
String url = p.getProperty("url");
String driverClass = p.getProperty("driverClass");
Class.forName(driverClass);
Connection connection = DriverManager.getConnection(url, user, password);
System.out.println(connection);
// com.mysql.cj.jdbc.ConnectionImpl@54c562f7
}
}
第三章:PreparedStatement 实现CRUD
- CRUD增删改查
3.1 Statement
- 弊端:会出现SQL语句注入问题,在用户输出非法的用户名和密码,可能会产生恶意的SQL语句,造成也登录成功的后果。
3.2 PreparedStatement
- 是Statement的子接口,用其可解决Statement的问题
- 预编译的SQL语句
3.3 实现 增
package com.hyb.JDBC;
import org.junit.Test;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Properties;
/**
* @program: QueryTest
* @description:
* @author: Huang Yubin
* @create: 2021-06-22 09:43
**/
public class SQLTest {
@Test
public void InsertTest() {
Connection connection = null;
PreparedStatement ps = null;
try {
InputStream o = SQLTest.class.getClassLoader().getResourceAsStream("myTest.properties");
Properties p = new Properties();
p.load(o);
String user = p.getProperty("user");
String password = p.getProperty("password");
String url = p.getProperty("url");
String driverClass = p.getProperty("driverClass");
Class.forName(driverClass);
connection = DriverManager.getConnection(url, user, password);
// 连接成功后,我们对自己的数据库表进行操作,下面给出我的数据库表
// name_b age work salary
// hyb 21 student 0
// hfy 13 father 1000
// zyl 21 student 0
// lzm 12 student 0
// cjk 13 film 10000
// 写入SQL语句
String sql="insert into firstme(name_b,age,work,salary) values(?,?,?,?)";
ps = connection.prepareStatement(sql);
// 填充占位符
ps.setString(1,"newHyb");
ps.setInt(2,21);
ps.setString(3,"student");
ps.setString(4,"0");
// 执行
ps.execute();
} catch (IOException | SQLException | ClassNotFoundException e) {
e.printStackTrace();
} finally {
try {
assert ps != null;
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
connection.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
// 插入成功后,会出现以下内容
// name_b age work salary
// hyb 21 student 0
// hfy 13 father 1000
// zyl 21 student 0
// lzm 12 student 0
// cjk 13 film 10000
// newHyb 21 student 0
}
}
3.4 *封装 增删改
-
以上,当我们进行增删改查数据库时都要进行连接和关闭资源的操作,所以可以对他们进行封装。
package com.hyb.JDBC; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.Properties; /** * @program: SQLConnection * @description: * @author: Huang Yubin * @create: 2021-06-22 10:31 **/ public class SQLConnection { public static Connection connect() throws Exception { Connection connection = null; InputStream o = SQLTest.class.getClassLoader().getResourceAsStream("myTest.properties"); Properties p = new Properties(); p.load(o); String user = p.getProperty("user"); String password = p.getProperty("password"); String url = p.getProperty("url"); String driverClass = p.getProperty("driverClass"); Class.forName(driverClass); connection = DriverManager.getConnection(url, user, password); return connection; } public static void Close(Connection cnt,PreparedStatement ps){ try { assert ps != null; ps.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } try { cnt.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } }
-
进行封装后,我们测试,执行修改数据库某条指定的操作
@Test public void upDatedSQl(){ Connection connect = null; PreparedStatement ps = null; try { // 1.获取数据库连接 connect = SQLConnection.connect(); // 2.进行SQl语句的预编译 ps = connect.prepareStatement("update firstme set name_b=? where name_b=? "); // 3. 填充占位符 ps.setObject(1,"oldHyb"); ps.setObject(2,"newHyb"); ps.execute(); System.out.println("修改成功!"); } catch (Exception e) { e.printStackTrace(); } finally { assert ps != null; SQLConnection.Close(connect,ps); } // 修改成功! // oldHyb student 21 0 }
-
封装一次后,是否可以再封装,答案是可以的!
数据库连接和资源关闭我们已经封装了,所以我们下一步进行增删改的操作,这里没有查,查询不一样。
package com.hyb.JDBC; import java.sql.Connection; import java.sql.PreparedStatement; /** * @program: SQLUpdate * @description: * @author: Huang Yubin * @create: 2021-06-22 11:10 **/ public class SQLUpdate { // Connection connect = null; // PreparedStatement ps = null; // try { 1.获取数据库连接 // connect = SQLConnection.connect(); 2.进行SQl语句的预编译 // ps = connect.prepareStatement("update firstme set name_b=? where name_b=? "); 3. 填充占位符 // ps.setObject(1,"oldHyb"); // ps.setObject(2,"newHyb"); // ps.execute(); // System.out.println("修改成功!"); // } catch (Exception e) { // e.printStackTrace(); // } finally { // assert ps != null; // SQLConnection.Close(connect,ps); // } // 要包装,我们要解决几个问题: // 1.每次传入的sql语句都不一样 // 2.更新语句不一样 // 1.我们可以传入一个String类型的字符串 // 2.这个更新语句,我们可以用可变形参,类型不一样,直接定义为Object类型便可以了 public static void updateSQL(String sql,Object ...arg){ Connection connect = null; PreparedStatement ps = null; try { // 1.获取数据库连接 connect = SQLConnection.connect(); // 2.进行SQl语句的预编译 ps = connect.prepareStatement(sql); // 3. 填充占位符 for (int i = 0; i < arg.length; i++) { // 这里填充占位符,记住在我们sql语句里,索引是从1开始的,而数组是从0开始的 ps.setObject(i+1,arg[i]); } ps.execute(); System.out.println("修改成功!"); } catch (Exception e) { e.printStackTrace(); } finally { assert ps != null; SQLConnection.Close(connect,ps); } } }
接下来,我们调用
@Test public void testSQLUpdate(){ SQLUpdate.updateSQL("update firstme set name_b=? where name_b=?","reNewHyb","oldHyb"); //这里对于每个表都是有用的,不过有时候会报错,因为表名有可能是SQL语句里的关键字,所以要是报错,可以检查一下表名,两边加单引号就可以了 // reNewHyb }
3.5 查
-
查询数据表操作,类似,但又有些不同,要处理返回的结果,也就是结果集。
-
但一张表可能有很多属性,而且每个属性都有很多数据,即使我们用数组来操作,依旧显得有些无力。
所以,我们一般用类来操作整个表。
那么就要引入ORM思想:
- 一个数据表对应一个类
- 表中的每条记录对应一个对象。
- 表中的字段对应类的各个属性。
-
下面我们如同增删改的步骤一样,先引入我们一般的查询写法。
@Test public void testQuery(){ PreparedStatement ps = null; ResultSet rs = null; try { // 1.我们封装了连接和关闭资源操作,直接调用就可以了 Connection connect = SQLConnection.connect(); // 2.进行SQl的预编译 ps = connect.prepareStatement("select name_b,age,work,salary from firstme where name_b=? "); // 3.填充占位符,第一个占位符,为hyb ps.setObject(1,"hyb"); // 4.启动查询,方法有些不一样 rs = ps.executeQuery(); // 5.对结果集进行处理,这里的处理思想和迭代器一样 if (rs.next()){ // 5.1 记住,rs表示整张表,而我们数据库的索引是从1开始的。 String name = rs.getString(1); int age = rs.getInt(2); String work= rs.getString(3); String salary = rs.getString(4); // 5.2 之后,我们要处理这里数据,让他们输出,可以采用ORM思想,建立这个表的类 firstmeTableClass ftc = new firstmeTableClass(name, age, work, salary); System.out.println(ftc); // firstmeTableClass{name_b='hyb', age=21, work='student', salary='0'} } } catch (Exception e) { e.printStackTrace(); } finally { // 6.关闭资源,也可以直接调用我们之前封装好的 try { assert rs != null; rs.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } try { ps.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } }
查询方法写完后,我们为方法里的类补全
package com.hyb.JDBC; /** * @program: firstmeClass * @description: * @author: Huang Yubin * @create: 2021-06-22 12:42 **/ public class firstmeTableClass { private String name_b; private int age; private String work; private String salary; public firstmeTableClass() { } public firstmeTableClass(String name_b, int age, String work, String salary) { this.name_b = name_b; this.age = age; this.work = work; this.salary = salary; } public String getName_b() { return name_b; } public void setName_b(String name_b) { this.name_b = name_b; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getWork() { return work; } public void setWork(String work) { this.work = work; } public String getSalary() { return salary; } public void setSalary(String salary) { this.salary = salary; } @Override public String toString() { return "firstmeTableClass{" + "name_b='" + name_b + '\'' + ", age=" + age + ", work='" + work + '\'' + ", salary='" + salary + '\'' + '}'; } }
3.6 *封装 查
package com.hyb.JDBC;
import javax.management.Query;
import java.lang.reflect.Field;
import java.sql.*;
/**
* @program: Query,通用查询
* @description:
* @author: Huang Yubin
* @create: 2021-06-22 12:08
**/
/*通用查询从操作,我们会遇到如下几个问题
* 1.传入什么sql语句
* 2.占位符有几个
* 3.我们都知道,一个表里有多少个列,我们定义的类就有多少个属性;
* 而这些属性个数正好也决定了这个类中的带参构造器的参数个数;
* 我们查询的时候要用到ORM思想,就必须去new对象,并初始化,
* *但当我们要查询的列数和表的列数不相同(也就是和类的构造器参数的个数不同)时,我们该如何去初始化这个类的对象?
* */
public class SQLQuery {
public static Object Query(String sql,Object ...arg){
Connection connect = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// 1.数据库连接
connect = SQLConnection.connect();
// 2.进行sql预编译
ps = connect.prepareStatement("select name_b,age from firstme where name_b=? ");
// 3.填充占位符
for (int i = 0; i < arg.length; i++) {
ps.setObject(i+1,arg[i]);
}
// 4.启动查询,返回一个结果集
rs = ps.executeQuery();
// 5.迭代
// 5.1.1 获取结果集的元数据,因为还是结果,所以我们要在迭代之前获取元数据
ResultSetMetaData mdt = rs.getMetaData();
// 5.1.2 获取这些元数据的列数
int columnCount = mdt.getColumnCount();
if (rs.next()){
// 5.1 在没有封装之前,我们要将我们要查的列传入到一个类中
// 但是现在这里不是所有的列,就不能用构造器的方法去初始化一个对象
// 所以我们要解决这个问题,只能利用反射去初始化一个对象的属性
// 但是要初始化这些属性,也必须知道我们到底要查哪些列,而上面我们进行预编译的sql是String类型的,所以很难获取。
// 所以要引入*元数据*的概念,String name=“hyb”,hyb便是这个String name的元数据(5.1.1)。
// 5.1.3 初始化一个对象,没有初始值
firstmeTableClass ftc = new firstmeTableClass();
for (int i = 0; i < columnCount; i++) {
// 5.1.4 获取每个列的列值,待会我们要将它赋值给类里的属性
Object o = rs.getObject(i + 1);
// 5.1.5 获取每列的列名,
String columnName = mdt.getColumnName(i+1);
// 5.1.6 通过反射初始化columnName这个列的值
Field df = firstmeTableClass.class.getDeclaredField(columnName);
df.setAccessible(true);
df.set(ftc,o);//df是ftc对象里的属性,被赋值为o
}
// 5.1.7 每一行都是一个对象,每次有行都要返回
return ftc;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6.资源关闭
SQLConnection.Close(connect,ps);
try {
if (rs!=null) {
rs.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return null;
}
}
封装完毕后,我们执行
@Test
public void Query(){
Object hybData = SQLQuery.Query("select name_b,age from firstme where name_b=? ", "hyb");
System.out.println(hybData);
// firstmeTableClass{name_b='hyb', age=21, work='null', salary='null'}
}
3.7 *思考
-
上面我们用自定义类代表数据表,下面引出一个问题:如果类中的属性名和数据表中的列名不一样会发生什么?
无法找到列名异常!
为什么会出现这种情况?请看下列代码:
// 5.1.5 获取每列的列名, String columnName = mdt.getColumnName(i+1); // 5.1.6 通过反射初始化columnName这个列的值 Field df = firstmeTableClass.class.getDeclaredField(columnName); df.setAccessible(true); df.set(ftc,o);//df是ftc对象里的属性,被赋值为o
我们可以看到,反射机制是根据你列名与类对应的属性名相等才可以赋值的,如果不相等,自然会报错。
那我们该如何解决这各小矛盾呢?总不能每一次定义的类的属性名和数据表里的列名一样,那样不规范了。
可以利用sql语句的一个小知识来解决,每次查询的sql语句,我们都可以为被查询列赋一个新的名字,不妨就利用这个细节去处理
ps = connect.prepareStatement("select name_b 类对应的属性名,age 类对应的属性名 from firstme where name_b=? ");
可是这样修改后,再去运行,我们会发现,又报错了,还是无法找到列名异常?
答案在这里:
// 5.1.5 获取每列的列名, String columnName = mdt.getColumnName(i+1);
这里我们可以看到,这个函数获取的还是列名,并不是列的别名,所以在Java里,提供了一个方法,没有别名就获取列名,有列名就获取别名的方法。
String columnName = mdt.getColumnLabel(i+1);
这样就完美了!
3.8 *任意表查询
-
前面我们对一种查询都进行了封装,但这些封装还是比较固定的,为了描述问题,请看(封装 查)里的一些代码:
public static Object Query(String sql,Object ...arg) .... firstmeTableClass ftc = new firstmeTableClass(); .... Object o = rs.getObject(i + 1); .... Field df = firstmeTableClass.class.getDeclaredField(columnName);
以上这些代码都是些在程序里的,把他们写死了,肯定限制了可移植性,我们目标要的肯定是什么类,什么表,只要用了这个方法,返回值就是想要的结果。
上面有四条语句,我们可以一步一步解决上面的问题:
- 首先,在函数声名的时候,我们可以将这个语句声名为泛型。
- 在new一个类的对象时,我们可以利用反射获取运行时类的对象。
- 那么,其他问题就很容易解决了。
-
改变后:
// 第一个<T>,决定了该方法为泛型方法,对任意表的查询, // T 代表返回值类型 // 第二个<T>泛型类,决定了返回值类型 public static <T> T Query(Class<T> className,String sql,Object ...arg)
T ftc = className.newInstance(); for (int i = 0; i < columnCount; i++) { // 5.1.4 获取每个列的列值,待会我们要将它赋值给类里的属性 Object o = rs.getObject(i + 1); // 5.1.5 获取每列的列名, String column = mdt.getColumnLable(i+1); // 5.1.6 通过反射初始化columnName这个列的值 Field df = ftc.getClass().getDeclaredField(column); df.setAccessible(true); df.set(ftc,o);//df是ftc对象里的属性,被赋值为o }
/*对于封装查询后的方法查询*/ @Test public void Query(){ Object hybData = SQLQuery.Query(firstmeTableClass.class, "select name_b,age from firstme where name_b=? ", "hyb"); System.out.println(hybData); // firstmeTableClass{name_b='hyb', age=21, work='null', salary='null'} }
3.9 利用集合处理多行的结果集
- 前面我们可以观察到,好像我输出一般都是一行一行或者是全部的,但是我们想要其中几行呢?不难想象,我们拿集合就可以了。
public static <T> List<T> ListQuery(Class<T> className,String sql,Object ...arg ){
Connection connect = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
connect = SQLConnection.connect();
ps = connect.prepareStatement(sql);
for (int i = 0; i < arg.length; i++) {
ps.setObject(i+1,arg[i]);
}
rs = ps.executeQuery();
ArrayList<T> obj = new ArrayList<>();
ResultSetMetaData mt = rs.getMetaData();
int columnCount = mt.getColumnCount();
while (rs.next()){
T t = className.newInstance();
for (int i = 0; i < columnCount; i++) {
String cl = mt.getColumnLabel(i+1);
Field dfd = className.getDeclaredField(cl);
dfd.setAccessible(true);
Object o = rs.getObject(i + 1);
dfd.set(t,o);
}
obj.add(t);
}
return obj;
} catch (Exception e) {
e.printStackTrace();
} finally {
SQLConnection.Close(connect,ps);
try {
if (rs != null) {
rs.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return null;
}
@Test
public void ListQuery(){
List<firstmeTableClass> hyb = SQLQuery.ListQuery(firstmeTableClass.class, "select name_b,age from firstme where age> ? ", "hyb");
System.out.println(hyb);
// [firstmeTableClass{name_b='hyb', age=21, work='null', salary='null'}, firstmeTableClass{name_b='hfy', age=13, work='null', salary='null'}, firstmeTableClass{name_b='zyl', age=21, work='null', salary='null'}, firstmeTableClass{name_b='lzm', age=12, work='null', salary='null'}, firstmeTableClass{name_b='cjk', age=13, work='null', salary='null'}, firstmeTableClass{name_b='reNewHyb', age=21, work='null', salary='null'}]
}
3.10 好处
- 上面说到Statement会产生SQl注入的问题,而为什么PreparedStatement为什么不会呢?多次练习可见,使用后者会进行预编译,这就使得我们后者的语句里,就是架构,执行的时候会被缓存,进行多次执行,大大减少了代码量,只有到后面我们要运行的时候才会执行,而前者,一开始就说要干什么,会将本来不是数据的东西,就看成了数据,会造成混乱,而每执行一次,就要重写代码。
- 不仅如此,使用后者,我们可以处理Blob类型的数据,比如图片,视频等。
3.11 Blob类型
-
MySQL中,支持四种的Blob数据:
-
tinyblob:仅255个字符
-
blob:最大限制到65K字节
-
mediumblob:限制到16M字节
-
这里数据库默认是只能插入小于1m的图片,所以在编程之前我们要进行如下预先设置:
我们要在mysql安装的目录下,找到my.ini文件夹,打开然后将此此语句(max_allowed_packet=16M)插入最后。
-
-
longblob:可达4GB
-
-
无论是查找还是插入图片和视频,处理结果都一样,只不过要将它们转换成流交换而已。
/*向数据库插入图片,在插入图片之前,我们先在Sql添加一列,如下:
*
name_b age work salary photo
hyb 21 student 0 null
hfy 13 father 1000 null
zyl 21 student 0 null
lzm 12 student 0 null
cjk 13 film 10000 null
reNewHyb 21 student 0 null
* */
/*插入方式都一样,就是要把图片打散成流
* 在这之前,我们要在本工程目录下,放一张图片(myLove.jpg),大小超过1m,演示插入数据库的操作*/
@Test
public void InsertPhoto(){
Connection connect = null;
PreparedStatement ps = null;
try {
// 获取数据库连接
connect = SQLConnection.connect();
// 进行预编译
ps = connect.prepareStatement("update firstme set photo= ? where name_b=? ");
// 填充占位符,要使用到流
FileInputStream fis = new FileInputStream(new File("myLove.jpg"));
ps.setObject(1,fis);
ps.setObject(2,"zyl");
// 执行
boolean execute = ps.execute();
// zyl 21 student 0 一张图片
// System.out.println(execute);false
if(!execute){ System.out.println("执行成功!");}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (ps != null) {
ps.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
if (connect != null) {
connect.close();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
/*下面展示如何将图片从数据库查询图片,并放入文件夹里,这里,我们将放入的图片查询*/
@Test
public void PhotoQuery(){
Connection connect = null;
PreparedStatement ps = null;
ResultSet rs = null;
InputStream bs = null;
FileOutputStream fs = null;
try {
// 获取数据库连接
connect = SQLConnection.connect();
ps = connect.prepareStatement("select photo from firstme where name_b=? ");
ps.setObject(1,"zyl");
rs = ps.executeQuery();
if (rs.next()){
Blob photo = rs.getBlob("photo");
// 获得二进制流
bs = photo.getBinaryStream();
// 读入文件里,这步骤前面讲过
fs = new FileOutputStream("forMyLove.jpg");
byte[] bys = new byte[1024];
int len;
while ((len=bs.read(bys))!=-1){
fs.write(bys,0,len);
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
assert fs != null;
fs.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
bs.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
rs.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
ps.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
try {
connect.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
3.12 批量操作
- 有关批量操作,有两个点,一是批量缓存,二是手动提交。
- 若是不进行这两步,效率非常慢。
@Test
public void BathOperation(){
Connection connect = null;
PreparedStatement ps = null;
try {
//
long stTime = System.currentTimeMillis();
// 1.获取数据库连接
connect = SQLConnection.connect();
// 预编译,这里只是预编译,还没有执行,到了for循环里面,进行execute才可以执行,这个时候数据才被提交
ps = connect.prepareStatement("insert into firstme(name_b) values(?)");
// 为了增加速度,不允许连接自动提交数据,先将所有数据“攒”起来,
connect.setAutoCommit(false);
for (int i=0;i<100;i++){
ps.setObject(1,"name_"+i);
// 分批缓存
ps.addBatch();
if (i%500==0){
ps.execute();
ps.clearBatch();
}
}
// 全部缓存后,统一提交数据
connect.commit();
long endTime = System.currentTimeMillis();
System.out.println(endTime-stTime);//2442
/*我们可以看到,用的毫秒数是很少的,所以,我们要加快速度,有两点:
* 1.加大批量缓存数目。
* 2.手动提供数据。*/
} catch (Exception e) {
e.printStackTrace();
} finally {
SQLConnection.Close(connect,ps);
}
}
3.13 Statement vs preparedStatement
- 后者是前者的子接口
- 在Java中,sql语句还是个字符串,这两个接口提供一个信使功能去与数据库对接。
- Statement会产生SQL注入,造成混乱。
- 后者通过预编译解决了SQL注入问题。
- 后者更高效,快捷,因为预编译,可对SQL语句进行缓存,在进行大量批量删除的时候,可以手动提交数据,大大减少了编译时间。
第四章:数据库事务
4.1 引入
- 很经典的转账问题:A->B,A执行转出SQL语句,B执行转入SQL语句,若中间出现网络挂断,A的SQL语句执行了,B的没有执行,就会现丢失问题。
4.2 *事务 1.0
-
为了解决这些问题,数据库引入了事务概念,一组逻辑操作单元(增删改查),使得数据从一种状态变换为另一种状态,如果出现中断,可以进行回滚(rollback),否则提交,称为事务。
-
数据一旦提交,无法回滚。
-
哪些数据会自动提交?DDL操作一旦执行,都会自动提交(无法手动提交),DML默认情况下,一旦执行,也会自动提交(可以手动提交)。当然,关闭连接后,也会自动提交。
-
所以,我们要解决上面的问题,就要回滚,而要回滚,就要防止它出问题的时候自动提交。
-
但我们会想到,我们一般执行完一条不同的SQL语句后,都会关闭连接,这样不就是自动了提交了?
所以,在整件事务中,我们执行到一半操作,要避免关闭连接,到最后关闭才可以。这是本章的一个重点。
public static void rollbackTx(Connection cnt,String sql,Object ...arg){
PreparedStatement ps = null;
try {
// 2.进行SQl语句的预编译
ps = cnt.prepareStatement(sql);
// 3. 填充占位符
for (int i = 0; i < arg.length; i++) {
// 这里填充占位符,记住在我们sql语句里,索引是从1开始的,而数组是从0开始的
ps.setObject(i+1,arg[i]);
}
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
assert ps != null;
//这里一定不能关闭连接,因为一旦关闭,数据就自动提交了,要关闭的是ps,ps关闭不会自动连接
SQLConnection.Close(null,ps);
}
}
/*下面展示事务回滚的操作,如何在出现网络堵塞的情况,转账不成功*/
// 我们还是用下面这个表(firstme)进行查询:
// name_b age work salary
// hyb 21 student 0
// hfy 13 father 1000
// zyl 21 student 0
// lzm 12 student 0
// cjk 13 film 10000
// reNewHyb 21 student 0
// 模拟cjk朝hfy转账五千,出现网络堵塞,是否成功!
@Test
public void RollbackTx(){
Connection connect = null;
try {
// 为了代码简洁,我们用之前写过的万能方法,进行操作。
connect = SQLConnection.connect();
//数据先不提交
connect.setAutoCommit(false);
SQLUpdate.rollbackTx(connect ,"update firstme set salary=salary-? where name_b=?",5000,"cjk");
// 网络异常
// System.out.println(10/0);
SQLUpdate.rollbackTx(connect,"update firstme set salary=salary+? where name_b=?",5000,"hfy");
//执行一系列操作后再提交
connect.commit();
System.out.println("修改成功!");
// hfy 13 father 6000
// cjk 13 film 5000
} catch (Exception e) {
e.printStackTrace();
try {
if (connect != null) {
// 当出现异常,进行回滚数据
connect.rollback();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
} finally {
// 设置会默认值
assert connect != null;
try {
connect.setAutoCommit(true);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
SQLConnection.Close(connect,null);
}
}
- 有一点值得注意的是,我们要是进行手动提交,建议执行过后要调回true状态,因为可能下一个连接继续用false。
4.3 事务的ACID属性
- 原子性:原子性是指事务是一个不可分割的工作单位,要么可以执行,要么不执行。如经典的转账问题,要么转账,要么不转账,不可能出现一方转账,一方没收到账的问题。
- 一致性:事务必须从一个一致性状态装换为另一个一致性状态,如经典的转账问题,不可能出现一方只转账,没收到钱的状态,对于数据库来讲,这样的问题是不可能出现数据不一致状态。
- 隔离性:指一个事务不能被另一个事务干扰,如经典的转账问题,不能出现第三方进行干扰,只能是两个人之间的转账。
- 持久性:指一个事务一旦被提交,就不能回滚,如经典的转账问题,转出去的钱,不可能再收回来,只能通过对方进行转账,那又是另一个事务了。
4.4 数据库的并发问题
对于两个事务a,b同时操作一张表。
- 脏读:a已经更新了数据库,但还未提交,此时b进行了读取,之后a才提交,这样来说b读取的数据是无效的。
- 不可重复读:a读取了字段,b进行了更新,a再读取,读取的不再是原来的数据。
- 幻读:a读取了一个字段,b增加了行数,再次读取,发现多了几行数据。
- 但实际上,对于我们每天的数据更新,都要进行数据库更新,而且我们要时刻获取立马更新的数据,无论它事务有没有提交,我们都需要获取最新的数据,也就是说,解决上面的不可重复读和幻读问题,没有多大的实际意义(但有时候也要进行解决)。解决脏读问题,才是有意义的,因为脏读得到的是无效的数据。
4.5 *数据库的隔离级别
- 数据库默认隔离级别为4,可解决脏读问题,意义重大,不用重写设置级别。
- 但是在一些特殊情况下,我们需要解决不可重复读和幻读问题,在Connection接口里,可以通过源代码查看隔离级别设置的变量。
/*A constant indicating that transactions are not supported.*/
int TRANSACTION_NONE = 0;
/**
* A constant indicating that
* dirty reads, non-repeatable reads and phantom reads can occur.
* This level allows a row changed by one transaction to be read
* by another transaction before any changes in that row have been
* committed (a "dirty read"). If any of the changes are rolled back,
* the second transaction will have retrieved an invalid row.
*/
int TRANSACTION_READ_UNCOMMITTED = 1;
/**
* A constant indicating that
* dirty reads are prevented; non-repeatable reads and phantom
* reads can occur. This level only prohibits a transaction
* from reading a row with uncommitted changes in it.
*/
int TRANSACTION_READ_COMMITTED = 2;
/**
* A constant indicating that
* dirty reads and non-repeatable reads are prevented; phantom
* reads can occur. This level prohibits a transaction from
* reading a row with uncommitted changes in it, and it also
* prohibits the situation where one transaction reads a row,
* a second transaction alters the row, and the first transaction
* rereads the row, getting different values the second time
* (a "non-repeatable read").
*/
int TRANSACTION_REPEATABLE_READ = 4;
/**
* A constant indicating that
* dirty reads, non-repeatable reads and phantom reads are prevented.
* This level includes the prohibitions in
* <code>TRANSACTION_REPEATABLE_READ</code> and further prohibits the
* situation where one transaction reads all rows that satisfy
* a <code>WHERE</code> condition, a second transaction inserts a row that
* satisfies that <code>WHERE</code> condition, and the first transaction
* rereads for the same condition, retrieving the additional
* "phantom" row in the second read.
*/
int TRANSACTION_SERIALIZABLE = 8;
- 不懂数值的意思,可以查看解释。
- 下面给出代码演示。
package com.hyb.JDBC;
import org.junit.Test;
import java.sql.Connection;
import java.sql.Time;
import java.util.List;
/**
* @program: TransactionTest
* @description:进行隔离级别的测试,还是使用下面的数据表对hfy的工资进行操作
* 为了方便掩饰,我们直接throws异常
* hyb 21 student 0
* hfy 13 father 1000
* zyl 21 student 0
* lzm 12 student 0
* cjk 13 film 10000
* @author: Huang Yubin
* @create: 2021-06-23 22:19
**/
public class TransactionTest {
// 事务一:读取数据操作
@Test
public void rideTx() throws Exception{
Connection connect1 = SQLConnection.connect();
// 查看隔离级别
System.out.println(connect1.getTransactionIsolation());
// 设置隔离级别,数据库默认为4(TRANSACTION_REPEATABLE_READ),代表解决脏读问题,这里可忽略,要想验证隔离级别,请设置其他级别。
// ->设置了不能解决脏读问题的隔离级别之后,我们会发现,在事务二没有提交之前,事务一是不可能读取成功,即读取的还是原来的。
// 如果我们将隔离级别换成不能读取脏读的(可以设置成0),那么会发现当事务二更新还未提交,事务一这边还是可以查询成功的,即查询到事务二更新但还未提交的值
connect1.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
// 因为执行insert语句还是会自动提交,所以我们得设置为不能自动提交
connect1.setAutoCommit(false);
List<firstmeTableClass> hfy = SQLQuery.ListTxQuery(connect1, firstmeTableClass.class, "select * from firstme where name_b=? ", "hfy");
System.out.println(hfy);
// 因为要演示,不能进行提交操作
}
// 事务二:写入数据操作
@Test
public void writeTx() throws Exception{
Connection connect2 = SQLConnection.connect();
connect2.setAutoCommit(false);
SQLUpdate.rollbackTx(connect2,"update firstme set salary=salary+? where name_b=?",1000,"hfy");
Thread.sleep(15000);
// 这里我们不进行提交,也即是说数据库里的表不会进行更新操作。
}
}
第五章:DAO
-
虽然开发当中我们可以这样操作数据库,但是仍然比较麻烦,所以,DAO是我们专门用来操作数据库的,是对以上方法的封装模式
-
DAO模式有几个关键点:
- 提供操作数据表的抽象类A,该类的方法只能被子类或本类使用,不能被外部使用。
- 提供一个类B存储要操作的数据库表的数据,具体对象为数据表的每一行。
- 提供一个接口C,规范抽象类A里的具体方法。
- 提供一个实现类D继承于抽象类A,并实现接口C。
-
第一步:抽象类A里有操作数据表的一般方法。
package com.hyb.JDBCDAO; import com.hyb.JDBC.SQLConnection; import com.hyb.JDBC.SQLTest; import com.hyb.JDBC.SQLUpdate; import org.junit.Test; import java.io.*; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Properties; /** * @program: OPTable * @description:提供操作数据库的基本方法,为了只能让其子类调用其方法,所以要将其类定义为抽象类,方法也不能是静态的。 * @author: Huang Yubin * @create: 2021-06-24 13:07 **/ public abstract class OPTable { // 1.获取数据库连接 public Connection Connect(){ Connection connection = null; try { InputStream o = SQLTest.class.getClassLoader().getResourceAsStream("myTest.properties"); Properties p = new Properties(); p.load(o); String user = p.getProperty("user"); String password = p.getProperty("password"); String url = p.getProperty("url"); String driverClass = p.getProperty("driverClass"); Class.forName(driverClass); connection = DriverManager.getConnection(url, user, password); } catch (IOException | SQLException | ClassNotFoundException e) { e.printStackTrace(); } return connection; } // 2.关闭操作,这里我们将原来的稍作更改,因为我们处理结果集和文件流还是要关闭资源的 public void Close(Connection cnt, PreparedStatement ps, ResultSet ...arg) { try { if (ps!=null) { ps.close(); } } catch (SQLException throwables) { throwables.printStackTrace(); } try { if (cnt!=null){ cnt.close(); } } catch (SQLException throwables) { throwables.printStackTrace(); } for (ResultSet resultSet : arg) { try { resultSet.close(); } catch (SQLException throwables) { throwables.printStackTrace(); } } } // 3.更新操作,包括普通插入,和视频图片等插入,更新数据。 public void Update(Connection cnt,String sql,Object ...arg){ PreparedStatement ps = null; try { // 1.获取数据库连接 // 2.进行SQl语句的预编译 ps = cnt.prepareStatement(sql); // 3. 填充占位符 for (int i = 0; i < arg.length; i++) { // 这里填充占位符,记住在我们sql语句里,索引是从1开始的,而数组是从0开始的 ps.setObject(i+1,arg[i]); } ps.execute(); System.out.println("修改成功!"); } catch (Exception e) { e.printStackTrace(); } finally { assert ps != null; Close(null,ps); } } // 多行查询 public <T> List<T> ListQuery(Connection cnt,Class<T> className, String sql, Object ...arg ){ PreparedStatement ps = null; ResultSet rs = null; try { ps = cnt.prepareStatement(sql); for (int i = 0; i < arg.length; i++) { ps.setObject(i+1,arg[i]); } rs = ps.executeQuery(); ArrayList<T> obj = new ArrayList<>(); ResultSetMetaData mt = rs.getMetaData(); int columnCount = mt.getColumnCount(); while (rs.next()){ T t = className.newInstance(); for (int i = 0; i < columnCount; i++) { String cl = mt.getColumnLabel(i+1); Field dfd = className.getDeclaredField(cl); dfd.setAccessible(true); Object o = rs.getObject(i + 1); dfd.set(t,o); } obj.add(t); } return obj; } catch (Exception e) { e.printStackTrace(); } finally { SQLConnection.Close(null,ps); try { if (rs != null) { rs.close(); } } catch (SQLException throwables) { throwables.printStackTrace(); } } return null; } }
-
第二步:当我们要查询数据表的数据时,需要将数据放入一个类中,提供这个类B。
package com.hyb.JDBCDAO; /** * @program: SQLTable * @description:这里以一个类去存储要操作的数据库表。 * @author: Huang Yubin * @create: 2021-06-24 13:04 **/ public class SQLTable { private String name; private int age; private String job; private String salary; public SQLTable() { } public SQLTable(String name, int age, String job, String salary) { this.name = name; this.age = age; this.job = job; this.salary = salary; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getJob() { return job; } public void setJob(String job) { this.job = job; } public String getSalary() { return salary; } public void setSalary(String salary) { this.salary = salary; } @Override public String toString() { return "SQLTable{" + "name='" + name + '\'' + ", age=" + age + ", job='" + job + '\'' + ", salary='" + salary + '\'' + '}'; } }
-
提供接口C,规范类A里的操作。
package com.hyb.JDBCDAO; import java.sql.Connection; import java.sql.PreparedStatement; import java.util.List; /** * @Description: 此接口用于规范类OPTable的操作 * @Param: * @return: * @Author: Huang Yubin * @Date: 2021/6/24 */ public interface OPInterface{ Connection getConnect(); void setClose(Connection cnt, PreparedStatement ps); void insert(Connection cnt); void update(Connection cnt,SQLTable table); List<SQLTable> ListQuery(Connection cnt); }
-
提供类D,继承类A,实现接口C
package com.hyb.JDBCDAO; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; /** * @program: OPClass * @description:提供实现类,继承SQLTable,实现OPInterface接口,写入sql语句,表示要干什么 * @author: Huang Yubin * @create: 2021-06-24 17:14 **/ public class OPClass extends OPTable implements OPInterface{ @Override public Connection getConnect() { return Connect(); } @Override public void setClose(Connection cnt, PreparedStatement ps) { Close(cnt,ps); } @Override public void insert(Connection cnt) { Update(cnt,"insert into firstme(name_b,age,work,salary) values(?,?,?,?)","RenewHyb",21,"student",0); } @Override public void update(Connection cnt, SQLTable table) { Update(cnt,"update firstme set name_b=? where name_b=? ","newHyb","hyb"); } @Override public List<SQLTable> ListQuery(Connection cnt) { return ListQuery(cnt, SQLTable.class, "select name_b name,age,work job,salary from firstme where name_b=? ", "hyb"); } }
-
第五步:进行测试
以本章统一数据表测试。
package com.hyb.JDBCDAO; import org.junit.Test; import java.sql.Connection; import java.sql.SQLException; import java.util.List; /** * @program: SQLTest * @description: * @author: Huang Yubin * @create: 2021-06-24 16:22 **/ public class SQLTest { @Test public void testInsert(){ OPClass op = new OPClass(); Connection connect = op.getConnect(); op.insert(connect); op.setClose(connect,null); } @Test public void testQuery(){ OPClass op = new OPClass(); Connection connect = op.getConnect(); List<SQLTable> sqlTables = op.ListQuery(connect); op.setClose(connect,null); System.out.println(sqlTables); } }
-
说明:在开发未使用框架的情况下,一般使用DAO模型建设操作数据库,以抽象类为主类,再建立要操作数据表类和规范接口,这就形成了操作数据库的DAO模式,在这种模式下,不容易混乱,思路清晰,是我们在未使用框架下的一种高效开发手段。
-
当然,这DAO模式还有一个小的升级版,我们进行DAO模式,是确立了要操作哪一张表,并且为这张表建立一个保存其数据的类,所以我们在下面的代码中:
@Override public List<SQLTable> ListQuery(Connection cnt) { return ListQuery(cnt, SQLTable.class, "select name_b name,age,work job,salary from firstme where name_b=? ", "hyb"); }
可以看出,我们在查询的时候,传入了SQLTable.class,这是合理的,因为我们要操作的就是这个类。但就是因为我们明确了要操作这个类,是否可以不用传入SQLTable.class呢?答案是可以,但这是一个比较困难的思路,要用反射。
-
第一:在抽象类中,指明为泛型,可以操作任何类。
public abstract class OPTable<T>
-
第二:在子类继承上,说明继承了SQLTable类型(SQLTable是保存要操作的数据表的数据的类)的父类。
public class OPClass extends OPTable<SQLTable> implements OPInterface
-
第三:既然说明了继承了哪个类型的父类,在本类OPClass中,类形参要删除。
ListQuery(cnt, SQLTable.class, "select name_b name,age,work job,salary from firstme where name_b=?
删除
SQLTable.class,
。既然不用传入SQLTable.class了,在
public <T> List<T> ListQuery(Connection cnt,Class<T> className, String sql, Object ...arg
)里,我们就不用传入Class了,但如果不传入Class className,这函数里的T t = className.newInstance();就不能使用了(我们必须用到这条语句,不能忽略,这是它后面反射代码的关键),所以我们要解决的第一个问题就是既要保证它能被使用,又能删除泛型类的传入。【在这里泛型方法已经没有用了,所以我们也要删除public后的】 -
第四:
我们再具体描述一下第三步最后的问题:
T t = className.newInstance();这条语句是在抽象类OPTable中的,在没删除
ListQuery(cnt, SQLTable.class, "select name_b name,age,work job,salary from firstme where name_b=?
中SQLTable.class,
之前,t是被SQLTable.class
赋值的,而现在删除了
SQLTable.class
,说明t没有被赋值了,但是我们也得让t拥有这个值。在此之前,我们说过,我们在OPClass类中,已经声明了我们要继承的就是SQLTable类型的父类,即抽象类的OPTable的泛型就是SQLTable,所以我们要t也拥有SQLTable.calss,只能通过反射获取子类中的声明好的泛型父类OPTable中的泛型SQLTable。在这之前,因为要用在泛型父类OPTable中用到className,所以我们得先声明为null。
public abstract class OPTable<T> { private Class<T> className=null; ...
那么我们可以在本类下通过反射获取泛型子类中声明好的父类泛型
public class OPClass extends OPTable<SQLTable> implements OPInterface//获取SQLTable
public abstract class OPTable<T> { private Class<T> className=null; // public OPTable() { // } { // 获取子类中父类的泛型 Type genericSuperclass = this.getClass().getGenericSuperclass(); ParameterizedType parameterizedType= (ParameterizedType) genericSuperclass; // 得到泛型数组,因为只有一个类型,所以这里是0 Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); className= (Class<T>) actualTypeArguments[0]; }
注意:上面代码里的this指的是子类的this,因为只有在子类中才能获取父类的泛型,如果这个this指代的父类的this,不合理,首先,这个父类是抽象的,不可能被实例化,其次,当要执行这个代码块的时候,只有子类调用ListQuery才会执行,也就是说每次要用到静态代码块,只是子类被实例化了一次,所以这个this只能指代子类。
但有人会问,既然指代子类,为什么不在子类写这个静态代码块呢?如果在子类写入静态代码块,那么假如有很多子类的时候,都要写一次,造成代码冗杂。
又有会问,不考虑冗杂问题,如果真放在子类中,能运行吗?毕竟父类声名的className是个null,而父类的方法里就有用className这个变量了。当然能运行,前面说过,通过子类才能调用父类需要变量className的方法,所以当子类一执行,静态代码块执行,产生className,这个时候调用父类方法,className自然有值啦!
-
第六章:数据库连接池
在使用开发基于数据库的 web 程序时,传统的模式基本是按以下步骤:
-
在主程序(如 servlet、beans、DAO)中建立数据库连接。
-
进行 sql 操作
-
断开数据库连接。
这种模式开发,存在的问题:
-
普通的 JDBC 数据库连接使用 DriverManager 来获取,每次向数据库建立连接的时候都要将 Connection 加载到
内存中,再验证 IP 地址,用户名和密码(得花费 0.05s~1s 的时间)。需要数据库连接的时候,就向数据库要求
一个,执行完成后再断开连接。这样的方式将会消耗大量的资源和时间。数据库的连接资源并没有得到很好的
重复利用.若同时有几百人甚至几千人在线,频繁的进行数据库连接操作将占用很多的系统资源,严重的甚至会
造成服务器的崩溃。
-
对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的 内存泄漏,最终将导致重启数据库。
-
这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄 漏,服务器崩溃。
数据库连接池的基本思想:
-
为解决传统开发中的数据库连接问题,可以采用数据库连接池技术(connection pool)。
数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需
要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。数据库连接池负责分配、管理和释
放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个。
数据库连接池在初始化时将创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库
连接数来设定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最
大数据库连接数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量
时,这些请求将被加入到等待队列中。
数据库连接池技术的优点:
-
资源重用:
由于数据库连接得以重用,避免了频繁创建,释放连接引起的大量性能开销。在减少系统消耗的基础上,
另一方面也增加了系统运行环境的平稳性。
-
更快的系统反应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工
作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时
间开销,从而减少了系统的响应时间
-
新的资源分配手段
对于多应用共享同一数据库的系统而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数
据库连接数的限制,避免某一应用独占所有的数据库资源
-
统一的连接管理,避免数据库连接泄露
在较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规
数据库连接操作中可能出现的资源泄露
两种开源的数据库连接池
-
JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口,该接口通常由服务器(Weblogic,
WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
-
DBCP 数据库连接池
-
C3P0 数据库连接池
-
-
DataSource 通常被称为数据源,它包含连接池和连接池管理两个部分,习惯上也经常把 DataSource 称为连接池
DBCP 数据源
-
DBCP 是 Apache 软件基金组织下的开源连接池实现,该连接池依赖该组织下的另一个开源系统:Common-pool. 如
需使用该连接池实现,应在系统中增加如下两个 jar 文件:
-
Commons-dbcp.jar:连接池的实现
-
Commons-pool.jar:连接池实现的依赖库
-
-
Tomcat 的连接池正是采用该连接池来实现的。该数据库连接池既可以与应用服务器整合使用,也可由应用程序独
立使用。
-
注意:
-
数据源和数据库连接不同,数据源无需创建多个,它是产生数据库连接的工厂,因此整个应用只需要一个数据
源即可。
-
当数据库访问结束后,程序还是像以前一样关闭数据库连接:conn.close(); 但 conn.close()并没有关闭数据库的
物理连接,它仅仅把数据库连接释放,归还给了数据库连接池。
-
6.1 连接池
功能 | *dbcp | *druid | *c3p0 | tomcat-jdbc | HikariCP |
---|---|---|---|---|---|
是否支持PSCache | 是 | 是 | 是 | 否 | 否 |
监控 | jmx | jmx/log/http | jmx,log | jmx | jmx |
扩展性 | 弱 | 好 | 弱 | 弱 | 弱 |
代码 | 简单 | 中等 | 复杂 | 简单 | 简单 |
sql拦截及解析 | 无 | 支持 | 无 | 无 | 无 |
更新时间 | 2015.8.6 | 2015.10.10 | 2015.12.09 | 2015.12.3 | |
特点 | 依赖于common-pool | 阿里开源,功能全面 | 历史久远,代码逻辑复杂,且不易维护 | 底层 | 优化力度大,功能简单,起源于boneCP |
连接池管理 | LinkedBlockingDeque | 数组 | FairBlockingQueue | threadlocal+CopyOnWriteArrayLis |
6.2 c3p0
-
下面演示c3p0连接池获取连接的步骤。
-
第一步:下载第三方jar包,c3p0-0.9.5.5.jar,mchange-commons-java-0.2.20.jar,下载网址:
http://mvnrepository.com/artifact/com.mchange/mchange-commons-java/0.2.15(进去,找到Jdbcpool,然后下载上面两个,可以下载不同版本)
-
第二步:两个jar都要导入EXternal Libraries下,上网查阅以前的视频,好像只要第一个jar就可以了,但是本人尝试过不行,可能是新版的原因,两个都导入后方可成功,导包过程若不懂请上网查阅。
-
第三步:请看代码。
// 方式一,获取数据库连接池,不建议这么写代码 @Test public void testConnection() throws Exception { ComboPooledDataSource cpld = new ComboPooledDataSource(); // 对比 // 获取jar包下的驱动类,要是没有jar包,请上网查找如何导入,应用 // import com.mysql.cj.jdbc.Driver; // Driver driver = new Driver(); cpld.setDriverClass("com.mysql.cj.jdbc.Driver"); 获取你数据库的URL,jdbc:mysql:协议,localhost ip地址,3306 端口号,你的想要操作数据库的名称 // String url="jdbc:mysql://localhost:3306/mysqldata"; cpld.setJdbcUrl("jdbc:mysql://localhost:3306/mysqldata"); cpld.setUser("root"); cpld.setPassword("15717747056HYB"); // 获取初始时连接池中的连接个数 cpld.setInitialPoolSize(10); // 获取连接 Connection connection = cpld.getConnection(); System.out.println(connection); // com.mchange.v2.c3p0.impl.NewProxyConnection@548e7350 [wrapping: com.mysql.cj.jdbc.ConnectionImpl@1a968a59] // 销毁池子,不建议这么做 DataSources.destroy(cpld); }
// 方式二:编辑为一个xml文件 private static ComboPooledDataSource connect = new ComboPooledDataSource("myConnection"); // 上面的创建池子语句最好放在外面,因为加入我们每次都调用下面获取连接的方法,就会出现造了很多池子。 // 而且这个池子建议是私有的和静态的,因为这会封装在一个类里,如同上面我们讲DAO一样 @Test public void testXmlConnection() throws SQLException { Connection connection = connect.getConnection(); System.out.println(connection); // com.mchange.v2.c3p0.impl.NewProxyConnection@589838eb [wrapping: com.mysql.cj.jdbc.ConnectionImpl@5cc484b9] }
<?xml version="1.0" encoding="UTF-8" ?> <c3p0-config> <!-- 设置配置名称,待会获取连接要用--> <named-config name="myConnection"> <!-- 获取连接四个基本信息--> <!-- cpld.setDriverClass("com.mysql.cj.jdbc.Driver");--> <!-- cpld.setJdbcUrl("jdbc:mysql://localhost:3306/mysqldata");--> <!-- cpld.setUser("root");--> <!-- cpld.setPassword("15717747056HYB");--> <property name="driverClass"> com.mysql.cj.jdbc.Driver </property> <property name="jdbcUrl"> jdbc:mysql://localhost:3306/mysqldata </property> <property name="user"> root </property> <property name="password"> 15717747056HYB </property> <!-- 进行数据库管理的基本信息--> <!-- 当数据库连接池连接不够时,c3p0一次性向数据库服务器申请的连接数--> <property name="acquireIncrement"> 5 </property> <!-- 初始化连接数--> <property name="initialPoolSize"> 10 </property> <!-- 数据库连接池维护的最少连接数--> <property name="minPoolSize"> 10 </property> <!-- 数据库连接池维护最多的连接数--> <property name="maxPoolSize"> 100 </property> <!-- 数据连接池维护最多的Statement个数,sql语句--> <property name="maxStatements"> 50 </property> <!-- 每个连接中维护最多的Statement个数--> <property name="minStatementsPerConnection"> 2 </property> </named-config> </c3p0-config>
注意:
- IDEA中,若不可以直接创建XML文件,上网查阅方式便可。
6.3 dbcp
-
如c3p0一样也必须导入两个jar包,commons-dbcp-1.4.jar,commons-pool-1.6.jar。
-
导入成功后,这里要创建的是properties文件
driverClass=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/mysqldata username=root password=15717747056HYB
信息前面的名字必须要和我一致,因为它提供的方法便是这个名字。
-
@Test public void connection() throws Exception{ Properties ps = new Properties(); FileInputStream fis = new FileInputStream("dbcp.properties"); ps.load(fis); // 创建一个连接池 DataSource ds = BasicDataSourceFactory.createDataSource(ps); // 获取一个连接 Connection connection = ds.getConnection(); System.out.println(connection); // jdbc:mysql://localhost:3306/mysqldata, UserName=root@localhost, MySQL Connector/J }
-
升级版和c3p0思想一样,只不过这里涉及到一些代码规范问题,所以要使用静态代码块
// 方式一和c3p0的一个样,下面只讲方式二,如c3p0相似,只不过dbcp建立的是properties文件夹 private static DataSource ds = null; static { try { Properties ps = new Properties(); FileInputStream fis = new FileInputStream("dbcp.properties"); ps.load(fis); ds=BasicDataSourceFactory.createDataSource(ps); } catch (Exception e) { e.printStackTrace(); } } @Test public void connection() throws Exception{ // 获取一个连接 Connection connection = ds.getConnection(); System.out.println(connection); // jdbc:mysql://localhost:3306/mysqldata, UserName=root@localhost, MySQL Connector/J }
6.4 *Druid
-
下面给出Durid常用
配置 缺省值 说明 name 配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:“DataSource-” + System.identityHashCode(this) url 连接数据库的url,不同数据库不一样。例如: jdbc:mysql://localhost:3306/mysqldata username 连接数据库的用户名 password 连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter driverClassName 根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下) initialSize 0 初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时 maxActive 8 最大连接池数量 maxIdle 8 已经不再使用,配置了也没效果 minIdle 最小连接池数量 maxWait 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。 poolPreparedStatements false 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。 maxOpenPreparedStatements -1 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100 validationQuery 用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。 testOnBorrow true 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 testOnReturn false 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能 testWhileIdle false 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 timeBetweenEvictionRunsMillis 有两个含义: 1) Destroy线程会检测连接的间隔时间2) testWhileIdle的判断依据,详细看testWhileIdle属性的说明 numTestsPerEvictionRun 不再使用,一个DruidDataSource只支持一个EvictionRun connectionInitSqls 物理连接初始化的时候执行的sql exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接 filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall proxyFilters 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系 -
非常重要,阿里巴巴开源的数据库连接池,但是套路和其他都一样。
package com.hyb.SQLPool; import com.alibaba.druid.pool.DruidDataSourceFactory; import org.junit.Test; import javax.sql.DataSource; import java.io.FileInputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; /** * @program: DruidTest * @description: * @author: Huang Yubin * @create: 2021-06-27 19:57 **/ public class DruidTest { private static DataSource source=null; static { try { Properties ps = new Properties(); FileInputStream fs=new FileInputStream("Druid.properties"); ps.load(fs); source= DruidDataSourceFactory.createDataSource(ps); } catch (Exception e) { e.printStackTrace(); } } @Test public void Druid() throws SQLException { Connection connection = source.getConnection(); System.out.println(connection); // com.mysql.cj.jdbc.ConnectionImpl@35fb3008 } }
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/mysqldata username=root password=15717747056HYB
第七章:Apache-DBUtils 实现CRUD
- 这只是别人对增删改查的封装,源码基本和前面所写的一样,由于是第三方的,所以我们还是得导入第三方jar包:commons-dbutils-1.7.jar
- DBUtils有三大核心。
DbUtils类
-
提供如关闭连接、装载 JDBC 驱动程序等常规工作的工具类,里面的所有方法都是静态的。主要方法如下:
- public static void close(…) throws java.sql.SQLException: DbUtils 类提供了三个重载的关闭方法。这些方法检查
所提供的参数是不是 NULL,如果不是的话,它们就关闭 Connection、Statement 和 ResultSet。
- public static void closeQuietly(…): 这一类方法不仅能在 Connection、Statement 和 ResultSet 为 NULL 情况下避免
关闭,还能隐藏一些在程序中抛出的 SQLEeception。
-
public static void commitAndClose(Connection conn)throws SQLException 用来提交连接的事务,然后关闭连接
-
public static void commitAndCloseQuietly(Connection conn): 用来提交连接的事务,然后关闭连接,并且在关闭
连接时不抛出 SQL 异常。
-
public static void rollback(Connection conn)throws SQLException 允许 conn 为 null,因为方法内部做了判断
-
public static void rollbackAndClose(Connection conn)throws SQLException
-
rollbackAndCloseQuietly(Connection)
-
public static boolean loadDriver(java.lang.String driverClassName):这一方装载并注册 JDBC 驱动程序,如果成功就
返回 true。使用该方法,你不需要捕捉这个异常 ClassNotFoundException。/2QueryRunner 类
该类封装了 SQL 的执行,是线程安全的。
(1)可以实现增、删、改、查、批处理、
(2)考虑了事务处理需要共用 Connection。
(3)该类最主要的就是简单化了 SQL 查询,它与 ResultSetHandler 组合在一起使用可以完成大部分的数据库操作,
能够大大减少编码量。
ResultSetHandler接口
- 用于处理结果集。
BeanHandler | 把结果集的第一条记录转为创建 BeanHandler 对象时传入的 Class 参数对应的对象. |
---|---|
BeanListHandler | 将结果集中每一条记录封装到指定的javaBean中,将这些javaBean在封装到List集合中,也传入一个Class类对象。 |
ColumnListHandler | 将结果集中指定的列的字段值,封装到一个List集合中 |
ArrayHandler | 将结果集中的第一条记录封装到一个Object[]数组中,数组中的每一个元素就是这条记录中的每一个字段的值 |
ArrayListHandler | 将结果集中的每一条记录都封装到一个Object[]数组中,将这些数组在封装到List集合中。 |
MapHandler | 将结果集第一行封装到Map集合中,Key 列名, Value 该列数据 |
MapListHandler | 将结果集第一行封装到Map集合中,Key 列名, Value 该列数据,Map集合存储到List集合 |
ScalarHandler | 它是用于单数据。例如select count(*) from 表操作。 |
QueryRunner实现类
-
QueryRunner 类提供了两个构造方法:
-
QueryRunner():默认的构造方法
-
QueryRunner(DataSource ds):需要一个 javax.sql.DataSource 来作参数的构造方法。
-
利用该类实现增删改查的操作
-
更新:
-
public int update(Connection conn, String sql, Object… params) throws SQLException:用来执行一个更新(插入、更新或 删除)操作。
-
public int update(Connection conn, String sql) throws SQLException:用来执行一个不需要置换参数的更新操作。
-
-
插入:
-
**public T insert(Connection conn,String sql,ResultSetHandler rsh)**throws SQLException:其中 rsh - The handler used to create the result object from the ResultSet of auto-generated keys. 返回值: An object generated by the handler.即自 动生成的键值
-
public T insert(Connection conn,String sql,ResultSetHandler rsh, Object… params) throws SQLException:只支持 INSERT
-
public T **insert(String sql,ResultSetHandler rsh)**throws SQLException:只支持 INSERT
-
public **T insert(String sql,ResultSetHandler rsh,Object… params)**throws SQLException:只支持 INSERT
-
-
批处理:
-
public int[] **batch(Connection conn,String sql,Object[][] params)**throws SQLException: INSERT, UPDATE, or DELETE 语句
-
public int[] **batch(String sql,Object[][] params)**throws SQLException: INSERT, UPDATE, or DELETE 语句
-
public T **insertBatch(Connection conn,String sql,ResultSetHandler rsh,Object[][] params)**throws SQLException:只 支持 INSERT
-
public T **insertBatch(String sql,ResultSetHandler rsh,Object[][] params)**throws SQLException:只支持 INSERT
-
-
查询:
-
public Object query(Connection conn, String sql, ResultSetHandler rsh,Object… params) throws SQLException:执行一 个查询操作,在这个查询中,对象数组中的每个元素值被用来作为查询语句的置换参数。该方法会自行处理PreparedStatement 和 ResultSet 的创建和关闭。
-
public Object query(String sql, ResultSetHandler rsh, Object… params) throws SQLException: 几乎与第一种方法一 样;唯一的不同在于它不将数据库连接提供给方法,并且它是从提供给构造方法的数据源(DataSource) 或使用 的 setDataSource 方法中重新获得 Connection。
-
public Object query(Connection conn, String sql, ResultSetHandler rsh) throws SQLException : 执行一个不需要置换 参数的查询操作。
-
public Object query( String sql, ResultSetHandler rsh) throws SQLException : 执行一个不需要置换参数的查询操作。
-
-
代码示例
package com.hyb.JDBC;
import com.hyb.SQLPool.Druid;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.ResultSetHandler;
import org.junit.Test;
import java.sql.Connection;
import java.sql.SQLException;
/**
* @program: QueryRunnarTest
* @description:
* @author: Huang Yubin
* @create: 2021-06-27 21:00
**/
public class QueryRunnerTest {
// 更新
@Test
public void testInsert(){
Connection connect = null;
try {
// QueryRunner 为查询类
QueryRunner runner = new QueryRunner();
// 我们利用连接池来获取连接
connect = Druid.getConnection();
int update = runner.update(connect, "update firstme set name_b=? where name_b = ?", "newHyb", "hyb");
// newHyb 21 student 0
// 获得更新次数
System.out.println(update);
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
try {
assert connect!=null;
connect.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
// 查询
@Test
public void queryTest(){
Connection cn = null;
try {
QueryRunner qr = new QueryRunner();
cn = Druid.getConnection();
// 1.返回一条记录 BeanHandler将结果集中第一条记录封装到一个指定的javaBean中。
// BeanHandler<firstmeTableClass> fs = new BeanHandler<firstmeTableClass>(firstmeTableClass.class);
//
// firstmeTableClass query = qr.query(cn, "select * from firstme where name_b=?", fs,"newHyb");
//
// 2.BeanListHandler将结果集中每一条记录封装到指定的javaBean中,将这些javaBean在封装到List集合中,也传入一个Class类对象。
// BeanListHandler<firstmeTableClass> fs = new BeanListHandler<>(firstmeTableClass.class);
//
// List<firstmeTableClass> query = qr.query(cn, "select * from firstme", fs);
//
// 3.**ColumnListHandler****将结果集中指定的列的字段值,封装到一个List集合中**
// ColumnListHandler<Object> fs = new ColumnListHandler<>();
// List<Object> query = qr.query(cn, "select name_b from firstme", fs);
// 4.**ArrayHandler****将结果集中的第一条记录封装到一个Object[]数组中,数组中的每一个元素就是这条记录中的每一个字段的值**
// ArrayHandler fs = new ArrayHandler();
// Object[] query = qr.query(cn, "select name_b,age from firstme", fs);
// for (Object o: query){
// System.out.println(o);
// }
// 5.**ArrayListHandler****将结果集中的每一条记录都封装到一个Object[]数组中,将这些数组在封装到List集合中。**
// ArrayListHandler fs = new ArrayListHandler();
// List<Object[]> query = qr.query(cn, "select * from firstme", fs);
// for (Object[] o:query){//集合中的每个元素都是Object[]类型
// for (Object p:o){//Object[]中的每个元素都是Object类型
// System.out.println(p);
// }
// }
// 6.**MapHandler****将结果集第一行封装到Map集合中,Key 列名, Value 该列数据**
// MapHandler fs = new MapHandler();
// Map<String, Object> query = qr.query(cn, "select * from firstme", fs);
// 7.**MapListHandler****将结果集第一行封装到Map集合中,Key 列名, Value 该列数据,Map集合存储到List集合**
// MapListHandler fs = new MapListHandler();
// List<Map<String, Object>> query = qr.query(cn, "select * from firstme", fs);
// for (Map<String,Object> o:query){
// System.out.println(o);
// }
// 8.**ScalarHandler****它是用于单数据。例如select count(*) from 表操作。**
// ScalarHandler<Long> fs = new ScalarHandler<>();
// Long query = qr.query(cn, "select count(*) from firstme", fs);
// System.out.println(query);
// 9.自定义实现类
ResultSetHandler<firstmeTableClass> fs=(resultSet)-> {
if (resultSet.next()){
String name_b = resultSet.getString("name_b");
int age = resultSet.getInt("age");
String work = resultSet.getString("work");
String salary = resultSet.getString("salary");
return new firstmeTableClass("name_b",age,"wrok","salary");
}
return null;
};
firstmeTableClass query = qr.query(cn, "select * from firstme", fs);
System.out.println(query);
} catch (SQLException throwables) {
throwables.printStackTrace();
} finally {
assert cn!=null;
try {
DbUtils.close(cn);
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
}