Java全栈(三)数据库技术:3.数据库之JDBC上

第一章 JDBC概述

之前我们学习了JavaSE,编写了Java程序,数据保存在变量、数组、集合等中,无法持久化,后来学习了IO流可以将数据写入文件,但不方便管理数据以及维护数据的关系;

后来我们学习了数据库管理软件MySQL,可以方便的管理数据。

那么如何将它俩结合起来呢?即Java程序<==>MySQL,实现数据的存储和处理。

那么就可以使用JDBC技术。

1.1 JDBC概述

JDBC:Java Database Connectivity,它是代表一组独立于任何数据库管理系统(DBMS)的API,声明在java.sql与javax.sql包中,是SUN(现在Oracle)提供的一组接口规范。由各个数据库厂商来提供实现类,这些实现类的集合构成了数据库驱动jar。

在这里插入图片描述

即JDBC技术包含两个部分:

(1)java.sql包和javax.sql包中的API

因为为了项目代码的可移植性,可维护性,SUN公司从最初就制定了Java程序连接各种数据库的统一接口规范。这样的话,不管是连接哪一种DBMS软件,Java代码可以保持一致性。

(2)各个数据库厂商提供的jar

因为各个数据库厂商的DBMS软件各有不同,那么内部如何通过sql实现增、删、改、查等管理数据,只有这个数据库厂商自己更清楚,因此把接口规范的实现交给各个数据库厂商自己实现。

1.2 JDBC使用步骤

代码编写步骤:

JDBC访问数据库步骤

1:注册一个Driver驱动
```
三部曲:

​    (1)将DBMS数据库管理软件的驱动jar拷贝到项目的libs目录中

​              例如:mysql-connector-java-5.1.36-bin.jar

​     (2)把驱动jar添加到项目的build path中。(IDEA中右键点击驱动jar包》点击add as library)

​     (3)将驱动类(DriverManager)加载到内存中,DriverManager可以用于创建命令发送器。来执行sql语句
               DriverManager.registerDriver(new Driver());  # 方式1
               Class.forName("com.mysql.cj.jdbc.Driver"); # 方式2。不同版本的mysql驱动包Driver类路径可能不同
```

两种加载驱动类DriverManager的方式:

  • DriverManager.registerDriver(new Driver());:注册一个Driver实例
  • Class.forName("com.mysql.cj.jdbc.Driver"):初始化Driver类。
    package com.mysql.cj.jdbc;
    
    import java.sql.DriverManager;
    import java.sql.SQLException;
    
    public class Driver extends NonRegisteringDriver implements java.sql.Driver {
        public Driver() throws SQLException {
        }
    	// 本质上是初始化Driver类时,会执行该静态代码块,完成Driver实例对象的创建。跟方式1一样
        static {
            try {
                DriverManager.registerDriver(new Driver());
            } catch (SQLException var1) {
                throw new RuntimeException("Can't register driver!");
            }
        }
    }
    

2:创建数据库连接(Connection)

Connection conn = DriverManager.getConnection(url,username,password);// mysql的url:jdbc:mysql://ip:端口/数据库名?参数名=参数值// 例如:jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=utf8

​ (如果JDBC程序与服务器端的字符集不一致,会导致乱码,那么可以通过参数指定服务器端的字符集)

3:创建SQL命令发送器Statement

// 创建Statement或PreparedStatement对象
// 创建statement对象
Connection connection = DriverManager.getConnection(url, "root", "xxx");
Statement statement = connection.createStatement();

4:通过Statement发送SQL命令并得到结果

// 注意:sql语句中的字符串只能用单引号括起来,因为外层是java中的字符串必须使用双引号,不能冲突
// executeUpdate用于提交增删改操作的sql,返回int数据。大于0表示执行成功
int res = statement.executeUpdate("insert into employ3 values(41,'西门庆','男',14000.22,1)");
// executeQuery用于执行查询操作的sql,返回ResultSet 对象。其中储存了sql查询的结果
ResultSet resultSet = statement.executeQuery("select * from employ3");

5:处理结果(select语句需要)
6:关闭数据库资源ResultSet Statement Connection

statement.close(); // 关闭命令发射器
connection.close(); // 关闭连接

在这里插入图片描述

相关的API:

1、DriverManager:驱动管理类

2、Connection:代表数据库连接

3、Statement和PreparedStatement:用来执行sql

​	执行增、删、改:int executeUpate()

​	执行查询:ResultSet executeQuery()

4、如何遍历ResultSet ?
	(0)ResultSet可以使用迭代来查询
​	(1)boolean next():判断是否还有下一行
​	(2)getString(字段名或序号):获取字符串类型字段的数据内容。注意如果使用序号,mysql中是以1开始的
	(3)getInt(字段名或序号):获取整数类型字段的数据内容。注意如果使用序号,mysql中是以1开始的
	(4)getObject(字段名或序号):获取任意类型字段的数据内容。注意如果使用序号,mysql中是以1开始的

示例代码1:增、删、改

public class TestJDBC {
	public static void main(String[] args) throws ClassNotFoundException, SQLException {
		//1、注册驱动
	//	(1)方式一:Class.forName("驱动类的全名称")
		Class.forName("com.mysql.jdbc.Driver");
 //		(2)创建驱动类的对象
//		new com.mysql.jdbc.Driver();//硬编码
		//(3)通过DriverManager注册驱动
//		DriverManager.registerDriver(new com.mysql.jdbc.Driver());//硬编码
		
		//2、获取连接,连接数据库
        //TCP/IP协议编程,需要服务器的IP地址和端口号
		//mysql的url格式:jdbc协议:子协议://主机名:端口号/要连接的数据库名
		String url = "jdbc:mysql://localhost:3306/test";//其中test是数据库名
		String user = "root";
		String password = "123456";
		Connection conn = DriverManager.getConnection(url, user, password);
	
		//3、执行sql
		//添加一个部门到数据库的t_department表中
		//(1)编写sql
		String sql = "insert into t_department values(null,'计算部2','计算钞票2')";
		/*
		 * 回忆:	TCP/IP程序时
		 * Socket代表连接
		 * socket.getOutputStream()来发送数据,
		 * socket.getInputStream()来接收数据
		 * 
		 * 可以把Connection比喻成Socket
		 *    把Statement比喻成OutputStream
		 */
		//(2)获取Statement对象
		Statement st = conn.createStatement();
		//(3)执行sql
		int len = st.executeUpdate(sql);
		//(4)处理结果
		System.out.println(len>0?"成功":"失败");
		
		//4、关闭
		st.close();
		conn.close();
	}
}

