目录
一、JDBC概念
JDBC全称Java DataBase Connectivity,即Java数据库连接,也就是用Java来操作数据库。
JDBC的本质其实是官方(sun公司)定义的一套操作所有关系型数据库的规则,即接口。
对于各个数据库厂商来说,它们可以实现这套接口,并提供数据库驱动jar包。
对于开发者来说,可以使用这套接口(JDBC)编程,真正执行的代码是具体驱动jar包中的实现类。
二、JDBC快速入门
步骤
- 导入驱动jar包
这里使用的是mysql-connector-java-8.0.11.jar
1.复制mysql-connector-java-8.0.11.jar到项目的libs目录下
2.右键–>Add As Library
- 注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
如果是老版本的驱动,则写com.mysql.jdbc.Driver
- 获取数据库连接对象 Connection
Connection conn =DriverManager.getConnection("jdbc:mysql://localhost:3306/数据库名", "账户", "密码");
- 定义sql
- 获取执行sql语句的对象 Statement
- 执行sql,接受返回结果
- 处理结果
- 释放资源
代码
package com.zhu.jdbc;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/*
JDBC快速入门
*/
public class JdbcDemo1 {
public static void main(String[] args) throws Exception {
//1. 导入驱动jar包
//2.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//3.获取数据库连接对象
Connection conn =DriverManager.getConnection("jdbc:mysql://localhost:3306/test2", "root", "root");
//4.定义sql语句
String sql = "update user set password = 888 where id = 2";
//5.获取执行sql的对象 Statement
Statement stmt = conn.createStatement();
//6.执行sql
int count = stmt.executeUpdate(sql);
//7.处理结果
System.out.println(count);
//8.释放资源
stmt.close();
conn.close();
}
}
执行前数据库的情况(test2数据库中的user表):
执行后报错:
Wed Apr 08 15:02:14 CST 2020 WARN: Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.
Exception in thread "main" java.sql.SQLException: The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone support.
原因是新版的驱动需要加上时区信息。
解决办法是在连接字符串后面加上?serverTimezone=UTC
其中UTC是统一标准世界时间。
完整的连接字符串示例:
jdbc:mysql://localhost:3306/test2?serverTimezone=UTC
或者还有另一种选择:
jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=UTF-8
这个是解决中文乱码输入问题,当然也可以和上面的一起结合:
jdbc:mysql://127.0.0.1:3306/test2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
修改后执行代码,表发生如下变化,可见JDBC的代码生效了。
三、详解JDBC的各个对象
既然JDBC是一套约定俗成的规则,我们要面向这个规则来编写代码,那么了解这个规则中的各个对象就显得至关重要了。下面将列举JDBC的重要对象,讲解其功能和用法。
1. DriverManager:驱动管理对象
两个功能:注册驱动、获取数据库连接
- 注册驱动:告诉程序该使用哪一个数据库驱动jar
方法:static void registerDriver(Driver driver):注册给定的驱动程序 。
写代码使用:
Class.forName("com.mysql.cj.jdbc.Driver");
通过查看源码发现:在com.mysql.cj.jdbc.Driver类中存在静态代码块:
也就是说这个类一旦被加载进内存,这段静态代码块就会执行,实现了注册驱动的操作。
注意:mysql5之后的驱动jar包可以省略注册驱动的步骤。因为jar包中的文件已经定义了这个Driver类,会在你手动注册前先帮你注册掉。
- 获取数据库连接:
方法:static Connection getConnection(String url, String user, String password)
参数:
url:指定连接的路径
语法:
jdbc:mysql://ip地址(域名):端口号/数据库名称
例子:
jdbc:mysql://localhost:3306/db3
细节:如果连接的是本机mysql服务器,并且mysql服务默认端口是3306,则url可以简写为:
jdbc:mysql:///数据库名称
2. Connection:数据库连接对象
两个功能:
- 获取执行sql 的对象
Statement createStatement()
或
PreparedStatement prepareStatement(String sql) - 管理事务:
开启事务:setAutoCommit(boolean autoCommit) :调用该方法设置参数为false,即关闭自动提交、开启事务。
提交事务:commit()
回滚事务:rollback()
3. Statement:执行sql的对象
功能:执行sql语句
方法:
- boolean execute(String sql) :可以执行任意的sql (不常用)
- int executeUpdate(String sql) :执行DML(insert、update、delete)语句、DDL(create,alter、drop)语句
executeUpdate的返回值:影响的行数,可以通过这个影响的行数判断DML语句是否执行成功 返回值>0的则执行成功,反之,则失败。
- ResultSet executeQuery(String sql) :执行DQL(select)语句,返回结果集对象
例子:
account表 添加一条记录
代码:
Statement stmt = null;
Connection conn = null;
try {
//1. 注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2. 定义sql
String sql = "insert into account values(null,'王五',3000)";
//3.获取Connection对象
conn = DriverManager.getConnection("jdbc:mysql:///db3", "root", "root");
//4.获取执行sql的对象 Statement
stmt = conn.createStatement();
//5.执行sql
int count = stmt.executeUpdate(sql);//影响的行数
//6.处理结果
System.out.println(count);
if(count > 0){
System.out.println("添加成功!");
}else{
System.out.println("添加失败!");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}finally {
//stmt.close();
//7. 释放资源
//避免空指针异常
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. ResultSet:结果集对象,封装查询结果
主要的两个方法:
- boolean next(): 游标向下移动一行,判断当前行是否是最后一行末尾(是否有数据),如果是,则返回false,如果不是则返回true
- getXxx(参数):获取数据
* Xxx:代表数据类型 如: int getInt() , String getString()
* 参数:
1. int:代表列的编号,注意是从1开始编号的 如: getString(1),表示获取当前行第一列的字符串值
2. String:代表列名称, 如: getDouble(“balance”)
使用步骤(以上面的表为例):
1. 游标向下移动一行
2. 判断是否有数据
3. 获取数据
//循环判断游标是否是最后一行末尾。
while(rs.next()){
//获取数据
int id = rs.getInt(1);
String name = rs.getString("name");
double balance = rs.getDouble(3);
System.out.println(id + "---" + name + "---" + balance);
}
案例
若现在有一个product表:
现在需要定义一个方法,查询product表的数据将其封装为对象,然后装载集合,返回。
1. 定义Product类
2. 定义方法 public List findAll(){}
public List<Product> findAll() {
Connection conn = null;
String sql = null;
Statement stmt = null;
ResultSet rs = null;
List<Product> list = null;
try {
// 1.注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 2.获取连接
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test2?serverTimezone=UTC", "root", "root");
// 3.定义sql
sql = "select * from product";
// 4.获取执行sql的对象
stmt = conn.createStatement();
// 5.执行sql
rs = stmt.executeQuery(sql);
// 6.遍历结果集,封装对象,装载集合
Product product = null;
list = new ArrayList<Product>();
while(rs.next()) {
// 获取数据
int id = rs.getInt("pid");
String name = rs.getString("pname");
Double price = rs.getDouble("price");
Date date = rs.getDate("pdate");
int cno = rs.getInt("cno");
// 创建product对象并赋值
product = new Product();
product.setId(id);
product.setName(name);
product.setPrice(price);
product.setDate(date);
product.setCno(cno);
// 装载集合
list.add(product);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return list;
}
运行可正确查出表中数据。
5. PreparedStatement:执行sql的对象
-
SQL注入问题:在拼接sql时,有一些sql的特殊关键字参与字符串的拼接。会造成安全性问题
例如随意输入用户,并输入密码:a’ or ‘a’ = 'a
sql:select * from user where username = ‘fhdsjkf’ and password = ‘a’ or ‘a’ = ‘a’ ,这样发生用错误的信息依然能查到结果。 -
解决sql注入问题:使用 PreparedStatement对象来解决。这是一种预编译的SQL:参数使用?作为占位符。
-
使用步骤:
- 定义sql
sql的参数使用?作为占位符。 如:select * from user where username = ? and password = ?; - 获取执行sql语句的对象 PreparedStatement Connection.prepareStatement(String sql)
- 给?赋值:
方法: setXxx(参数1,参数2)
参数1:?的位置编号 ——从1 开始
参数2:?的值 - 执行sql,接受返回结果,不需要传递sql语句
- 示例:
//2.定义sql
//2.1 张三 - 500
String sql1 = "update account set balance = balance - ? where id = ?";
//2.2 李四 + 500
String sql2 = "update account set balance = balance + ? where id = ?";
//3.获取执行sql对象
pstmt1 = conn.prepareStatement(sql1);
pstmt2 = conn.prepareStatement(sql2);
//4. 设置参数
pstmt1.setDouble(1,500);
pstmt1.setInt(2,1);
pstmt2.setDouble(1,500);
pstmt2.setInt(2,2);
//5.执行sql
pstmt1.executeUpdate();
- 后期都会使用PreparedStatement来完成增删改查的所有操作
- 可以防止SQL注入
- 效率更高
四、抽取JDBC工具类
通过上面查询product表的例子不难看出,代码中重复代码特别多,因此可以通过抽取工具类的方法来简化书写。
- 思路:
- 注册驱动的抽取
- 抽取一个方法获取连接对象
不想传递参数(麻烦),还得保证工具类的通用性,可以通过配置文件来解决
jdbc.properties:
url=
user=
password=
driver=
- 抽取一个方法释放资源
- 实现:
package com.zhu.utils;
import java.io.FileReader;
import java.io.IOException;
import java.net.URL;
import java.sql.*;
import java.util.Properties;
/**
* JDBC工具类
*/
public class JDBCUtils {
private static String url;
private static String user;
private static String password;
private static String driver;
/**
* 文件的读取,只需要读取一次即可拿到这些值。使用静态代码块
*/
static{
//读取资源文件,获取值。
try {
//1. 创建Properties集合类。
Properties pro = new Properties();
//获取src路径下的文件的方式--->ClassLoader 类加载器
ClassLoader classLoader = JDBCUtils.class.getClassLoader();
URL res = classLoader.getResource("jdbc.properties");
String path = res.getPath();
//2. 加载文件
pro.load(new FileReader(path));
//3. 获取数据,赋值
url = pro.getProperty("url");
user = pro.getProperty("user");
password = pro.getProperty("password");
driver = pro.getProperty("driver");
//4. 注册驱动
Class.forName(driver);
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
/**
* 获取连接
* @return 连接对象
*/
public static Connection getConnection() throws SQLException {
return DriverManager.getConnection(url, user, password);
}
/**
* 释放资源
* @param stmt
* @param conn
*/
public static void close(Statement stmt,Connection conn){
if( stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if( conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 释放资源
* @param stmt
* @param conn
*/
public static void close(ResultSet rs,Statement stmt, Connection conn){
if( rs != null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if( stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if( conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
五、JDBC控制事务
-
操作:
1. 开启事务
2. 提交事务
3. 回滚事务 -
使用Connection对象来管理事务
开启事务:setAutoCommit(boolean autoCommit) :调用该方法设置参数为false,即开启事务
需要在执行sql之前开启事务提交事务:commit()
当所有sql都执行完提交事务回滚事务:rollback()
在catch中回滚事务 -
代码示例
public class JDBCDemo3 {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement pstmt1 = null;
PreparedStatement pstmt2 = null;
try {
//1.获取连接
conn = JDBCUtils.getConnection();
//开启事务
conn.setAutoCommit(false);
//2.定义sql
//2.1 张三 - 500
String sql1 = "update account set balance = balance - ? where id = ?";
//2.2 李四 + 500
String sql2 = "update account set balance = balance + ? where id = ?";
//3.获取执行sql对象
pstmt1 = conn.prepareStatement(sql1);
pstmt2 = conn.prepareStatement(sql2);
//4. 设置参数
pstmt1.setDouble(1,500);
pstmt1.setInt(2,1);
pstmt2.setDouble(1,500);
pstmt2.setInt(2,2);
//5.执行sql
pstmt1.executeUpdate();
// 手动制造异常
int i = 3/0;
pstmt2.executeUpdate();
//提交事务
conn.commit();
} catch (Exception e) {
//事务回滚
try {
if(conn != null) {
conn.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally {
JDBCUtils.close(pstmt1,conn);
JDBCUtils.close(pstmt2,null);
}
}
}
六、数据库连接池
概念
其实就是一个容器(集合),存放数据库连接的容器。
当系统初始化后,容器被创建,容器中会申请一些连接对象,当用户来访问数据库时,从容器中获取连接对象,用户访问完之后,会将连接对象归还给容器。
优点
- 节约资源
- 用户访问高效
实现
- 标准接口:DataSource —— javax.sql包
方法:
- 获取连接:getConnection()
- 归还连接:Connection.close()
如果连接对象Connection是从连接池中获取的,那么调用Connection.close()方法,则不会再关闭连接了,而是归还连。
- 一般我们不去实现它,因为数据库厂商已经造好了轮子:
- C3P0:数据库连接池技术
C3P0是一个开放源代码的JDBC连接池,它在lib目录中与Hibernate一起发布,包括了实现jdbc3和jdbc2扩展规范说明的Connection 和Statement 池的DataSources 对象。
- Druid:由阿里巴巴提供,较新
Druid是目前最好的数据库连接池,在功能、性能、扩展性方面,都超过其他数据库连接池,包括DBCP、C3P0、BoneCP、Proxool、JBoss DataSource。Druid已经在阿里巴巴部署了超过600个应用,经过一年多生产环境大规模部署的严苛考验。
C3P0
使用步骤:
-
导入jar包 (两个) c3p0-0.9.5.2.jar; mchange-commons-java-0.2.12.jar
别忘了导入数据库驱动jar包! -
定义配置文件:
名称: c3p0.properties 或者 c3p0-config.xml
路径:直接将文件放在src目录下即可。 -
创建数据库连接池对象:ComboPooledDataSource
-
获取连接: getConnection
代码:
//1.创建数据库连接池对象
DataSource ds = new ComboPooledDataSource();
//2. 获取连接对象
Connection conn = ds.getConnection();
Druid
-
使用步骤:
1. 导入jar包 druid-1.0.9.jar
2. 定义配置文件:properties形式,可以为任意名称,放在任意目录下
3. 加载配置文件
4. 获取数据库连接池对象:通过工厂来来获取 DruidDataSourceFactory
5. 获取连接:getConnection -
代码:
//3.加载配置文件
Properties pro = new Properties();
InputStream is = DruidDemo.class.getClassLoader().getResourceAsStream("druid.properties");
pro.load(is);
//4.获取连接池对象
DataSource ds = DruidDataSourceFactory.createDataSource(pro);
//5.获取连接
Connection conn = ds.getConnection();
- 改进工具类
这里可以对上文写的工具类进行改进,静态代码块除了加载配置文件,还可以初始化连接池对象,类中还提供获取连接池和从池中获取连接的方法。
public class JDBCUtils {
//1.定义成员变量 DataSource
private static DataSource ds ;
static{
try {
//1.加载配置文件
Properties pro = new Properties();
pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
//2.获取DataSource
ds = DruidDataSourceFactory.createDataSource(pro);
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接
*/
public static Connection getConnection() throws SQLException {
return ds.getConnection();
}
/**
* 释放资源
*/
public static void close(Statement stmt,Connection conn){
close(null,stmt,conn);
}
public static void close(ResultSet rs , Statement stmt, Connection conn){
if(rs != null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(stmt != null){
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
if(conn != null){
try {
conn.close();//归还连接
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 获取连接池方法
*/
public static DataSource getDataSource(){
return ds;
}
}
七、Spring JDBC
这是Spring框架对JDBC的简单封装,提供了一个JDBCTemplate对象简化JDBC的开发。
使用步骤:
- 导入jar包
- 创建JdbcTemplate对象,依赖于数据源DataSource
JdbcTemplate template = new JdbcTemplate(ds);
- 调用JdbcTemplate的方法来完成CRUD的操作
- update():执行DML语句。增、删、改语句
- queryForMap():查询结果将结果集封装为map集合,将列名作为key,将值作为value 将这条记录封装为一个map集合
这个方法查询的结果集长度只能是1
- queryForList():查询结果将结果集封装为list集合
将每一条记录封装为一个Map集合,再将Map集合装载到List集合中
- query():查询结果,将结果封装为JavaBean对象,无需写繁琐的setter!
query的参数:RowMapper
一般我们使用BeanPropertyRowMapper实现类,可以完成数据到JavaBean的自动封装
new BeanPropertyRowMapper<类型>(类型.class)
- queryForObject:查询结果,将结果封装为对象,一般用于聚合函数的查询
案例
需求:
1. 修改1号数据的 salary 为 10000
2. 添加一条记录
3. 删除刚才添加的记录
4. 查询id为1的记录,将其封装为Map集合
5. 查询所有记录,将其封装为List
6. 查询所有记录,将其封装为Emp对象的List集合
7. 查询总记录数
import cn.itcast.domain.Emp;
import cn.itcast.utils.JDBCUtils;
import org.junit.Test;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
public class JdbcTemplateDemo2 {
//1. 获取JDBCTemplate对象
private JdbcTemplate template = new JdbcTemplate(JDBCUtils.getDataSource());
/**
* 1. 修改1号数据的 salary 为 10000
*/
@Test
public void test1(){
//2. 定义sql
String sql = "update emp set salary = 10000 where id = 1001";
//3. 执行sql
int count = template.update(sql);
System.out.println(count);
}
/**
* 2. 添加一条记录
*/
@Test
public void test2(){
String sql = "insert into emp(id,ename,dept_id) values(?,?,?)";
int count = template.update(sql, 1015, "郭靖", 10);
System.out.println(count);
}
/**
* 3.删除刚才添加的记录
*/
@Test
public void test3(){
String sql = "delete from emp where id = ?";
int count = template.update(sql, 1015);
System.out.println(count);
}
/**
* 4.查询id为1001的记录,将其封装为Map集合
* 注意:这个方法查询的结果集长度只能是1
*/
@Test
public void test4(){
String sql = "select * from emp where id = ? or id = ?";
Map<String, Object> map = template.queryForMap(sql, 1001,1002);
System.out.println(map);
//{id=1001, ename=孙悟空, job_id=4, mgr=1004, joindate=2000-12-17, salary=10000.00, bonus=null, dept_id=20}
}
/**
* 5. 查询所有记录,将其封装为List
*/
@Test
public void test5(){
String sql = "select * from emp";
List<Map<String, Object>> list = template.queryForList(sql);
for (Map<String, Object> stringObjectMap : list) {
System.out.println(stringObjectMap);
}
}
/**
* 6. 查询所有记录,将其封装为Emp对象的List集合,使用BeanPropertyRowMapper
*/
@Test
public void test6(){
String sql = "select * from emp";
List<Emp> list = template.query(sql, new BeanPropertyRowMapper<Emp>(Emp.class));
for (Emp emp : list) {
System.out.println(emp);
}
}
/**
* 7. 查询总记录数
*/
@Test
public void test7(){
String sql = "select count(id) from emp";
Long total = template.queryForObject(sql, Long.class);
System.out.println(total);
}
}