昨天学习了JDBC,连接数据库的操作,今天对JDBC做一个整合理解
JDBC的简述:
JDBC是Java用来操作数据库的工具,实际就是不同的数据库实现了Java的接口,我们可以理解为:Java规范了接口,数据库实现了接口
作用:通过Java代码操作数据库
这里就是简述一下JDBC,如果大家如果想看的更详细,可以去看看大佬们的详述,我这边还是想直接敲代码带大家理解一下JDBC的使用,和通过反射来封装方法,使我们可以方便的插入和查询
JDBC的过程:
在敲代码之前,我们需要理解一下,我们是如何实现Java与数据库的连接(我这里的数据库是mysql )
通过这个图我觉得大家应该理解了java是如何对数据库进行操作的,我这里再详细解释一下
我们可以将整个过程看成是一个货车拉货的过程
我们知道java是面向对象的,所以我们需要将里面的每一个组件都变成对象
首先,我们需要知道我们要连接哪个数据库厂商,因为有mysql,mgdb等等数据库
第二步,我们需要创建一个连接通道,将java与数据库打通,我们的数据交流在该通道流动,这就是拉货的马路
第三步,我们需要获取一个货车,也就是执行者对象
第四步,我们要告诉货车,你要拉什么货,然后让他去拉货吧,也就是你需要执行什么sql语句,并开始执行
第五步,货车回来了,满载而归,取得了货物,也就是我们获取了我们执行sql语句后返回的数据
最后,我们需要把本次工作的东西全部拆除,卸磨杀驴,其实就是释放资源
JDBC的代码执行:
我们清楚了JDBC的执行过程,下面我们开始编写代码来实现,我们要先有一个数据库和表哈,我这里给大家准备了一个:
CREATE DATABASE IF NOT EXISTS test;
USE test;
CREATE TABLE IF NOT EXISTS t_user (
id INT PRIMARY KEY AUTO_INCREMENT,
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
password VARCHAR(64) NOT NULL COMMENT '密码',
nickname VARCHAR(20) NOT NULL COMMENT '昵称'
);
INSERT INTO t_user (account, password, nickname) VALUES ('root', '123456', '经理'), ('admin', '666666', '管理员');
哦对了,我们需要导入JDBC的jar包的,我不知道大家的IDEA的版本是多少,所以我这里就按我的举例了,首先我们要下载jar包,连接如下:
链接:JDBC的jar包
提取码:1111
我们首先需要在项目下创建一个lib文件夹,然后将两个jar包拖入
我这里截不了图,右键两个jar包,选择最下面有一个添加到库中,执行就好,出现可以展开,证明添加成功了,大家不懂可以去看看大佬们的方法
下面是Java中的执行过程,让我们牢记这6步:
1.注册驱动
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
通过反射我们获取mysql的class对象,为什么执行一个获取class对象就能注册成功呢,我们需要知道,通过反射获取class对象时,会进行加载的过程,执行静态代码块,我们看一下Driver的源码
从源码可以看到,在静态代码块中实际上执行的,是new了一个Driver对象,所以我们可以获取到相应的对象
2.获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
下面获取调用连接的方法,格式是这样的:
//jdbc:数据库厂商名://ip地址:prot/数据库名
//jdbc:mysql://127.0.0.1:3306/atguigu
3.创建执行者对象
Statement statement = connection.createStatement();
我们通过连接来获取一个执行对象,也就是小车
4.执行sql语句
String sql = "select * from t_user;";
ResultSet resultSet = statement.executeQuery(sql);
sql语句中我们是查询了该表的所有信息,然后告诉卡车,你要拉什么货,就是你要查询的内容,然后返回查询结果
这里我说一下executeQuery,它接收的是查询语句 ,DQL
如果我们执行的是非DQL也就是增删改,我们需要使用executeUpdate这个方法
5.获取数据集并解析
while (resultSet.next()){
int id = resultSet.getInt("id");
String account = resultSet.getString("account");
String password = resultSet.getString("password");
String nickname = resultSet.getString("nickname");
System.out.println(id + "--" + account + "--" + password + "--" + nickname);
}
这里我觉得是和集合很像的,集合我们遍历是使用迭代器,然后利用迭代器的光标.next去获取集合的对象,这里的.next也是一样的,我们循环获取数据表的数据,get我就不多说了,字面的意思,数据库的字段类型是什么,那么这里就要获取对应的数据类型
6.关闭资源
resultSet.close();
statement.close();
connection.close();
最后我们释放资源,我们要记住我们创建了什么
一个通道,一个执行者(卡车),一个数据集(货物)
思考:
我们看这个过程实际上是有问题的
第一:不安全,会被sql注入,我们能不能规定,让我们自定义sql语句
第二:代码复用率太高了,我们不能执行一次数据就连接然后一顿操作,最后关闭,大货车来来回回跑,也会爆胎,效率太低,我们能不能让他只跑一次,装一堆数据回来
第三:连接时间的利用率低,我们将连接分为三个阶段,连接,使用,断开
如果连接+断开<使用,这样利用率高,如果连接+断开>>使用,利用率低,很显然我们属于第二种,所以有没有一种操作,可以让我们复用连接呢?
下面我们引入两个东西,连接池和自定义sql,也就是预编译
预编译:
预编译和上面的区别就是,我们需要先规定sql语句的格式,通过对占位符的赋值来进行操作,我们可以理解为传参,上代码:
import java.sql.*;
public class a {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
//1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2.获取连接
Connection connection = DriverManager
.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root", "123456");
//3.创建statement
Statement statement = connection.createStatement();
//4.发送sql语句,获取返回结果
String sql = "insert into t_user (account,password,nickname) values(?,?,?)";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//5.占位符赋值
preparedStatement.setObject(1, "小红");
preparedStatement.setString(2, "789");
preparedStatement.setString(3, "服务员");
//6.发送SQL语句
preparedStatement.executeUpdate();
//7.关闭资源
preparedStatement.close();
statement.close();
connection.close();
//6.关闭资源
preparedStatement.close();
statement.close();
connection.close();
}
}
前面我不去说了,没有差距,只有一个地方有了变化,让我们看第四步,发送sql语句的部分
之前我们是写死的sql语句,现在我们改为占位符,我们需要记住这个api,prepareStatement
我们获取prepareStatement这个对象,将预编译的sql语句传入,下面我们要对占位符进行传参,数据库中是1开头的,而不是0,我们需要记住,然后给对应的问号赋值就好了
1,小红 ----》 account = 小红
2,789 ----》 password = 789
3,服务员 ---》 nickname = 服务员
然后让货车开走,executeUpdate,因为我们这次是非DQL语句,要用executeUpdate
连接池:
我们需要知道连接池要干什么:
通常我们连接过后然后操作,最后释放掉,这个连接能不能提前获取,获取很多,我们需要的时候直接去拿,释放改成回收,让它重新回到池中
下面我不是JDBC的内容,说一下池的概念:
按我的理解,池和缓存其实是一样的,就是把东西先获取,然后存到内存里,需要的时候直接调用,还记得IO的缓存机制吗,不就是把数据统一放到数组中,将整个数组写入吗,按我的理解,池是相反的,我们先获取数据的整个数组,然后将数组里的数据,一个一个读取出来。
下面就是连接池的具体实现,我使用的连接池是Druid,对应的jar包在上面的链接里
在创建连接池之前我们需要知道properties配置文件
我这里不去讲硬编码的方式去创建连接池了,代码多而且灵活度非常低,所以使用properties配置文件,去读取配置文件的内容,然后进行连接创建,上代码:
druid.properties
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbc:mysql://127.0.0.1:3306/atguigu
切记切记,如果你是jdk版本和idea版本很高,一定要把配置文件放在src目录下,不要放在你的软件包下,不然会出错,本人血泪,这个问题卡了半天,还以为代码错误
JDBCutils类
package com.atguigu.api.utils;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
/*内部包含一个连接池对象,并且对外提供获取连接和回收连接的方法
实现:
属性 连接池对象【实例化一次】
单例模式
static{
静态代码块,全局调用一次
}
方法:
对外提供连接的方法
回收外部传入连接方法
利用线程本地变量,存储连接信息,确保一个线程的多个方法可以获取同一个connection
优势:事务操作的时候 service 和 dao 属于同一个线程,不用再传参数了
可以调用getConnection自动获取的是相同的连接池
*/
public class JdbcUtilsV2 {
private static DataSource dataSource = null;
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
static {
//初始化连接池对象
InputStream ips = Thread.currentThread().getContextClassLoader().getResourceAsStream("druid.properties");
Properties properties = new Properties();
try {
properties.load(ips);
} catch (IOException e) {
throw new RuntimeException(e);
}
try {
dataSource = DruidDataSourceFactory.createDataSource(properties);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
//对外提供获取连接的方法
public static Connection getConnection() throws SQLException {
//线程本地变量中是否存在
Connection connection = threadLocal.get();
//第一次没有
if (connection == null) {
//线程本地变量没有,连接池获取,返回本地变量的连接
connection = dataSource.getConnection();
threadLocal.set(connection);
}
return connection;
}
public static void freeConnection() throws SQLException {
Connection connection = threadLocal.get();
if (connection != null){
threadLocal.remove();//清空线程本地变量数据
connection.setAutoCommit(true);//事务状态回到默认状态
connection.close();//回收到连接池
}
}
}
我这里是封装好的连接池,我给大家解释一下
我们需要知道怎么获取配置文件,首先创建配置文件对象
Thread.currentThread().getContextClassLoader():获取当前线程的类加载器
getResourceAsStream()
是java中用于加载资源文件的方法之一。
它是 ClassLoader
和 Class
类提供的方法,用于从类路径(ClassPath)中获取资源文件并返回一个输入流 (InputStream
)
我们只需要知道它传入一个文件路径,返回的是一个输入流就好
Properties的 load()方法的用法是传入一个输入文件流的对象,然后load方法可以去读取里面的数据,也就是说可以把数据从文件中放到内存中展示。
这里就是获取配置文件的数据
而连接池的创建非常简单,我们需要先获取DruidDataSourceFactory对象,然后将读取到的数据传入进行数据库连接就好,这时候连接池就已经创建好了
dataSource是一个接口,大家如果想了解很深,可以看看下面的文章
DataSource数据源_datasource数据源配置-CSDN博客
我在上面定义了静态变量来指向本次的连接,因为我们需要规定,我们要访问的是同一个连接对象
这是一个讲解线程本地变量的文章
并定义了一个静态变量指向连接池,方便获取
这是一个提供连接的方法,首先我们判断线程本地变量是否存在,如果不存在,我们去池中获取,方法很简单:池对象.getConnection()返回一个连接
然后将该连接对象传入线程本地变量,最后返回此次的连接对象
这是一个释放方法,还是一个套路,首先获取线程本地变量是否存在,如果存在的话我们将其清空,然后回收该连接,并将事务状态变成true,如果没有开启事务也不会影响,主要是为了回归默认
连接池的SQL包装:
package com.atguigu.api.utils;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
/*
* 封装两个方法,一个简化增删改,非DQL
* 一个简化查询,DQL*/
public class BaseDao {
/*
* 封装简化非DQL语句
* sql 带占位符的sql语句
* params 占位符的值
* return 执行影响的行数*/
public static int executeUpdate(String sql, Object... params) throws SQLException {
Connection connection = JdbcUtilsV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
//可变参数可以当数组使用
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i + 1, params[i]);
}
int rows = preparedStatement.executeUpdate();
preparedStatement.close();
if (connection.getAutoCommit()) {
//没有开启事务,正常回收连接
JdbcUtilsV2.freeConnection();
}
return rows;
}
public static <T> List<T> executeQuery(Class<T> clazz, String sql, Object... params) throws Exception {
List<T> list = new ArrayList<>();
//获取连接
Connection connection = JdbcUtilsV2.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i + 1, params[i]);
}
//结果集解析
ResultSet resultSet = preparedStatement.executeQuery();
//获取列信息的对象
ResultSetMetaData metaData = resultSet.getMetaData();
//获取列信息的数量
int columnCount = metaData.getColumnCount();
while (resultSet.next()) {
Constructor<T> declaredConstructor = clazz.getDeclaredConstructor();
T t = declaredConstructor.newInstance();
for (int i = 1; i <= columnCount; i++) {
//列的属性值
Object value = resultSet.getObject(i);
//列的属性名
String propertyName = metaData.getColumnLabel(i);
//反射给对象的属性值赋值
Field fileld = clazz.getDeclaredField(propertyName);
fileld.setAccessible(true);
fileld.set(t, value);
}
list.add(t);
}
resultSet.close();
preparedStatement.close();
if (connection.getAutoCommit()) {
JdbcUtilsV2.freeConnection();
}
return list;
}
}
我们在指向sql语句的时候,我们也是要写很多重复代码的,下面我将sql的执行部分,进行了封装,我来讲一下思路
非DQL语句的方法
我们想一下我们之前的sql执行
我们传入了两部分数据,一个是预编译的sql语句,一个是占位符的赋值
第一个参数很好理解,我们看第二部分,占位符的赋值
这一部分又被拆成两份,第一个是哪个占位符,第二个是对应占位符的数据
我觉得它很像一个数据结构,没错是数组
我们将占位符看成数组的索引,将传入的数据看成数组索引对应的数据,也就是说我们可以根据索引获取对应位置的值,那么我可以直接传入一个数组,就可以动态直接非DQL操作了
我们再看此方法,sql语句没什么好说的,Object... params这是java参数的特殊语法,相当于传入了一个数组,我们把params当初数组看就好,下面的操作就大差不差了,从连接池中获取连接对象,然后告诉货车你要拉什么货(sql参数),下面就是为对应占位符赋值了,那么直接使用循环动态获取,我们唯一要注意的是,占位符是从1开始的,所以赋值时候 i 要+1,数组不变,索引0是占位符1的数据,赋值结束,发车,将执行对象释放
最后判断一下该连接是否处于自动提交状态,也就是事务模式是否开启,如果没有开启,调用刚刚包装好的方法,进行回收。我需要解释一下为什么这里要加入一个判断开没开启事务,因为在整个业务中,我们需要业务层和控制层,而整个事务的提交,我们去统一的交给业务层去做
如果我们在这里就将本次操作的连接释放,那么下面的操作,就不再是相同的连接对象了
DQL查询的方法
我们先来想一下查询是不是获取的是行信息,而行信息,是由列信息组成的
我们转换到java的面向对象,我们是不是可以这样想,一行就是一个对象,而列信息就是该对象的属性,而整张表是不是一个集合,存放的都是相同的对象
我的思路是,我不直接对数据表进行查询了,我构建一个表可以吧,也就是集合,我将所有对象也就是行数据,整合到一个集合中,最后返回这个集合,对该集合进行查询,不就可以了,而且我们是直接对内存进行操作,速度是非常快的,那我们开始实现一下
这是本次包装的方法,参数比非DQL方法多了一个,我传入了一个Class对象,为什么,因为我们可以直接反射获取该对象的所有属性,并规定了集合存储的对象类型,这是泛型。
下面我创建一个存储该对象的集合,也就是本方法返回的内容
下面和非DQL的方法操作一样,我不多说了,给对应位置的占位符赋值,然后发车
.getMetaData()是获取结果集,列的信息,大家可以看看下面这个文章,讲述了几种方法,我就不去讲了,直接说干什么了
java数据库编程(8)ResultSetMetaData_resultsetmetadata.getcolumncount-CSDN博客
metaData.getColumnCount()获取列的数量,也就是有多少属性值。
使用while循环遍历数据表,获取每一行的数据,我们的目的是将行数据变成对应的对象,存入到集合中,所以,利用反射获取其无参数构造函数,创建该行数据的对象
下面我们需要为该对象的属性赋值,我们通过metaData.getColumnCount()获取到列的数量也就是属性的数量,所以我们直接开始循环遍历,获取该行的每个列名和该列表对应的数据,并通过反射为此行对象进行属性赋值,最后将该行对象添加到集合中
示例
本次示例是通过封装的方法对数据库的两个用户进行转账功能,代码如下:
package com.atguigu.api.utils;
import org.junit.Test;
import java.sql.Connection;
public class BankService {
@Test
public void start() throws Exception {
transfer("ergouzi","lvdandan",500);
}
public void transfer(String address, String subAddress, int money) throws Exception {
BankDao bankDao = new BankDao();
Connection connection = JdbcUtilsV2.getConnection();
try {
//开启事务
//关闭事物提交
connection.setAutoCommit(false);
//执行数据库动作
//执行数据库动作
bankDao.add(address,money);
System.out.println("==========");
bankDao.sub(subAddress,money);
bankDao.display();
//事务提交
connection.commit();
}catch (Exception e) {
//事物回滚
connection.rollback();
//抛出异常
throw e;
}finally {
JdbcUtilsV2.freeConnection();
}
}
}
测试程序中,我执行了转账的这一个行为,传入三个参数,转账人,收款人,转账的金额,涉及到转账那么一定是一个事务,也就是转账人的金额减少,收款人的金额增加,如果不是一个事务的话,那么会出现一方没变一方增加的情况。
那么开始对数据库的操作,首先需要获取连接,调用封装的连接池,获取一个共用连接,关闭自动提交,开启事务执行增加金额和减少金额的方法,查询数据表,最后提交,如果事务失败,会进行回滚,事务结束后,进行回收。
下面我们看控制层
package com.atguigu.api.utils;
import java.util.List;
public class BankDao {
/*
* 加钱的数据库操作方法
* account 加钱的行号
* money 加钱的金额
* */
public void add(String account,int money) throws Exception {
//3.编写SQL语句结果
String sql = "update t_bank set money = money + ? where account = ?;";
int i = BaseDao.executeUpdate(sql, money, account);
System.out.println("加钱成功" + i);
}
/*
* 减钱的数据库操作方法
* account 减钱的行号
* money 减钱的金额
* */
public void sub(String account,int money)throws Exception{
//3.编写SQL语句结果
String sql = "update t_bank set money = money - ? where account = ?;";
int i = BaseDao.executeUpdate(sql, money, account);
System.out.println("减钱成功" + i);
}
public void display() throws Exception {
List<User> users = BaseDao.executeQuery(User.class, "select * from t_bank;");
for (User user : users){
System.out.println(user);
}
}
}
package com.atguigu.api.utils;
public class User {
public int id;
public String account;
public long money;
@Override
public String toString() {
return "User [id=" + id + ", account=" + account + ", money=" + money + "]";
}
}
我们经过封装后,可以看到代码简洁了很多,下面就是安装封装好的调用就行,就用减钱的方法来说,传入减钱的用户,和金额,首先编写sql语句,然后将sql语句,用户,和金额传入到非DQL方法中,而DQL方法中,我们传入了行数据需要变成的对象,User类,然后传入sql语句,就会返回一个数据库表的集合,对集合进行遍历输出即可。
结果:
成功!
谢谢大家!如果有不对的地方希望大家指出!
标签:JDBC,封装,使用,连接,获取,connection,sql,我们,连接池 From: https://blog.csdn.net/jiangyule/article/details/137325644