示例代码2:查询

public class TestSelect {
	public static void main(String[] args) throws Exception{
		// 1、注册驱动
		Class.forName("com.mysql.jdbc.Driver");

		// 2、连接数据库
		Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");

		// 3、执行sql
		String sql = "SELECT * FROM t_department";
		Statement st = conn.createStatement();
		
		ResultSet rs = st.executeQuery(sql);//ResultSet看成InputStream
		// 4、处理结果
		while(rs.next()){//next()表示是否还有下一行
			Object did = rs.getObject(1);//获取第n列的值
			Object dname = rs.getObject(2);
			Object desc = rs.getObject(3);
			/*
			int did = rs.getInt("did");//也可以根据列名称,并且可以按照数据类型获取
			String dname = rs.getString("dname");
			String desc = rs.getString("description");
			 */
			
			System.out.println(did +"\t" + dname + "\t"+ desc);
		}

		// 5、关闭
		rs.close();
		st.close();
		conn.close();
	}
}

1.3 使用statement的一些问题

1.3.1 sql拼接问题

案例:我们希望键盘输入用户名,密码保存到sql的用户表中

class Demo2{

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");
        // 创建命令发射器
        Statement statement = connection.createStatement();
        // 准备sql
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String user = scanner.nextLine();
        System.out.println("请输入密码:");
        int password = scanner.nextInt();
        // 这种拼接是错误的,因为user和password是sql中的字符串,本身就需要引号括起来
        // String sql = "insert into user(user,password) values(" + user + "," + password + ")"; // 等价于"insert into user(user,password) values(张三,123456)"是错误的sql语句

        // 等价于 insert into user(user,password) values(' +  张三  +  ','  + 123456  +  ')
        // 拼接的sql是:insert into user(user,password) values('张三','123456')  是正确的sql
        String sql = "insert into user(user,password) values('" + user + "','" + password + "')";

        // 执行sql
        int res = statement.executeUpdate(sql);

        // 获取结果
        System.out.println(res>0?"注册成功":"注册失败");

        // 关闭资源
        scanner.close();
        statement.close();
        connection.close();
    }
}

可以发现,statement要动态拼接sql非常麻烦。

1.3.2 sql注入

案例:键盘输入用户名密码,然后查询user表是否存在该用户名密码,如果存在则提示登录成功,否则登录失败

class Demo3{
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");
        // 创建命令发射器
        Statement statement = connection.createStatement();
        // 准备sql
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String user = scanner.nextLine();
        System.out.println("请输入密码:");
        String password = scanner.nextLine();

        String sql = "select * from user where (user='"+user+"'and password='"+password+"')";
        System.out.println("sql = " + sql);

        // 执行sql
        ResultSet res = statement.executeQuery(sql);

        // 获取结果
        String resUser = null;
        String resPassword = null;
        while (res.next()){
            resUser = res.getString("user");
            resPassword = res.getString("password");
        }
        if (resUser != null && resPassword != null){
            System.out.println("登录成功");
        }else {
            System.out.println("登陆失败");
        }

        // 关闭资源
        scanner.close();
        statement.close();
        connection.close();
    }
}

然后我们来运行代码,实现sql注入:

请输入用户名:
张三
请输入密码:
123' or '1'='1
sql = select * from user where (user='张三'and password='123' or '1'='1')
登录成功

可以看到我们使用sql注入,即使输入错误的密码也登录成功了。

第二章 使用PreparedStatement处理CRUD

2.1 通过PreparedStatement来解决Statement的问题

Statement的问题:

(1)sql拼接

		String sql = "insert into t_employee(ename,tel,gender,salary) values('" + ename + "','" + tel + "','" + gender + "'," + salary +")";
		Statement st = conn.createStatement();
		int len = st.executeUpdate(sql);

(2)sql注入

		String sql = "SELECT * FROM t_employee where ename='" + ename + "'";
		//如果我此时从键盘输入ename值的时候,输入:张三' or '1'= '1
		//结果会把所有数据都查询出来
		Statement st = conn.createStatement();
		ResultSet rs = st.executeQuery(sql);

(3)无法处理blob等类型的数据(图片,文件等)

创建含有Blob字段类型的表 测试插入

String sql = "insert into user(username,photo) values('zs', 图片字节流)";
//此时photo是blob类型的数据时,无法在sql中直接拼接

PreparedStatement解决问题:

避免sql拼接

class Demo2{

    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");
        // 准备sql
        String sql = "insert into user(user,password) values(?,?)"; //使用?来占用一个位置
        // 创建命令发射器
        PreparedStatement statement = connection.prepareStatement(sql);// 在创建发射器时,要传带?的sql,这样mysql端就会对这个sql进行预编译
        // 填充sql
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String user = scanner.nextLine();
        System.out.println("请输入密码:");
        int password = scanner.nextInt();
        statement.setObject(1,user); // 给第一个?赋值
        statement.setObject(2,password); // 给第2个?赋值
        // 执行sql
        int res = statement.executeUpdate(); // 因为sql在创建发射器时已经编译完成,不需要再次传入sql
        // 获取结果
        System.out.println(res>0?"注册成功":"注册失败");

        // 关闭资源
        scanner.close();
        statement.close();
        connection.close();
    }
}

解决sql注入

class Demo3 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");

        // 准备sql
        String sql = "select * from user where (user=? and password=?)";
        // 创建命令发射器
        PreparedStatement statement = connection.prepareStatement(sql);
        // 填充sql
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入用户名:");
        String user = scanner.nextLine();
        System.out.println("请输入密码:");
        String password = scanner.nextLine();
        statement.setObject(1,user);
        statement.setObject(2,password);
        // 执行sql
        ResultSet res = statement.executeQuery();

        // 获取结果
        String resUser = null;
        String resPassword = null;
        while (res.next()) {
            resUser = res.getString("user");
            resPassword = res.getString("password");
        }
        if (resUser != null && resPassword != null) {
            System.out.println("登录成功");
        } else {
            System.out.println("登陆失败");
        }

        // 关闭资源
        scanner.close();
        statement.close();
        connection.close();
    }
}

运行一下代码。看看sql注入能否成功:

请输入用户名:
张三
请输入密码:
123 or 1==1 # 注入失败了,因为现在会将输入的所有内容作为密码进行校验
登陆失败

