一、JDBC 概述
1.1 持久化概述
持久化(persistence): 把内存中的数据保存到可掉电式存储设备 (硬盘、U盘等) 中以供之后使用。
大多数企业级应用,数据持久化是将数据保存到各种关系数据库, 而在 Java中,数据库存取技术只能通过 JDBC 来访问数据库。 JDBC 是 Java 语言访问数据库的基石,其他技术都是对 JDBC 的封装.
1.2 JDBC 概述
JDBC 介绍
JDBC(Java DataBase Connectivity Java 数据库连接)是一种用于执行 SQL 语句的 Java API,可以为多种关系型数据库提供统一访问,它由一组用 Java 语言编写的类和接口组成。
JDBC 本质
其实就是Java 官方提供的一套规范(接口),帮助开发人员快速实现不同关系型数据库的连接。
总结 : JDBC 本身是 Java 连接数据库的标准,由 Java 编写的一组接口组成,接口的实现由各个数据库厂商来完成.
二、JDBC 快速入门
2.1 环境准备
- 拷贝 MySQL 的 JDBC 驱动,到 Java 项目中: mysql-connector-java-5.1.26-bin.jar
注意: 是 jar 包,不是 zip 包.
2.选择 jar,把 jar 引用到 classpath 路径. idea 项目中创建一个目录 lib
右键 lib 目录,点 Add as Library...
2.2 JDBC 开发口诀
- 加载注册驱动.
- 获取连接对象.
- 创建/获取语句对象.
- 执行SQL语句.
- 释放资源.
三、功能类详解
3.1 DriverManager 类
DriverManager : 驱动管理对象,主要用于加载注册关系型数据库的 Driver 类以及获取和关系型数据库的连接对象
- 加载注册驱动
注册给定的驱动对象:static void registerDriver(Driver driver);
写法:Class.forName("com.mysql.jdbc.Driver");在 com.mysql.jdbc.Driver 类中存在静态代码快
1. 把 com.mysql.jdbc.Driver 这一份字节码加载进 JVM.
字节码被加载进JVM,就会执行其静态代码块.而其底层的静态代码块在完成注册驱动工作,将驱动注册到DriverManger 中.
注意: Java6 开始,JDBC4.0 有一个新特性-无需加载注册驱动.
在 jar 包中存在一个java.sql.Driver 配置文件。此文件已经包含 com.mysql.jdbc.Driver 驱动名称。 程序会自动从 META-INF/services/java.sql.Driver 去读取当前的驱动类的全限定名,所
以目前写不写加载注册驱动都没问题,但是 web 项目必须写上加载注册驱动代码,否则无法连接数据库。
- 获取连接对象
获取连接对象:static Connection getConnection(String url,String username,String password);
参数
url :指定连接某一个数据库的路径,语法:
url=jdbc:mysql://localhost:3306/jdbcdemo
MySQL url: 如果连接的是本机的 MySQL,并且端口是默认 3306 ,则可以简写:
url=jdbc:mysql:///jdbcdemo
password :连接数据库的密码
3.2 Connection 类
Connection :关系型数据库连接对象,相当于 Java 程序和数据库的通信桥梁
- 获取执行语句对象
获取静态语句执行对象: Statement createStatement();
获取预编译语句执行对象: PreparedStatement prepareStatement(String sql);
- 释放资源
关闭连接对象: void close();
- 事务
3.3 Statement 类
Statement:静态SQL语句执行对象,用于执行字符串的 SQL 语句
1. 执行 DML / DDL 语句 : int executeUpdate(String sql);
返回值 : DML 受影响的行数,DDL 则返回 0
参数:insert、update、delete、create语句
2. 执行 DQL 语句:ResuleSet executeQuery(String sql);
返回值:执行查询之后的结果数据
参数:select 语句
关闭 Statement 对象 :void close();
四、DAO 思想
4.1 DAO 引入
在没有 DAO 的时候,我们的代码存在大量的重复。
4.2 DAO 介绍和方法设计
DAO(Data Access Object) 数据访问对象是一个面向对象的数据库接口. 顾名思义就是与数据库打交道,将所有对数据源的访问操作抽象封装在一个公共 API 中。
DAO 中的主要操作: 增删改查(CRUD). 引入 DAO 之后,此时设计如下图。
DAO 作为组件,主要的是方法的设计,而方法设计需要注意什么呢?
- 在保存功能中,调用者需要传递多个参数进来,然后把这些数据保存到数据库中
- 在查询功能中,结果集的每行数据有多个列的值,然后把这些数据返回给调用者
意识: 在开发过程中,如果遇到需要传递的数据有多个的时候,通常需要使用 JavaBean 对其进行封装最终方法设计如下:
- //调用者将需要保存的数据封装到Student对象中,然后传递进来
2 void save(Student stu)
3 //在查询之后,将每行数据封装到Student对象中,再返回给调用者
4 Student selectOne(long id);
4.3 DAO层开发规范
DAO 其实是一个组件(可以重复使用),包括:
分包规范: 域名倒写.项目模块名.组件; cn.wolfcode.pss.util; // 存放工具类 StringUtil cn.wolfcode.pss.domain; //装pss模块的domain类,模型对象.(Student) cn.wolfcode.pss.dao; //装pss模块的dao接口. cn.wolfcode.pss.dao.impl; //装pss模块的dao接口的实现类. cn.wolfcode.pss.test; // 暂时存储DAO的测试类,以后的测试类不应该放这里.
命名规范:
以下的 Xxx 表示一个模型对象,比如 Employee,Department,Student
DAO 接口 : 表示对某个模型的 CRUD 操作做规范,以 I 开头,interface
例: IEmployeeDAO/IStudentDAO
DAO 实现类: 表示对某个 DAO 接口的实现
标准:XxxDAOImpl
例:EmployeeDAOImpl/StudentDAOImpl
DAO 测试类: 测试 DAO 组件中的所有方法
标准:XxxDAOTest: XxxDAO 组件的测试类,
例:EmployeeDAOTest,StudentDAOTest
开发建议: 面向接口编程,声明 DAO 对象 传统的做法 : EmployeeDAOImpl dao = new
EmployeeDAOImpl(); 面向接口编程 : IEmployeeDAO dao = new EmployeeDAOImpl();
把实现类赋给接口类型,体现多态的特性:可以屏蔽不同子类之间实现的差异.
4.4 根据 DAO 规范搭建项目
4.4.1 步骤
- 创建项目
- 导入数据库驱动包
- 创建表和模型包以及模型对象 (domain/Student)
- 创建 DAO 包和 DAO 接口,设计 DAO 接口方法 (dao/IStudentDAO)
- 创建 DAO 实现包,实现 DAO 接口(dao.impl/StudentDAOImpl)
- 创建测试目录,生成测试类和方法(test/StudentDAOTest)
- 书写实现,实现一个方法测试一个方法并且测试通过
4.4.2 测试先行
public class StudentDAOTest {
private IStudentDAO studetDAO = new StudentDAOImpl();
@Test
public void insert() {
Student stu = new Student(null,"小明","ming@",18);
studetDAO.insert(stu);
}
@Test
public void delete() {
studetDAO.delete(2L);
}
@Test
public void update() {
Student stu = new Student(1L,"小明","ming@",19);
studetDAO.update(stu);
}
@Test
public void selectOne() {
Student stu = studetDAO.selectOne(1L);
System.out.println(stu);
}
@Test
public void selectList() {
List<Student> list = studetDAO.selectList();
for (Student stu:list) {
}
}
}
4.4.3 DAO 之保存操作
public void insert(Student stu) {
String sql = "INSERT INTO t_student(name,email,age) VALUES('" + stu.getName() + "','"
+ stu.getEmail() + "',"
+ stu.getAge() + ")";
System.out.println(sql);
Connection conn = null;
Statement st = null;
try {
// 1 加载注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获取连接对象
conn = DriverManager.getConnection("jdbc:mysql:///jdbcdemo", "root", "admin");// 3 获取语句对象
st = conn.createStatement();
// 4 执行sql 语句
st.executeUpdate(sql);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (st != null) {
st.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
在完成保存的过程中,执行 SQL 需要的参数是调用者传递进来的,所以需要将其拼接到 SQL 里面
String sql = "INSERT INTO t_student(name,email,age) VALUES('" + stu.getName() + "','"
- stu.getEmail() + "',"
- stu.getAge() + ")";
System.out.println(sql);
可以看出,这里的拼接操作极其恶心, 容易错,还不容易发现,拼接多了语义很不清晰,所以这里需要优化(PreparedStatement)
伍、预编译语句对象
5.1 预编译语句对象简介
PreparedStatement 接口: 是 Statement 接口的子接口, 享有 Statement 中的方法.
使用的预编译语句对象,sql 语句中使用 ? 来作为值的占位符.
5.2 API 详解
Connection API:
PreparedStatement conn对象的.prepareStatement(String sql)
PreparedStatement API:
// 常用方法:
void setXxx(int parameterIndex,Xxx value); //设置第几个占位符的真正参数值.
// Xxx 表示数据类型,比如 String,int,long,Date等.
void setObject(int parameterIndex, Object x); //设置第几个占位符的真正参数值. 5
int executeUpdate(); //执行DDL/DML语句. 注意:没有参数
// 若当前 SQL是 DDL语句,则返回 0.
// 若当前 SQL是 DML语句,则返回受影响的行数.
ResultSet executeQuery(); //执行DQL语句,返回结果集.
close(); //释放资源
有了 PreparedStatement 就可以使用占位符 ? 来代替拼接,这样语义更加清晰,数据设置也很清晰,所以以后都使用 PreparedStatement 预编译语句对象。
5.3 插入功能优化
调整之后的插入操作:
public void insert(Student stu) {
String sql = "INSERT INTO t_student(name,email,age) VALUES(?,?,?)";
System.out.println(sql);
// 贾琏欲执事
Connection conn = null;
PreparedStatement pst = null;
try {
// 1 加载注册驱动
Class.forName("com.mysql.jdbc.Driver");
// 2 获取连接对象
conn = DriverManager.getConnection("jdbc:mysql:///jdbcdemo", "root", "admin");
// 3 获取预编译语句对象
pst = conn.prepareStatement(sql);
// 给sql 的? 设置数据
pst.setObject(1,stu.getName());
pst.setObject(2,stu.getEmail());
pst.setObject(3,stu.getAge());
// 4 执行sql 语句
pst.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (pst != null) {
pst.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
六、基于 JDBC 的 DAO 案例
6.1 DAO 之更新、删除
更新和删除都属于 DML 操作,它们和插入的不同仅仅就 SQL 语句不同而已,其他的都是相同的。
DML 操作模板:
//String sql = "INSERT INTO t_student(name,email,age) VALUES(?,?,?)"; //String sql = "DELETE FROM t_student WHERE id=?";
String sql = "UPDATE t student SET name=?,email=?,age=? WHERE id=?";
Connection conn = null;
PreparedStatement pst = null;
try {
//1 加载注册驱动
Class.forName("com.mysql.jdbc.Driver");
//2 获取连接对象
conn = DriverManager.getConnection("jdbc:mysql:///jdbcdemo", "root", "admin");
//3 获取预编译语句对象
pst = conn.prepareStatement(sql);
//给sql 的? 设置数据
pst.setString(1, stu.getName());
pst.setString(2, stu.getEmail());
pst.setInt(3, stu.getAge());
pst.setLong(1,id);
pst.setString(1,stu.getName());
pst.setString(2,stu.getEmail());
pst.setInt(3,stu.getAge());
pst.setLong(4,stu.getId());
// 4 执行sql 语句
pst.executeUpdate();
} catch (Exception e) { e.printStackTrace();
} finally {
try {
if (pst != null) {
pst.close();
}
} catch (SQLException e) { e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
在此 DML 操作就完成了,目前就差 DQL 了。
6.2 DAO 之查询操作
DQL 操作和DML 操作的不同之处是 DQL 需要获取返回的数据,而这些数据都被存在一个叫 ResultSet 的对象中
ResultSet Statement对象的 executeQuery(String sql); //执行DQL语句,返回结果集.
ResultSet PreparedStatement对象的 executeQuery(); //执行DQL语句,返回结果集.
6.2.1_查询结果对象
ResultSet 接口: 通过执行 DQL 语句查询之后的结果对象.
ResultSet 对象具有指向其当前数据行的光标。最初,光标被置于第一行之前。next 方法将光标移动到下一行;因为该方法在 ResultSet 对象没有下一行时返回 false,所以可以在 while 循环中使用它来迭代结果集
// 常用方法:select id,name,age
boolean next(); //判断当前光标是否能向下移动,能向下移动返回true,并同时将光标移动到下一行Xxx getXxx(int columnIndex); //取出当前光标所在行的第columnIndex列的数据(columnIndex 从1开始算).
Xxx getXxx(String columnName);//取出当前光标所在行的列名为columnName列的数据,columnName可以是别名.
// Xxx 表示数据类型,比如String,int,long,Date等. 推荐使用列名来取数据.
close();// 释放资源
ResultSet 中存的数据则为查询出来的结果,这个结果就是一张表的结果。
id | name | | age |
1 | 小明 | xiao@ | 18 |
2 | 小东 | dong@ | 18 |
而我们要做的就是从 ResultSet 中来取出这些数据。
操作步骤
先取行: 默认光标在第一行,则为标题行(数据)使用 ResultSet 的 next 方法完成
再取列: id name age
// 取出当前光标所在行的第columnIndex列的数据(columnIndex 从 1 开始算).
Xxx getXxx(int columnIndex); // 索引可能会变化,所以不建议使用
// 取出当前光标所在行的列名为columnName列的数据,columnName可以是别名.(推荐)
Xxx getXxx(String columnName);
七、重构设计
在 DAO 的实现过程中,发现释放资源的代码非常恶心,而且还需要反复编写,这种代码我们通常可以抽取到工具类中: JDBCUtil
7.1 抽取 JDBCUtil 工具类
// JDBCUtil.java
// 释放资源
public static void close(Connection conn, Statement pst, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 释放资源
try {
if (pst != null) {
pst.close();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (conn != null) {
conn.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
在获取连接的过程,需要指定连接的信息: 驱动类的全限定名 / url / 用户名 /密码(连接数据库的四要素), 但是每次都需要编写对应的字符串,麻烦,重复,也容易错。
将获取连接对象的操作抽取到工具类中,统一编写连接数据库的信息
//JDBCUtil.java
public static Connection getConnection() throws Exception {
// 加载注册驱动
Class.forName("com.mysql.jdbc.Driver");
//获取连接对象
return DriverManager.getConnection("jdbc:mysql://localhost:3306/javaweb", "root", "admin");
}
加载注册驱动在整个程序只需要执行一次,所以将加载的代码放到静态代码块中
//JDBCUtil.java
static {
// 加载注册驱动
// 该操作,在整个程序中只需要执行一次
try {
Class.forName("com.mysql.jdbc.Driver");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
上面的代码中,我们是将连接数据库的信息编写在 Java 代码中的,这当中包括数据库的用户名和密码Java代码会编译成字节码文件,然后把字节码文件交给用户去使用,当用户需要修改数据库密码时,此时不方便
问题: 硬编码
解决方案: 配置文件 properties(key-value) || xml
由于四要素是key=value 格式,所有选择使用 properties
7.2_抽取db.properties
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/javaweb 4 username=root
password=admin
将文件中的数据加载到内存中的 Properties 对象,然后再从 Properties 中获取数据,设置给 JDBC
// JDBCUtil.java
private JdbcUtil() {}
private static Properties p;
static {
// 加载注册驱动
// 该操作,在整个程序中只需要执行一次
try {
// properties配置文件只需要加载一次
InputStream in = Thread.currentThread().getContextClassLoader()
getResourceAsStream("db.properties");
p = new Properties();
p.load(in)
Class.forName(p.getProperty("driverClassName"));
} catch (Exception e) {
e.printStackTrace();
}
}
// 获取连接对象
public static Connection getConnection() throws Exception {
// 获取连接对象
return DriverManager.getConnection(
p.getProperty("url"),
p.getProperty("username"),
p.getProperty("password"));
}