第一章 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());  # 方式1Class.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)编写sqlString 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)执行sqlint 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、执行sqlString 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();// 准备sqlScanner 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')  是正确的sqlString sql = "insert into user(user,password) values('" + user + "','" + password + "')";// 执行sqlint 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();// 准备sqlScanner 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);// 执行sqlResultSet 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");// 准备sqlString sql = "insert into user(user,password) values(?,?)"; //使用?来占用一个位置// 创建命令发射器PreparedStatement statement = connection.prepareStatement(sql);// 在创建发射器时,要传带?的sql,这样mysql端就会对这个sql进行预编译// 填充sqlScanner 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个?赋值// 执行sqlint 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");// 准备sqlString sql = "select * from user where (user=? and password=?)";// 创建命令发射器PreparedStatement statement = connection.prepareStatement(sql);// 填充sqlScanner 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);// 执行sqlResultSet 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");// 准备sqlString sql = "insert into img(img,iname) values(?,?),(?,?)"; //使用?来占用一个位置// 创建命令发射器PreparedStatement statement = connection.prepareStatement(sql);// 在创建发射器时,要传带?的sql,这样mysql端就会对这个sql进行预编译// 填充sqlFileInputStream 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个?赋值// 执行sqlint 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");// 准备sqlString sql = "select * from img where (id=1)";// 创建命令发射器PreparedStatement statement = connection.prepareStatement(sql);// 执行sqlResultSet 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、执行sqlString 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, "测试项目数据");//执行sqlint 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、执行sqlString 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、执行sqlString 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");// 执行sqlResultSet 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;}@Overridepublic 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.执行sqlResultSet 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.填充sqlfor (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.执行sqlint 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.填充sqlfor (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.执行sqlint 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.填充sqlfor (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.执行sqlResultSet 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.填充sqlfor (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.执行sqlint 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.填充sqlfor (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.执行sqlResultSet 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.填充sqlfor (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.执行sqlResultSet 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);// 执行sqlBasiDao1 basiDao1 = new BasiDao1();basiDao1.basUpdate("update acount set money = money-500 where id = 1"); // 连接2basiDao1.basUpdate("update acount set money = money+500 where id = 2"); // 连接3connect.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.填充sqlfor (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.执行sqlint 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);// 执行sqlBasiDao1 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(建议配置下)
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
minEvictableIdleTimeMillis
connectionInitSqls 物理连接初始化的时候执行的sql
exceptionSorter 根据dbType自动识别 当数据库抛出一些不可恢复的异常时,抛弃连接
filters 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat日志用的filter:log4j防御sql注入的filter:wall
proxyFilters 类型是List,如果同时配置了filters和proxyFilters,是组合关系,并非替换关系

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

  1. java 前端工作内容_java前端、java后端、java全栈工作主要内容是什么?哪个薪资高?...

    摘要 最近,听了一场关于java全栈工程师职位的简介说明,里面很清楚的说明了一下前端,后端,全栈都是做什么工作的.其实,想做这个行业,就应该了解职能以及技能需求,这样学习才能更高效.我知道一些刚刚入行 ...

  2. 全栈工程师薪水_2020 Java 全栈工程师进阶路线图,不可错过

    技术更新日新月异,对于初入职场或者刚开始学习的同学来说,经常会困惑该往那个方向发展,这一点所有刚开始学习的人基本都有这个体会的. 刚开始学习 Java 那会,最大的问题就是不知道该学什么,以及学习的顺 ...

  3. Java全栈学习路线-拭去心尘

    一.JavaSE(熟练使用IDEA(私下学习可以,但进公司做项目不要使用破解版)和Eclipse) 辅助语言:C++(面向对象语言,偏向底层,语法和Java类似,我个人偏好C++为辅助语言)+pyth ...

  4. 2019 Java 全栈工程师进阶路线图,一定要收藏

    技术更新日新月异,对于初入职场的同学来说,经常会困惑该往那个方向发展,这一点松哥是深有体会的. 我刚开始学习 Java 那会,最大的问题就是不知道该学什么,以及学习的顺序,我相信这也是很多初学者经常面 ...

  5. Java全栈工程师学习

    Java全栈看似内容庞杂,只要掌握方法成功离我们很近,话不多说,上硬货! Web后端架构 后端进阶第一步,先把Web架构相关的技术学好吧,因为之前大家都做过Java Web项目,想必对这块内容还是比较 ...

  6. 各大互联网公司面经分享:Java全栈知识+1500道大厂面试真题

    这篇文章给大家分享一下我遇到的一些质量较高的面试经历,具体经过就不多说了,就把面试题打出来供各位读者老哥参考如有不全的地方,各位海涵. 猿辅导 八皇后问题 求二叉树的最长距离(任意两个节点的路径 中最 ...

  7. 各大厂面经分享:Java全栈知识+1500道大厂面试真题

    这篇文章给大家分享一下我遇到的一些质量较高的面试经历,具体经过就不多说了,就把面试题打出来供各位读者老哥参考如有不全的地方,各位海涵. 猿辅导 八皇后问题 求二叉树的最长距离(任意两个节点的路径 中最 ...

  8. Java全栈体系路线(总结不易,持续更新中)

    文章目录 Java全栈工程师 <font color=orange>Java基础 基础语法 面向对象 工具类 集合框架 序列化 反射机制 注解 文件处理 设计模式 视频教程 文档教程 练习 ...

  9. 1. JAVA全栈知识体系--- Java基础

    1. JAVA全栈知识体系- Java基础 文章目录 1. JAVA全栈知识体系--- Java基础 1.1 语法基础 面向对象特性? a = a + b 与 a += b 的区别 3*0.1 == ...

最新文章

  1. 培训第二弹:全国大学生智能汽车竞赛百度竞速组预告
  2. linux视频学习6(mysql的安装/)
  3. (包含重力矢量)Pygame粒子模拟
  4. MySQL存储过程语句(if,while)的使用
  5. qpython3第三方库_Python第三方库的安装 --Python3
  6. 官宣!华为主导首个软件定义摄像机国际标准诞生
  7. 知识分享:如何用插件实现操作校验
  8. 【动态规划笔记】区间dp:合并果子
  9. 创建显示特殊文档的视图:$FormulaClass的奥秘
  10. c语言学习建议,学习c语言的建议
  11. Java实现18位身份证号码的校验码计算校验
  12. 【元胞自动机】基于元胞自动机多车道信号交叉口仿真含Matlab源码
  13. Linksys E 刷Tomato shibby
  14. 平面设计基础(PS)知识点总结
  15. 再见SNDA,在离职之后
  16. 网站数据被入侵怎么办 如何防止网站数据库被攻击 被篡改
  17. /etc/passwd,/etc/shadow文件详解,及密码复杂度设置
  18. No provisioned iOS devices are available with a compatible iOS version.
  19. 使用云祺虚拟机备份软件备份H3C CAS 虚拟机
  20. 量子前沿英雄谱|IonQ联合创始人兼首席技术官Jungsang Kim

热门文章

  1. 考研英语长难句分析方法
  2. 第16届北京国际体育电影周正式启动
  3. nginx -s reload原理
  4. 什么是BSP票和BSP电子票
  5. GDI+——使用Graphics类绘制基本图形
  6. babylon.js
  7. ijkplayer的简单使用
  8. CCNA 教学 大纲 50小时 CCNA www.91xueit.com 视频教学下载 同样的CCNA不一样的感觉
  9. 私有化的IM即时通讯平台,企业首选的沟通工具
  10. 苹果MAC电脑OS系统使用Cisco AnyConnect教程