处理blob类型的数据(图片,文件等)

		String sql = "insert into user(username,photo) values(?,?)";
		PreparedStatement pst = conn.prepareStatement(sql);
		
		//设置?的值
		pst.setObject(1, "zs");
		FileInputStream fis = new FileInputStream("D:/QMDownload/img/美女/15.jpg");
		pst.setBlob(2, fis);
		
		int len = pst.executeUpdate();
		System.out.println(len>0?"成功":"失败");
  • 注意两个问题:

    ①my.ini关于上传的字节流文件有大小限制,可以在my.ini中配置变量

    ​ max_allowed_packet=16M

    ②mysql中使用tinyblob、blob、mediumblob、longblob四种数据类型来以二进制的形式储存图片,文件等数据。

    ③mysql中每一种blob有各自大小限制:

    tinyblob:255字节、blob:65k、mediumblob:16M、longblob:4G

练习:
新建一个img表,向表中储存图片,读取图片
①:新建img表

create table img(
id int primary key auto_increment,
img mediumblob,
iname char(20)
);

②:向表中插入数据

class Demo2 {

    public static void main(String[] args) throws ClassNotFoundException, SQLException, FileNotFoundException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");
        // 准备sql
        String sql = "insert into img(img,iname) values(?,?),(?,?)"; //使用?来占用一个位置
        // 创建命令发射器
        PreparedStatement statement = connection.prepareStatement(sql);// 在创建发射器时,要传带?的sql,这样mysql端就会对这个sql进行预编译
        // 填充sql
        FileInputStream file1 = new FileInputStream("E:\\hehe\\0K0xGPu9gU.jpeg");
        FileInputStream file2 = new FileInputStream("E:\\hehe\\4c024fcb172f8e3c4614747deff0b80e.jpg");
        statement.setObject(1, file1); // 给第一个?赋值 // blob类型的数据,要求传入一个InputStream类型的数据
        statement.setObject(2, "img1"); // 给第2个?赋值
        statement.setObject(3, file2); // 给第3个?赋值 // blob类型的数据,要求传入一个InputStream类型的数据
        statement.setObject(4, "img2"); // 给第4个?赋值
        // 执行sql
        int res = statement.executeUpdate(); // 因为sql在创建发射器时已经编译完成,不需要再次传入sql
        // 获取结果
        System.out.println(res > 0 ? "保存成功" : "保存失败");
        // 关闭资源
        statement.close();
        connection.close();
    }
}

查看数据库表,数据添加成功:
在这里插入图片描述
③:获取图片

class Demo3 {
    public static void main(String[] args) throws ClassNotFoundException, SQLException, IOException {
        // 创建驱动
        Class.forName("com.mysql.cj.jdbc.Driver");
        // 创建连接
        String url = "jdbc:mysql://localhost:3306/test2";
        Connection connection = DriverManager.getConnection(url, "root", "xxx");

        // 准备sql
        String sql = "select * from img where (id=1)";
        // 创建命令发射器
        PreparedStatement statement = connection.prepareStatement(sql);
        // 执行sql
        ResultSet res = statement.executeQuery();

        // 获取结果
        String resUser = null;
        String resPassword = null;
        while (res.next()) {
            Blob img = res.getBlob("img"); // 返回的是一个blob对象
            // 将读取到的blob数据内容保存到文件中
            InputStream binaryStream = img.getBinaryStream();// 将blob对象转化为InputStream对象
            byte[] bytes = new byte[8];
            int len;
            FileOutputStream fileOutputStream = new FileOutputStream("D:ss.jpg");
            while ((len = binaryStream.read(bytes)) != -1) {
                fileOutputStream.write(bytes,0,len);
            }
        }

        // 关闭资源
        statement.close();
        connection.close();
    }
}

运行代码,图片保存成功了:
在这里插入图片描述

2.2 获取自增长键值

实际开发中,我们往往将id字段设置为主键并自动增长,那么当我们update一条数据时,获取到它的id就非常关键

/*
 * 我们通过JDBC往数据库的表格中添加一条记录,其中有一个字段是自增的,那么在JDBC这边怎么在添加之后直接获取到这个自增的值
 * PreparedStatement是Statement的子接口。
 * Statement接口中有一些常量值:
 * (1)Statement.RETURN_GENERATED_KEYS
 * 
 * 要先添加后获取到自增的key值:
 * (1)PreparedStatement pst = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
 * (2)添加sql执行完成后,通过PreparedStatement的对象调用getGeneratedKeys()方法来获取自增长键值,遍历结果集
 * 		ResultSet rs = pst.getGeneratedKeys();
 */
public class TestAutoIncrement {
	public static void main(String[] args) throws Exception{
		//1、注册驱动
		Class.forName("com.mysql.jdbc.Driver");
		
		//2、获取连接
		Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
		
		//3、执行sql
		String sql = "insert into t_department values(null,?,?)";
		/*
		 * 这里在创建PreparedStatement对象时,传入第二个参数的作用,就是告知服务器端
		 * 当执行完sql后,把自增的key值返回来。
		 */
		PreparedStatement pst = conn.prepareStatement(sql,Statement.RETURN_GENERATED_KEYS);
		
		//设置?的值
		pst.setObject(1, "测试部");
		pst.setObject(2, "测试项目数据");
		
		//执行sql
		int len = pst.executeUpdate();//返回影响的记录数
		if(len>0){
			//从pst中获取到服务器端返回的键值
			ResultSet rs = pst.getGeneratedKeys();
			//因为这里的key值可能多个,因为insert语句可以同时添加多行,所以用ResultSet封装
			//这里因为只添加一条,所以用if判断
			if(rs.next()){
				Object key = rs.getObject(1); // 这里用1去获取自增值,不要跟查询语句返回的结果集搞混了
				System.out.println("自增的key值did =" + key);
			}
		}
			
		//4、关闭
		pst.close();
		conn.close();
	}
}

2.3 批处理

需要在url的末尾添加此参数:rewriteBatchedStatements=true

/*
 * 批处理:
 * 	批量处理sql
 * 
 * 例如:
 * (1)订单明细表的多条记录的添加
 * (2)批量添加模拟数据
 * ...
 * 
 * 不用批处理,和用批处理有什么不同?
 * 批处理的效率很多
 * 
 * 如何进行批处理操作?
 * (1)在url中要加一个参数
 *     rewriteBatchedStatements=true
 *     那么我们的url就变成了  jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true
 *     这里的?,表示?后面是客户端给服务器端传的参数,多个参数直接使用&分割
 * (2)调用方法不同
 * pst.addBatch();
 * int[] all = pst.executeBatch();
 * 
 * 注意:如果批量添加时,insert使用values,不要使用value
 */
