JDBC笔记
一、为什么要学习jdbc
- jdbc是Java和数据库之间的必要纽带
java跟数据库之间的通信要通过jdbc来实现 - 数据库层框架底层原理
jdbc是mybatis、hibernate、jpa等应用层框架的底层驱动
二、jdbc的相关概念
2.1 定义
JDBC : Java Database Connectivity || Java连接数据库技术
即,在java代码中,使用JDBC提供的方法,可以发生字符串类型的sql语句到数据库管理软件(Mysql、Oracle等),并且获取语句的执行结果,进而通过java代码实现数据库的crud操作的技术。
jdbc由两部分组成:
1. Java提供的jdbc规范(接口)
2. 各个数据库厂商的实现驱动jar包
2.2 核心api
-
DriverManager
1.将第三方数据库厂商的实现驱动jar包注册到程序中
2.根据数据库连接信息获取connection -
Connection
1.表示数据库建立的连接,可以在连接的对象上,多次执行数据库crud的操作
2.可以获取到statement | preparedstatement | callablestatement对象 -
statement | preparedstatement | callablestatement
1.具体发送sql语句到数据库软件的对象
2.用不同方式发送过程有所不同,最主要掌握preparedstatement!
a. 静态sql语句使用statement
b. 有动态传参的sql语句使用preparedstatement
c. 执行存储过程的sql语句使用callablestatement -
result
1.面对对象思维的产物(抽象成数据库查询的结果集)
2.存储的是查询数据库结果的对象
3.需要我们进行解析才能获取到其中的数据
三、核心api的使用
3.1 使用前配置
- 首先创建一个lib文件夹用于存放要使用的jar包,将jar包复制到该文件夹下
- 将jar包加入到Library中
- 数据库的准备
CREATE DATABASE atguigu;
USE atguigu;
CREATE TABLE t_user(
id INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户主键',
account VARCHAR(20) NOT NULL UNIQUE COMMENT '账号',
PASSWORD VARCHAR(64) NOT NULL UNIQUE COMMENT '密码',
nickname VARCHAR(20) NOT NULL UNIQUE COMMENT '昵称'
);
INSERT INTO t_user(account,PASSWORD,nickname)
VALUES ('root','123456','经理'),('admin','777777','管理员');
3.2 jdbc执行过程
- 注册驱动 依赖的jar包,进行安装
- 建立与数据库的连接connection
- 创建发送sql语句的对象statement
- statement对象发送sql语句并且获取到结果集
- 解析结果集,获取到数据
- 销毁资源,释放connection、statement、resultSet对象
3.3 通过statement查询数据库实例demo
package jdbc.com.demo.statement;
import com.mysql.cj.jdbc.Driver;
import java.sql.*;
/**
* ClassName: StatementQueryPart
* Package: jdbc.com.demo.statement
* Description:
* 使用statement查询t_user表下的全部数据
* @Author 乌冬面
* @Create 2023/10/29 0029 20:13
* @Version 1.0
*/
public class StatementQueryPart {
/**
* todo:
* DriverManager
* Connection
* Statement
* ResultSet
* @param args
*/
public static void main(String[] args) throws SQLException {
//1.注册驱动
/**
* 依赖:
* 驱动版本 8+ com.mysql.cj.jdbc.Driver
* 8以下的版本 com.mysql.jdbc.Driver
*/
DriverManager.registerDriver(new Driver());
//2.获取connection
/**
* java程序要想和数据库建立连接,肯定需要提供连接的数据库的相关信息
* 数据库ip地址
* 数据库端口号
* 账号
* 密码
* 连接数据库的名称
* DriverManager.getConnection(String url,String username,String password)
* url : jdbc:数据库厂商名://ip地址:port/数据库名
* username : 数据库软件账号
* password : 数据库软件密码
*/
Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/atguigu","root","123456");
//3.创建statement
Statement statement = connection.createStatement();
//4.通过statement发送sql语句返回结果集
String sql = "select * from t_user;";
ResultSet resultSet = statement.executeQuery(sql);
//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);
}
//6.销毁资源
resultSet.close();
statement.close();
connection.close();
}
}
程序运行结果如下:
3.4 详解statement实例
3.4.1 注册驱动
在上面的案例中,我们注册驱动时使用的方法为:
DriverManager.registerDriver(new Driver());
这样编写会出现问题,因为在registerDriver()方法调用和Driver类创建的时候都会注册驱动,驱动注册了两次,无端产生了性能的消耗,所以我们希望只注册一次驱动。因为Driver类的静态代码块始终都是要触发的,所以我们选择只触发其静态代码块。
因此,我们必须要了解什么时候静态代码块会被触发:
类加载机制的流程:
- 加载[class文件 -> jvm虚拟机的class对象]
- 连接[验证(检查文件类型) -> 准备(静态变量默认值) -> 解析(触发静态代码块)]
- 初始化[静态属性赋真实值]
触发类加载的情况:
- new 关键字
- 调用静态方法
- 调用静态属性
- 接口 1.8 default默认实现
- 反射
- 子类触发父类
- 程序的入口main
所以我们可以简便的使用如下代码实现注册驱动:
Class.forName("com.mysql.cj.jdbc.Driver");
3.4.2 getConnection()方法
getConnection()是一个重载方法,允许开发者使用不同方式传入数据库连接核心参数。
其核心属性有:
- 数据库软件所在主机的ip地址:localhost | 127.0.0.1
- 数据库软件所在主机的端口号:3306
- 连接的具体数据库:atguigu
- 连接的账号:root
- 连接的密码:123456
- 可选信息
getConnection()有三种实现形式:
/**
* 三个参数
* getConnection(string url,string user,string password)
* string url 数据库软件所在信息,连接具体库,以及其他可选信息
* 语法: jdbc:数据库管理软件名称[mysql\oracle...]://ip地址|主机名:port端口号/数据库名?key=value&key=value...
* 在本机的省略写法 jdbc:mysql://localhost:3306/atguigu == jdbc:mysql:///atguigu 省略本机地址和3306端口号
* 强调:必须是本机,并且端口号为3306才可以省略
* string user root
* string password 123456
*
* 两个参数
* getConnection(string url,properties info)
* string url 同上
* properties info 存储账号跟密码
* properties 类似于Map 只不过key跟value都是用字符串
形式存储的
*
* 一个参数
* getConnection(string url)
* string url 除传入以上的信息以外,在可选信息中加入账号跟密码的信息
*
* 可选信息: 8.0.25以后会自动识别时区,默认使用utf-8版本
* serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=true
*/
以下分别对三种方法都进行实现:
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu","root","123456");
Properties info = new Properties();
info.put("user","root");
info.put("password","123456");
Connection connection1 = DriverManager.getConnection("jdbc:mysql:///atguigu", info);
Connection connection2 = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=123456");
3.4.3 resultSet解析
resultSet类是一个抽象的结果集,包含了从数据库中查询得到的所有结果。
要想进行数据解析,要做两件事情:
- 移动游标指定获取数据行
- resultSet内部包含一个游标,指定当前数据行
- 默认游标指定的是第一行数据之前
- 调用next()方法向后移动一行游标
- next()返回值为 true表示往后还有数据,false表示后面没有数据了
- 获取指定数据行的列数据
resultSet.get类型(列名,可以是别名 | 列下标从1开始)
以下是具体resultSet解析数据的代码:
while (resultSet.next()){
int anInt = resultSet.getInt(1);
String account1 = resultSet.getString("account");
String password1 = resultSet.getString(3);
String nickname = resultSet.getString("nickname");
System.out.println("nickname: " + nickname);
}
3.5 preparedstatement使用
使用statement会发送sql查询会产生的问题
- sql语句需要字符串拼接,比较麻烦
- 只能拼接字符串类型,其他类型的数据类型无法处理
- 可能发生注入攻击(动态值充当了sql语句结构,影响了原有的查询结果)
因此,我们使用preparedStatement来进行动态传入参数的sql语句的执行
/**
* preparedStatement
* 1.编写sql语句结果 不包含动态值部分的语句,动态值部分使用占位符 ? 替代
* 注意: ?只能替代动态值
* 2.创建preparedStatement,并且传入动态值
* 3.动态值 占位符 赋值
* 4.发送sql语句并且返回结果
*
* */
String sql = "SELECT * FROM t_user WHERE account = ? AND password = ?;";
PreparedStatement preparedStatement = connection.prepareStatement(sql);
/*
* 对单独的占位符进行赋值
* 1. 参数1 : index 占位符的位置 从左向右从1开始
* 2. 参数2 : object 占位符的值 可以设置任何类型的数据,避免了我们拼接和类型更加丰富
* */
preparedStatement.setObject(1,account);
preparedStatement.setObject(2,password);
//由于sql的结构已经确定,并且占位符的值都已经赋值成功,所以在执行时不需要传入参数
ResultSet resultSet = preparedStatement.executeQuery();
if (resultSet.next()) {
System.out.println("登录成功");
}else {
System.out.println("登录失败");
}
注意:
如果想在查询之后将结果集存储到一个List集合之中,应该将结果按行的顺序取出,一行之中每一列的数据都存入到一个Map之中,然后将Map一次存入List集合即可
获取列信息的相关方法:
- ResultMetaData resultSet.getMetaData()获取的是当前列的信息对象,可以通过它获取列的名称,根据下角标获取列的数量
- resultSet.getMetaData().getColumnCount() 获取当前结果集列的数量
- resultSet.getMetaData().getColumnLabel(i)根据下角标i获取到列的别名,如果没有写别名,则获取列的名称,推荐这种方式
- resultSet.getMetaData().getColumnName(i)根据下角标i获取到列的名称,不会获取别名
四、jdbc拓展
4.1 主键回显
主键回显是指:在对表数据进行插入等操作时,想要获取到表自增长的主键的值,通过主键回显的方式,来获取到该值。
具体步骤
- 首先要在创建preparedStatement对象时,传入一个PreparedStatement.RETURN_GENERATED_KEYS参数,告知其带回主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
- 取回主键是在preparedStatement对象execute之后,通过PreparedStatement类的getGeneratedKeys()方法,该方法返回一个ResultSet对象,获取ResultSet结果集的方法与之前相同,先用next()方法使其指针移动一位,然后用get()发方法获取主键值
ResultSet generatedKeys = preparedStatement.getGeneratedKeys(); generatedKeys.next(); int id = generatedKeys.getInt(1); System.out.println("返回的主键为:" + id );
总体demo
@Test
public void test_01() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?user=root&password=123456");
String sql = "insert into t_user(account,password,nickname) values(?,?,?)";
//要想使用主键回显,要在创建preparedStatement对象时,传入一个PreparedStatement.RETURN_GENERATED_KEYS参数,告知其带回主键
PreparedStatement preparedStatement = connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, "test7");
preparedStatement.setString(2, "1020");
preparedStatement.setString(3, "跟韩剧");
int rows = preparedStatement.executeUpdate();
if (rows > 0){
System.out.println("数据插入成功");
//可以获取回显的主键
ResultSet generatedKeys = preparedStatement.getGeneratedKeys();
generatedKeys.next();
int id = generatedKeys.getInt(1);
System.out.println("返回的主键为:" + id );
} else {
System.out.println("数据插入失败");
}
preparedStatement.close();
connection.close();
}
4.2 批量插入数据优化
按照传统的插入方式进行批量操作时,每次操作都要重新执行一次execute操作,运行的效率很慢。
因此我们在进行大量数据的插入操作时使用批量操作
使用步骤
-
在url中要在地址?之后加上rewriteBatchedStatements=true语句,其作用是允许批量插入操作
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?rewriteBatchedStatements=true&user=root&password=123456");
-
在编写sql语句时,一定要使用values而不能用value,而且sql语句最后不能添加;
String sql = "insert into t_user(account,password,nickname) values(?,?,?)";
-
在循环体内执行了set操作之后使用addBatch()方法,将当前插入的数据追加到values之后
for (int i = 0; i < 10000; i++) { preparedStatement.setString(1, "dd" + i); preparedStatement.setString(2, "ww" + i); preparedStatement.setString(3, "xiaohong" + i); preparedStatement.addBatch(); //不执行,追加到values的后边 }
-
当数据全部插入完成使用executeBatch()方法执行批量插入
preparedStatement.executeBatch();//执行批量操作
总体demo
@Test
public void test_02() throws ClassNotFoundException, SQLException {
Class.forName("com.mysql.cj.jdbc.Driver");
//允许批量插入操作
Connection connection = DriverManager.getConnection("jdbc:mysql:///atguigu?rewriteBatchedStatements=true&user=root&password=123456");
String sql = "insert into t_user(account,password,nickname) values(?,?,?)";
long start = System.currentTimeMillis();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < 10000; i++) {
preparedStatement.setString(1, "dd" + i);
preparedStatement.setString(2, "ww" + i);
preparedStatement.setString(3, "xiaohong" + i);
preparedStatement.addBatch(); //不执行,追加到values的后边
}
preparedStatement.executeBatch();//执行批量操作
long end = System.currentTimeMillis();
System.out.println(end - start);
preparedStatement.close();
connection.close();
}
五、druid连接池
5.1 使用连接池的原因
在传统的获取连接的方式中,每次在使用jdbc操作前都要先建立一个数据库的连接,在操作完成之后再销毁连接。当操作量特别大时,这样的方式不仅繁琐,而且每次都建立销毁连接的开销很大。因此,提出连接池,是为了将连接复用起来,提高效率。
5.2 druid连接池的使用
druid连接池的使用有两种方式:软连接和硬链接的方式,下面将依次介绍
1 硬链接方式
//1.创建一个连接池对象
DruidDataSource dataSource = new DruidDataSource();
//2.设置连接池参数[必须/非必须]
//必须 连接数据库驱动类的全限定符[注册驱动] | url | user | password
//非必须 初始化连接数、最大连接数
dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
dataSource.setUrl("jdbc:mysql://localhost:3306/atguigu?user=root&password=123456");
dataSource.setUsername("root");
dataSource.setPassword("123456");
dataSource.setInitialSize(5); //设置初始化连接数
//3.获取连接
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println(connection);
//4.回收连接
connection.close();
2 软连接的使用,首先要创建一个properties文件,在文件中存储创建连接池的具体参数信息
driverClassName=com.mysql.cj.jdbc.Driver
username=root
password=123456
url=jdbd:mysql:///atguigu
注意:在properties文件中的各种key关键字的名称是特定的,具体可以使用参考文档查看,不要写错了
然后要获取到这些参数信息,让然后创建连接池
//1.读取外部配置文件 Properties
Properties properties = new Properties();
//src下的文件可以使用类加载器获取
InputStream ips = DruidUse.class.getClassLoader().getResourceAsStream("druid.properties");
properties.load(ips);
//2.使用连接池的工具类的工厂模式,创建连接池
DataSource dataSource = DruidDataSourceFactory.createDataSource(properties);
//3.获取连接
Connection connection = null;
try {
connection = dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
}
System.out.println(connection);
//4.回收连接
connection.close();
六、工具类的封装
6.1 非DQL的封装
public int executeUpdate(String sql, Object... params) throws SQLException {
//获取连接
Connection connection = JdbcUtils.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();
//没有开启事务,则由dao层关闭connection
if (connection.getAutoCommit()){
JdbcUtils.closeConnection();
}
return rows;
}
6.2 DQL的封装
/**
* 使用泛型来返回数据
* <T>声明一个泛型,不确定类型
* 确定泛型 使用参数Class<T> clazz
* 使用反射技术为属性赋值
* @param clazz
* @param sql
* @param params
* @return
* @param <T>
* @throws SQLException
*/
public <T> List<T> executeQuery(Class<T> clazz,String sql, Object... params) throws SQLException, InstantiationException, IllegalAccessException {
Connection connection = JdbcUtils.getConnection();
PreparedStatement preparedStatement = connection.prepareStatement(sql);
for (int i = 0; i < params.length; i++) {
preparedStatement.setObject(i + 1, params[i]);
}
ResultSet resultSet = preparedStatement.executeQuery();
List<T> list = new ArrayList<>();
while (resultSet.next()) {
//调用类的无参构造函数实例化对象
T t = clazz.newInstance();
for (int i = 0; i < resultSet.getMetaData().getColumnCount(); i++) {
//获取对象的属性名
String columnName = resultSet.getMetaData().getColumnLabel(i + 1);
//获取对象的属性值
Object value = resultSet.getObject(i + 1);
try {
//使用反射给对象赋值
Field field = clazz.getDeclaredField(columnName);
//属性可以设置,打破private的修饰限制
field.setAccessible(true);
//赋值方法 , t->要赋值的对象(如果属性是静态的,第一个参数可以为null) ,value->具体的属性值
field.set(t, value);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
list.add(t);
}
return list;
}