public class TestBatch {
	public static void main(String[] args) throws Exception{
		long start = System.currentTimeMillis();
		//例如:在部门表t_department中添加1000条模拟数据
		//1、注册驱动
		Class.forName("com.mysql.jdbc.Driver");
		//2、获取连接
		Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true", "root", "123456");
		//3、执行sql
		String sql = "insert into t_department values(null,?,?)";
		PreparedStatement pst = conn.prepareStatement(sql);
		//设置?的值
		for (int i = 1; i <=1000; i++) {
			pst.setObject(1, "模拟部门"+i);
			pst.setObject(2, "模拟部门的简介"+i);
			pst.addBatch();//添加到批处理一组操作中,攒一块处理
/*          if(i % 500 == 0){//有时候也攒一部分,执行一部分
				//2.执行
				pst.executeBatch();
				//3.清空
				pst.clearBatch();
			}*/
		}
		pst.executeBatch(); // 执行批处理
		//4、关闭
		pst.close();
		conn.close();
		long end = System.currentTimeMillis();
		System.out.println("耗时:" + (end - start));//耗时:821
	}
}

2.4 事务

/*
 * mysql默认每一个连接是自动提交事务的。
 * 那么当我们在JDBC这段,如果有多条语句想要组成一个事务一起执行的话,那么在JDBC这边怎么设置手动提交事务呢?
 * (1)在执行之前,设置手动提交事务
 * Connection的对象.setAutoCommit(false)
 * (2)成功:
 * Connection的对象.commit();
 * 失败:
 * Connection的对象.rollback();
 * 
 * 补充说明:
 * 为了大家养成要的习惯,在关闭Connection的对象之前,把连接对象设置回自动提交
 * (3)Connection的对象.setAutoCommit(true)
 * 
 * 因为我们现在的连接是建立新的连接,那么如果没有还原为自动提交,没有影响。
 * 但是我们后面实际开发中,每次获取的连接,不一定是新的连接,而是从连接池中获取的旧的连接,而且你关闭也不是真关闭,而是还给连接池,供别人接着用。以防别人拿到后,以为是自动提交的,而没有commit,最终数据没有成功。
 */
public class TestTransaction {
	public static void main(String[] args) throws Exception{
		/*
		 * 一般涉及到事务处理的话,那么业务逻辑都会比较复杂。
		 * 例如:购物车结算时:
		 * (1)在订单表中添加一条记录
		 * (2)在订单明细表中添加多条订单明细的记录(表示该订单买了什么东西)
		 * (3)修改商品表的销量和库存量
		 * ...
		 * 那么我们今天为了大家关注事务的操作,而不会因为复杂的业务逻辑的影响导致我们的理解,那么我们这里故意
		 * 用两条修改语句来模拟组成一个简单的事务。
		 * update t_department set description = 'xx' where did = 2;
		 * update t_department set description = 'yy' where did = 3;
		 * 
		 * 我希望这两天语句要么一起成功,要么一起回滚
		 * 为了制造失败,我故意把第二条语句写错
		 * update t_department set description = 'yy' (少了where) did = 3;
		 */
		
		//1、注册驱动
		Class.forName("com.mysql.jdbc.Driver");
		
		//2、获取连接
		Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
		
		//设置手动提交事务
		conn.setAutoCommit(false);
		
		//3、执行sql
		String sql1 = "update t_department set description = 'xx' where did = 2";
		String sql2 = "update t_department set description = 'yy' did = 3";//这是错的
		
		//使用prepareStatement的sql也可以不带?
		PreparedStatement pst = null;
		try {
			pst = conn.prepareStatement(sql1);
			int len = pst.executeUpdate();
			System.out.println("第一条:" + (len>0?"成功":"失败"));
			
			pst = conn.prepareStatement(sql2);
			len = pst.executeUpdate();
			System.out.println("第二条:" + (len>0?"成功":"失败"));
			
			//都成功了,就提交事务
			System.out.println("提交");
			conn.commit();
		} catch (Exception e) {
			System.out.println("回滚");
			//失败要回滚
			conn.rollback();
		}
		//4、关闭
		pst.close();
		conn.setAutoCommit(true);//还原为自动提交
		conn.close();
	}
}

2.5 封装PreparedStatement

(1)封装的意义

  • 我们前面的代码都是直接编码的,没有任何封装。这会导致我们每一次访问数据库都需要写一大堆代码。
  • 我们可以将访问数据库的代码封装为一个util类,这样我们想访问数据库只需要实例化该util类对象即可

(2)使用properties文件存放数据库相关的配置信息

像前面代码那样将数据库的url、用户名、密码等直接硬编码到代码中是不合适的,我们可以使用配置文件来储存。

①properties文件

properties是一种配置文件,以key=value的形式来存放数据。

现在我们在模块的src目录下去创建properties对象,用来储存我们的数据库配置信息
在这里插入图片描述

②Properties类介绍
  • Properties类是Hashtable的子类,也是一种集合,以[key,value]的形式储存数据。
  • Properties可以很方便的去操作properties文件
  • 常用API
    public synchronized Object setProperty(String key, String value):将【key,value】储存到Properties集合中
    public String getProperty(String key):通过key获取value的值
    public void store(OutputStream out, String comments):将集合中的数据持久化保存到文件中,comments是对该文件的描述
    public synchronized void load(InputStream inStream):从文件中的数据读取到集合中
    
  • 练习
    // 向集合中添加读取数据
    class Demo6{
        public static void main(String[] args) {
            Properties properties = new Properties();
            properties.setProperty("1", "哈哈"); // 储存数据到集合中
            properties.setProperty("2", "嘿嘿");
            properties.setProperty("3", "嘻嘻");
            String res = properties.getProperty("1");// 获取"1"对应的value值
            System.out.println("res = " + res);
        }
    }
    
    // 将集合中的数据保存到文件中
    class Demo4{
    
        public static void main(String[] args) throws IOException {
            Properties properties = new Properties();
            properties.setProperty("1", "哈哈");
            properties.setProperty("2", "嘿嘿");
            properties.setProperty("3", "嘻嘻");
            OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties"),"UTF-8");
            properties.store(outputStreamWriter,"this is a properties file"); // 将集合中的数据保存到文件jdbc.properties中
        }
    }
    
    class Demo5{
    	// 读取文件中的数据到集合中
        public static void main(String[] args) throws IOException {
            Properties properties = new Properties();
            InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties"), "UTF-8");
            properties.load(inputStreamReader);  // 读取文件中的数据到集合中
            System.out.println("properties.getProperty(\"1\") = " + properties.getProperty("1"));
        }
    }
    
③实现配置信息读取的封装
package com.oy.jdbctest.jdbcutils;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class MyJdbcUtils{
    private static String user;
    private static String password;
    private static String url;
    private static String driver;
    // 读取配置文件的内容,最好放到静态代码块中。这样只会在初始化类时,加载一次
    // 而不会每生成一个实例化对象都调用一次,减少IO操作
    static {
        try {
            Properties properties = new Properties();
            //properties.load(new FileInputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties")); // 使用配置文件的绝对路径
            properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")); // 使用反射来使用配置文件的相对路径
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            url = properties.getProperty("url");
            driver = properties.getProperty("driver");
            System.out.println(url+"\t"+user+"\t"+password+"\t"+driver);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

(3)数据库连接的封装

我们可以将数据库连接封装为一个方法。

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Properties;

public class MyJdbcUtils{
   private static String user;
   private static String password;
   private static String url;
   private static String driver;
   // 读取配置文件的内容,最好放到静态代码块中。这样只会在初始化类时,加载一次
   // 而不会每生成一个实例化对象都调用一次,减少IO操作
   static {
       try {
           Properties properties = new Properties();
           //properties.load(new FileInputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties")); // 使用配置文件的绝对路径
           properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")); // 使用反射来使用配置文件的相对路径
           user = properties.getProperty("user");
           password = properties.getProperty("password");
           url = properties.getProperty("url");
           driver = properties.getProperty("driver");
           System.out.println(url+"\t"+user+"\t"+password+"\t"+driver);
       } catch (IOException e) {
           e.printStackTrace();
       }

   }

   // 连接数据库
   public Connection  getConnect(){

       try {
           // 创建驱动
           Class.forName("com.mysql.cj.jdbc.Driver");
           // 创建连接
           String url = "jdbc:mysql://localhost:3306/test2";
           Connection connection = DriverManager.getConnection(url, "root", "xxx");
           return connection;
       } catch (ClassNotFoundException e) {
           e.printStackTrace();
       } catch (SQLException throwables) {
           throwables.printStackTrace();
       }
       return null;
   }
}

(4)资源关闭的封装

连接数据库后,会有一大堆东西需要关闭。我们也可以给他包装成一个方法

package com.oy.jdbctest.jdbcutils;

import java.io.FileInputStream;
import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class MyJdbcUtils{
    private static String user;
    private static String password;
    private static String url;
    private static String driver;
    // 读取配置文件的内容,最好放到静态代码块中。这样只会在初始化类时,加载一次
    // 而不会每生成一个实例化对象都调用一次,减少IO操作
    static {
        try {
            Properties properties = new Properties();
            //properties.load(new FileInputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties")); // 使用配置文件的绝对路径
            properties.load(JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")); // 使用反射来使用配置文件的相对路径
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            url = properties.getProperty("url");
            driver = properties.getProperty("driver");
            System.out.println(url+"\t"+user+"\t"+password+"\t"+driver);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 连接数据库
    public static Connection  getConnect(){

        try {
            // 创建驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
            // 创建连接
            String url = "jdbc:mysql://localhost:3306/test2";
            Connection connection = DriverManager.getConnection(url, "root", "xxx");
            return connection;
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return null;
    }

    // 关闭资源
    public static void closeConnect(Connection connection, Statement statement, ResultSet resultSet){
        if (connection != null){
            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement!=null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (resultSet!=null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }
}

(5)测试:使用封装的工具类完成数据库的访问

class Demo7{
    public static void main(String[] args) throws SQLException {
        // 获取连接
        Connection connect = MyJdbcUtils.getConnect();
        // 生成sql命令发射器
        PreparedStatement preparedStatement = connect.prepareStatement("select * from employ3");
        // 执行sql
        ResultSet resultSet = preparedStatement.executeQuery();
        // 获取结果
        while (resultSet.next()){
            String name = resultSet.getString("ename");
            String salary = resultSet.getString("salary");
            System.out.println("姓名:"+name+"\t薪资:"+salary);
        }
        // 如果每这么多资源需要关闭,直接填null就行
        JdbcUtils.closeConnect(connect,preparedStatement,resultSet);
    }
}

2.65 查询结果封装为对象(使用元数据)

实际开发者,一张数据表往往对应Java中的一个类,表字段对应类的成员属性。所有我们获取到数据表中的数据时,需要将它们封装为该类的一个对象。

如下是一张accout表,我们获取到表中的数据后将每一条记录封装为一个Account对象。
在这里插入图片描述
①:创建Account类

package com.oy.jdbctest.accouttest;

public class Account {
    private int id;
    private String aname;
    private double money;
    public Account(int id, String aname, double money) {
        this.id = id;
        this.aname = aname;
        this.money = money;
    }
    public Account() {
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }

    public String getAname() {
        return aname;
    }
    public void setAname(String aname) {
        this.aname = aname;
    }
    public double getMoney() {
        return money;
    }
    public void setMoney(double money) {
        this.money = money;
    }
    @Override
    public String toString() {
        return "Account{" +
                "id=" + id +
                ", aname='" + aname + '\'' +
                ", money=" + money +
                '}';
    }
}

②:获取数据表account中的所有数据,并封装为Account类对象

package com.oy.jdbctest.accouttest;

import com.oy.jdbctest.jdbcutils.JdbcUtils;

import java.lang.reflect.Field;
import java.sql.*;

class Test {
    public static void main(String[] args) {

        try {
            // 1.获取连接
            Connection connect = MyJdbcUtils.getConnect();
            // 2.生成发射器
            PreparedStatement pst = connect.prepareStatement("select * from acount");
            // 3.执行sql
            ResultSet resultSet = pst.executeQuery();

            // 4.处理结果,将读取的数据封装为对象
            // getMetaData()方法可以获取结果集resultSet的元数据
            // 数据就是描述结果集中的数据的数据,例如:列数,列名称, 列别名等
            /*
             * 元数据.getColumnCount(); 获取列数
             * 元数据.getColumnLabel(n); 获取第n列字段的别名,从1开始计算
             * 元数据.getColumnClassName(n); 获取第n列字段的字段名(别名不起作用)
             */
            ResultSetMetaData metaData = resultSet.getMetaData();
            int len = metaData.getColumnCount();
            while (resultSet.next()){
                Account account = new Account(); // 每一条记录创建一个对象
                Class<? extends Account> aClass = account.getClass(); // 获取class对象,使用反射给属性赋值
                // 遍历所有字段,并将值赋给对象的属性
                for (int i = 1; i <= len; i++) {
                    String key = metaData.getColumnLabel(i); // 获取字段
                    Object value = resultSet.getObject(key); // 获取该条记录字段的值
                    Field declaredField = aClass.getDeclaredField(key); // 获取属性对象
                    declaredField.setAccessible(true); // 将私有属性设置为可访问
                    declaredField.set(account,value); // 给属性赋值
                }
                System.out.println(account); // 打印一下对象
            }

        } catch (SQLException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

第三章 增删改查通用方法封装

前面我们将数据库的连接,释放操作进行了封装。但是还不够,我们可以将数据库的增删改查操作进行封装,这样我们在想要操作数据库时,只需要使用相关方法即可,不需要去关心数据库的连接,释放操作。

1.增删改方法封装

package com.oy.jdbctest.jdbcutils;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class BasiDao1 {
    /**
     * /通用的增删改操作
     * @param sql: 需要执行的sql
     * @param args:需要传入的参数
     * @return int: 本次操作影响的记录数
     */
    public int basUpdate(String sql,Object... args) throws SQLException {
        // 1.获取连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.创建命令发射器
        PreparedStatement preparedStatement = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            preparedStatement.setObject(i,args[i-1]);
        }
        // 4.执行sql
        int i = preparedStatement.executeUpdate();
        return i;
    }
}

测试一下:

class Demo10 {
    public static void main(String[] args) {
        try {
            BasiDao1 basiDao1 = new BasiDao1();
            int res = basiDao1.basUpdate("insert into user values(null,?,?)", "王麻子", "123456");
            System.out.println(res > 0 ? "添加成功" : "添加失败");
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}

结果如下:数据添加成功
在这里插入图片描述

2.通用的查询方法封装

(1)有实体类对象的查询

查询的方法会返回一个结果集,需要将结果集中的数据封装为对应的Java类对象。前面我们已经讲过通过元数据和反射来实现,但是现在我们要实现通用的查询方法,也就是改方法来能适用于任何的类(满足JavaBean)。那么我们需要使用泛型来实现

package com.oy.jdbctest.jdbcutils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;

public class BasiDao1 {



    /**
     * /通用的增删改操作
     * @param sql: 需要执行的sql
     * @param args:需要传入的参数
     * @return int: 本次操作影响的记录数
     */
    public int basUpdate(String sql,Object... args) throws SQLException {
        // 1.获取连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.创建命令发射器
        PreparedStatement preparedStatement = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            preparedStatement.setObject(i,args[i-1]);
        }
        // 4.执行sql
        int i = preparedStatement.executeUpdate();
        // 关闭资源
        MyJdbcUtils.closeConnect(connect,preparedStatement,null);
        return i;
    }

    /**
     * 通用的查询方法,适用于多个JavaBean类对象
     * @param clazz:数据表对应的Java类的class对象
     * @param sql:需要执行的sql语句
     * @param args:需要传入的参数
     * @param <T>:对象类型
     * @return Arraylist<T>: 对象列表
     */
    public <T> ArrayList<T> getAll(Class<T> clazz,String sql, Object... args) throws Exception {
        // 1.创建连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.生成命令发射器
        PreparedStatement pst = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            pst.setObject(i,args[i-1]);
        }
        // 4.执行sql
        ResultSet resultSet = pst.executeQuery();
        // 5.处理结果,将其封装为对应的对象
        ResultSetMetaData metaData = resultSet.getMetaData(); //获取结果集中的元数据
        int count = metaData.getColumnCount(); // 获取字段列数
        ArrayList<T> lists = new ArrayList<>(); // 生成一个列表,用于存放对象
        while (resultSet.next()){ // 获取到结果中的每一行记录
            T t = clazz.newInstance(); // 获取JavaBean的实例对象
            for (int i = 0; i < count; i++) {
                String columnLabel = metaData.getColumnLabel(i + 1); // 获取字段名
                Object value = resultSet.getObject(columnLabel); // 获取记录中对应字段的数据值
                // 使用反射将数据值保存JavaBean实例对象的属性中
                Field declaredField = clazz.getDeclaredField(columnLabel);
                declaredField.setAccessible(true);
                declaredField.set(t,value);
            }
            lists.add(t); // 将对象添加到集合中
        }
        MyJdbcUtils.closeConnect(connect,pst,resultSet);
        return lists;
    }
}

测试一下:

class Demo11{
    public static void main(String[] args) throws Exception {
        BasiDao1 basiDao1 = new BasiDao1();
        ArrayList lists = basiDao1.getAll(Account.class,"select * from acount");
        lists.forEach(System.out::println);
    }
}

效果如下:

Account{id=1, aname='张三', money=2500.0}
Account{id=2, aname='李四', money=1500.0}

(2)没有实体类对象的查询

  • 还存在这种情况,我们的查询sql中有分组函数(COUNT,SUM等)会导致字段改变,这样我们Java类就对象不上查询结果了。这种没有实体类对象的查询该如何封装呢
  • 解决办法:使用Map来储存数据,然后将Map储存在List中
package com.oy.jdbctest.jdbcutils;

import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

public class BasiDao1 {



    /**
     * /通用的增删改操作
     * @param sql: 需要执行的sql
     * @param args:需要传入的参数
     * @return int: 本次操作影响的记录数
     */
    public int basUpdate(String sql,Object... args) throws SQLException {
        // 1.获取连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.创建命令发射器
        PreparedStatement preparedStatement = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            preparedStatement.setObject(i,args[i-1]);
        }
        // 4.执行sql
        int i = preparedStatement.executeUpdate();
        // 关闭资源
        MyJdbcUtils.closeConnect(connect,preparedStatement,null);
        return i;
    }

    /**
     * 通用的查询方法,适用于多个JavaBean类对象
     * @param clazz:数据表对应的Java类的class对象
     * @param sql:需要执行的sql语句
     * @param args:需要传入的参数
     * @param <T>:对象类型
     * @return Arraylist<T>: 对象列表
     */
    public <T> ArrayList<T> getAll(Class<T> clazz,String sql, Object... args) throws Exception {
        // 1.创建连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.生成命令发射器
        PreparedStatement pst = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            pst.setObject(i,args[i-1]);
        }
        // 4.执行sql
        ResultSet resultSet = pst.executeQuery();
        // 5.处理结果,将其封装为对应的对象
        ResultSetMetaData metaData = resultSet.getMetaData(); //获取结果集中的元数据
        int count = metaData.getColumnCount(); // 获取字段列数
        ArrayList<T> lists = new ArrayList<>(); // 生成一个列表,用于存放对象
        while (resultSet.next()){ // 获取到结果中的每一行记录
            T t = clazz.newInstance(); // 获取JavaBean的实例对象
            for (int i = 0; i < count; i++) {
                String columnLabel = metaData.getColumnLabel(i + 1); // 获取字段名
                Object value = resultSet.getObject(columnLabel); // 获取记录中对应字段的数据值
                // 使用反射将数据值保存JavaBean实例对象的属性中
                Field declaredField = clazz.getDeclaredField(columnLabel);
                declaredField.setAccessible(true);
                declaredField.set(t,value);
            }
            lists.add(t); // 将对象添加到集合中
        }
        MyJdbcUtils.closeConnect(connect,pst,resultSet);
        return lists;
    }

    /**
     * 通用的查询方法,适用于没有实体类的查询
     * @param sql:待执行的sql
     * @param args:传入的参数
     * @return ArrayList: Map对象列表
     */
    public ArrayList<Map> getAllToMap(String sql,Object... args) throws Exception{
        // 1.获取连接
        Connection connect = MyJdbcUtils.getConnect();
        // 2.生成命令发射器
        PreparedStatement pst = connect.prepareStatement(sql);
        // 3.填充sql
        for (int i = 1; i < args.length+1; i++) {
            //设置?的值
            //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
            //Object... args可变形参,可以传入0~n个值
            //如果没有传入,说明没有?需要设置
            //如果传入了n个值,那么说明sql中有n个?需要设置
            // sql中的?是从1开始,而args的下标是从0开始
            pst.setObject(i,args[i-1]);
        }
        // 4.执行sql
        ResultSet resultSet = pst.executeQuery();
        // 5.处理结果
        ResultSetMetaData metaData = resultSet.getMetaData(); // 获取元数据
        int count = metaData.getColumnCount(); // 获取字段列数
        ArrayList<Map> lists = new ArrayList<>();
        while (resultSet.next()){ // 获取每一条记录,进行处理
            HashMap<String, Object> hashMap = new HashMap<>();
            for (int i = 0; i < count; i++) {
                String key = metaData.getColumnLabel(i + 1); // 获取字段名
                Object value = resultSet.getObject(key); // 获取对应字段名的值
                hashMap.put(key,value);
            }
            lists.add(hashMap);
        }
        MyJdbcUtils.closeConnect(connect,pst,resultSet);
        return lists;
    }
}

测试一下:

{avg(salary)=17258.22375, e_pid=1}
{avg(salary)=20250.29, e_pid=2}
{avg(salary)=15282.34, e_pid=3}
{avg(salary)=13644.253333, e_pid=4}
{avg(salary)=13932.08, e_pid=5}
{avg(salary)=8752.66, e_pid=6}
{avg(salary)=10256.3, e_pid=7}
{avg(salary)=7855.44, e_pid=8}
{avg(salary)=15466.88, e_pid=9}
{avg(salary)=3400.22, e_pid=10}
{avg(salary)=4000.33, e_pid=11}

3.使用ThreadLocal实现事务处理

  • 前面我们对增删查改方法进行了封装,但是有一个问题。那就是无法实现事务处理。如下所示:我们的事务操作虽然是在同一个线程下完成的,但开启事务和执行sql的不是同一个连接,所以事务是无法生效的
    Connection connect = null;
    try {
        connect = MyJdbcUtils.getConnect(); // 连接1
        // 开启事务
        connect.setAutoCommit(false);
        // 执行sql
        BasiDao1 basiDao1 = new BasiDao1();
        basiDao1.basUpdate("update acount set money = money-500 where id = 1"); // 连接2
        basiDao1.basUpdate("update acount set money = money+500 where id = 2"); // 连接3
        connect.commit();
    } catch (SQLException e) {
        try {
            connect.rollback(); // 失败就回滚
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        e.printStackTrace();
    }
    
  • 解决办法:就是要保证在同一个线程下,无论创建几次连接,它们都要是同一个连接。可以使用多线程中的ThreadLocal类来实现
  • ThreadLocal简介:
    ThreadLocal用于保存某个线程共享变量,原因是在Java中,每一个线程对象中都有一个ThreadLocalMap<ThreadLocal, Object>,其key
    就是一个ThreadLocal,而Object即为该线程的共享变量。而这个map是通过ThreadLocal的set和get方法操作的。对于同一个static 
    ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。
    
    1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的值。
    
    2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的值。
    
    3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。
    
  • 实现原理:
1.我们在调用getConnect时,不直接分配一个新的连接。而是使用ThreadLocal.get方法检查当前线程中的共享变量是否存在。
2.如果为null表明当前线程中还没有数据库连接。则创建一个新连接。并使用ThreadLocal.set方法把该连接对象设置为线程共
享变量
3.再次调用getConnect时,那么ThreadLocal.get方法返回的就不是null了。这时我们则不新建连接,而是将ThreadLocal
中的共享对象返回
4.当调用closeConnect释放连接时,将ThreadLocal中的共享对象也释放掉

我们在MyJdbcUtils 类中引入ThreadLocal


import java.io.IOException;
import java.sql.*;
import java.util.Properties;

public class MyJdbcUtils {
    private static String user;
    private static String password;
    private static String url;
    private static String driver;
    private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
    // 读取配置文件的内容,最好放到静态代码块中。这样只会在初始化类时,加载一次
    // 而不会每生成一个实例化对象都调用一次,减少IO操作
    static {
        try {
            Properties properties = new Properties();
            //properties.load(new FileInputStream("E:\\Java全栈\\code\\JavaSE\\src\\jdbc.properties")); // 使用配置文件的绝对路径
            properties.load(MyJdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties")); // 使用反射来使用配置文件的相对路径
            user = properties.getProperty("user");
            password = properties.getProperty("password");
            url = properties.getProperty("url");
            driver = properties.getProperty("driver");
            System.out.println(url+"\t"+user+"\t"+password+"\t"+driver);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    // 连接数据库
    public static Connection  getConnect(){

        try {
            // 创建驱动
            if (threadLocal.get() != null){
                // 表示该线程已经有连接了
                return threadLocal.get();
            }else {
                // 表示该线程还没有连接
                Class.forName("com.mysql.cj.jdbc.Driver");
                // 创建连接
                String url = "jdbc:mysql://81.71.84.102:3306/test2";
                Connection connection = DriverManager.getConnection(url, "root", "ouyi1994");
                threadLocal.set(connection);
                return connection;
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException throwables) {
            throwables.printStackTrace();
        }
        return null;
    }

    // 关闭资源
    public static void closeConnect(Connection connection, Statement statement, ResultSet resultSet){
        if (connection != null){
            try {
                threadLocal.remove(); // 清除共享数据
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (statement!=null){
            try {
                statement.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
        if (resultSet!=null){
            try {
                resultSet.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }

    }
}

注意:现在的增删查改方法中就不能直接关闭connect了,只能在事务完成后关闭、

public int basUpdate(String sql,Object... args) throws SQLException {
    // 1.获取连接
    Connection connect = MyJdbcUtils.getConnect();
    // 2.创建命令发射器
    PreparedStatement preparedStatement = connect.prepareStatement(sql);
    // 3.填充sql
    for (int i = 1; i < args.length+1; i++) {
        //设置?的值
        //因为不知道sql中是否有?,以及?设置为什么值,通过形参来传入
        //Object... args可变形参,可以传入0~n个值
        //如果没有传入,说明没有?需要设置
        //如果传入了n个值,那么说明sql中有n个?需要设置
        // sql中的?是从1开始,而args的下标是从0开始
        preparedStatement.setObject(i,args[i-1]);
    }
    // 4.执行sql
    int i = preparedStatement.executeUpdate();
    // 关闭资源
    MyJdbcUtils.closeConnect(null,preparedStatement,null); // 这里就不能关闭connect了,因为事务可能还需要使用
    return i;
}

测试代码:

class Demo13{
    public static void main(String[] args){

        Connection connect = null;
        try {
            connect = MyJdbcUtils.getConnect();
            // 开启事务
            connect.setAutoCommit(false);
            // 执行sql
            BasiDao1 basiDao1 = new BasiDao1();
            basiDao1.basUpdate("update acount set money = money-500 where id = 1");
            basiDao1.basUpdate("update acount set money = money-500 where id = 2");
            connect.commit();
        } catch (SQLException e) {
            try {
                connect.rollback(); // 失败就回滚
            } catch (SQLException ex) {
                ex.printStackTrace();
            }
            e.printStackTrace();
        }finally {
            MyJdbcUtils.closeConnect(connect,null,null); // 需要关闭连接
        }

    }

}

第四章 数据库连接池

1、什么是数据库连池

连接对象的缓冲区。负责申请,分配管理,释放连接的操作。

2、为什么要使用数据库连接池

Connection对象在每次执行DML和DQL的过程中都要创建一次,DML和DQL执行完毕后,connection对象都会被销毁. connection对象是可以反复使用的,没有必要每次都创建新的.该对象的创建和销毁都是比较消耗系统资源的,如何实现connection对象的反复使用呢?使用连接池技术实现。

3.连接池的优势

  • 预先准备一些链接对象,放入连接池中,当多个线程并发执行时,可以避免短时间内一次性大量创建链接对象,减少计算机单位时间内的运算压力,提高程序的响应速度
  • 实现链接对象的反复使用,可以大大减少链接对象的创建次数,减少资源的消耗

4、市面上有很多现成的数据库连接池技术:

  • JDBC 的数据库连接池使用 javax.sql.DataSource 来表示,DataSource 只是一个接口(通常被称为数据源),该接口通常由服务器(Weblogic, WebSphere, Tomcat)提供实现,也有一些开源组织提供实现:
    • DBCP 是Apache提供的数据库连接池,速度相对c3p0较快,但因自身存在BUG,Hibernate3已不再提供支持
    • C3P0 是一个开源组织提供的一个数据库连接池,速度相对较慢,稳定性还可以
    • Proxool 是sourceforge下的一个开源项目数据库连接池,有监控连接池状态的功能,稳定性较c3p0差一点
    • BoneCP 是一个开源组织提供的数据库连接池,速度快
    • Druid 是阿里提供的数据库连接池,据说是集DBCP 、C3P0 、Proxool 优点于一身的数据库连接池

5、阿里的德鲁伊连接池技术(Druid)

(1)加入jar包

例如:druid-1.1.10.jar

(2)代码步骤

第一步:建立一个数据库连接池

第二步:设置连接池的参数

第三步:获取连接

public class TestPool {
	public static void main(String[] args) throws SQLException {
		//1、创建数据源(数据库连接池)对象
		DruidDataSource ds =new DruidDataSource();
		
		//2、设置参数
		//(1)设置基本参数
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost:3306/test");
		ds.setUsername("root");
		ds.setPassword("123456");
		
		//(2)设置连接数等参数
		ds.setInitialSize(5);//一开始提前申请好5个连接,不够了,重写申请
		ds.setMaxActive(10);//最多不超过10个,如果10都用完了,还没还回来,就会出现等待
		ds.setMaxWait(1000);//用户最多等1000毫秒,如果1000毫秒还没有人还回来,就异常了
		
		//3、获取连接
		for (int i = 1; i <=15; i++) {
			Connection conn = ds.getConnection();
			System.out.println("第"+i+"个:" + conn);
			
			//如果这里没有关闭,就相当于没有还
			conn.close();#这里关闭,是还回池中
		}
	}
}
配置缺省说明
name配置这个属性的意义在于,如果存在多个数据源,监控的时候可以通过名字来区分开来。 如果没有配置,将会生成一个名字,格式是:”DataSource-” + System.identityHashCode(this)
url连接数据库的url,不同数据库不一样。例如:mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username连接数据库的用户名
password连接数据库的密码。如果你不希望密码直接写在配置文件中,可以使用ConfigFilter。详细看这里:https://github.com/alibaba/druid/wiki/使用ConfigFilter
driverClassName根据url自动识别 这一项可配可不配,如果不配置druid会根据url自动识别dbType,然后选择相应的driverClassName(建议配置下)
initialSize0初始化时建立物理连接的个数。初始化发生在显示调用init方法,或者第一次getConnection时
maxActive8最大连接池数量
maxIdle8已经不再使用,配置了也没效果
minIdle最小连接池数量
maxWait获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
poolPreparedStatementsfalse是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxOpenPreparedStatements-1要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validationQuery用来检测连接是否有效的sql,要求是一个查询语句。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会其作用。
testOnBorrowtrue申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
testOnReturnfalse归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testWhileIdlefalse建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
timeBetweenEvictionRunsMillis有两个含义: 1)Destroy线程会检测连接的间隔时间2)testWhileIdle的判断依据,详细看testWhileIdle属性的说明
numTestsPerEvictionRun不再使用,一个DruidDataSource只支持一个EvictionRun
minEvictableIdleTimeMillis
connectionInitSqls物理连接初始化的时候执行的sql
exceptionSorter根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
proxyFilters类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值