一、JDBC概述
1.1数据持久化
- 持久化:把数据保存到可掉电式存储设备中以供之后使用。大多数情况下,数据持久化意味着将内存中的数据保存到硬盘上加以固化。而持久化的实现过程大多通过各种关系数据库来完成。
- 持久化的主要应用是将内存中的数据存储在关系型数据库中,当然也可以存储在磁盘文件、XML数据文件中。
1.2Java中的数据存储技术
在Java中,数据库存取技术可以分为如下几类:
JDBC直接访问数据库。
JDO(Java Data Object)技术。
第三方O/R工具,如Hibernate,Mybatis等
JDBC是java访问数据库的基石,JDO、Hibernate、MyBatis等只是更好的封装了JDBC
1.3JDBC介绍
- JDBC是一个独立于特定的数据库管理系统、通用的SQL数据存取和操作的公共接口,定义了用来访问数据库的标准Java类库,(java.sql,javax.sql)使用这些类库可以以一种标准的方法、方便地访问数据库资源。
- JDBC为访问不同的数据库提供了一种统一的途径,为开发者屏蔽了一些细节问题。
- JDBC的目标是使JAVA程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统,这样就使得程序员无需对特定的数据库系统的特点有过多的了解,从而大大简化和加快了开发过程。
1.4JDBC的体系结构
JDBC接口包括两个层次
- 面向应用的API:JavaAPI,抽象接口,供应用程序开发人员使用(连接数据库,执行SQL语句,获得结果);
- 面向数据库的API:Java Driver API,供开发商开发数据库驱动程序用。
1.5JDBC程序编写步骤
二、获取数据库连接
2.1Driver接口实现类
接口介绍:
- java.sql.Driver 接口是所有JDBC驱动程序需要实现的接口。这个接口是提供给数据库厂商使用的,不同数据库厂商提供不同的实现。
- 在程序中不需要直接去访问实现了Driver接口的类,而是由驱动程序管理器类(java.sql.DriverManager)去调用这些Driver实现。
加载以及注册驱动
原始状态:
public static void connectMysql3() throws Exception {
String url = "jdbc:mysql://127.0.0.1:3306/myemployees";
Properties prop = new Properties();
prop.setProperty("user", "root");
prop.setProperty("password", "********");
//获取驱动
Class clazz = Class.forName("com.mysql.cj.jdbc.Driver");
Driver driver = (Driver) clazz.newInstance();
//注册驱动
DriverManager.registerDriver(driver);
//获取连接
Connection conn = DriverManager.getConnection(url, prop);
System.out.println(conn);
}
由于加载Driver的时候,有静态方法直接实现了驱动注册操作,所以这部分代码可以省略;
另外,为了避免硬编码,将数据与代码解耦,可以将数据放在jdbc.properties单独文件中管理,代码更新如下:
public static void connectMysql() throws Exception {
InputStream is = ConnectionMySql.class.getClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(is);
String user = prop.getProperty("user");
String password = prop.getProperty("password");
String url = prop.getProperty("url");
String driverClass = prop.getProperty("driverClass");
//获取驱动,同时会加载注册驱动的静态代码
Class.forName(driverClass);
//获取连接
Connection conn = DriverManager.getConnection(url,user,password);
System.out.println(conn);
}
三、实现CRUD
3.1 操作访问数据库
数据库连接被用于向数据库服务器发送命令和SQL语句,并接受数据库服务器返回的结果。其实一个数据库连接就是一个Socket连接。
在java.sql包中有3个接口分别定义了对数据库的调用的不同方式:
- Statement:用于执行静态SQL语句并返回它所生成结果的对象。
- PrepatedStatement:SQL语句被预编译并存储在此对象中,可以使用此对象多次高效的执行该语句。
- CallableStatement:用于执行SQL存储过程;
3.2PreparedStatement实现数据库的增删改查
小问题:为什么不使用Statement操作数据表:
- sql语句需要拼接字符串,量大繁琐,容易出错。
- 会存在sql注入问题
首先准备一个简单的学生表如图:
1.最基本的插入数据
public class PrepareStatementTest {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
try {
//1.读取文件信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(is);
String user = prop.getProperty("user");
String password = prop.getProperty("password");
String url = prop.getProperty("url");
String driverClass = prop.getProperty("driverClass");
//2.获取驱动,同时会加载注册驱动的静态代码
Class.forName(driverClass);
//3.获取连接
conn = DriverManager.getConnection(url, user, password);
//4.预编译sql语句,返回PreparedStatement的实例
String sql = "INSERT INTO student(name, age, hobby) VALUES(?,?,?);";
ps = conn.prepareStatement(sql);
//5.填充占位符
ps.setString(1, "KOBE");
ps.setInt(2, 24);
ps.setString(3, "basketball");
//6.执行操作
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//7.资源关闭
try {
if (ps != null) {
ps.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
2.通用的增删改方法
封装工具类
public class JDBCUtils {
/**
* 获取连接
*
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
//1.读取文件信息
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("jdbc.properties");
Properties prop = new Properties();
prop.load(is);
String user = prop.getProperty("user");
String password = prop.getProperty("password");
String url = prop.getProperty("url");
String driverClass = prop.getProperty("driverClass");
//2.获取驱动,同时会加载注册驱动的静态代码
Class.forName(driverClass);
//3.获取连接
return DriverManager.getConnection(url, user, password);
}
/**
* 关闭资源
*
* @param connection
* @param statement
*/
public static void closeResource(Connection connection, Statement statement) {
try {
if (statement != null) {
statement.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (connection != null) {
connection.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
public static void updateTable(String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
try {
//获取连接
conn = getConnection();
//prepareStatement预加载sql
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//执行
ps.execute();
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
closeResource(conn, ps);
}
}
}
主方法类
public class DeleteTest {
public static void main(String[] args) {
String sql ="INSERT INTO student(name, age, hobby) VALUES(?,?,?)";
JDBCUtils.updateTable(sql,"LOONG",18,"SLEEP");
}
}
3.最基本的查询数据
封装的关闭资源方法在这里用到,需要新增一个参数,把ResultSet资源也关闭。
因为查询不知道具体返回的列数,查询要用到元数据,还要用到反射,将实例属性得到后才能完成赋值操作。
public class QueryTest {
public static void main(String[] args) {
String sql = "SELECT * FROM student WHERE id =?";
Student student = queryTable(sql,5);
System.out.println(student);
}
public static Student queryTable(String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//获取连接
conn = JDBCUtils.getConnection();
//prepareStatement预加载sql
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//执行,并返回结果集
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集列数
int columnCount = rsmd.getColumnCount();
if (rs.next()) {
Student student = new Student();
for (int i = 0; i < columnCount; i++) {
//获取每列数值
Object columnValue = rs.getObject(i + 1);
//获取列名
String columnLabel = rsmd.getColumnLabel(i + 1);
//赋值
Field field = Student.class.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(student, columnValue);
}
return student;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}
}
4.通用的查询方法
理念:
为了不让方法只能一个类使用,我们引入了泛型的概念,传参的时候指定类,就做到类通用了;
为了返回结果不再限制为一条,我们将结果集用ArrayList装,将一次赋值变成循环赋值。
public static <T> List<T> queryTable(Class<T> tClass, String sql, Object... args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
//获取连接
conn = JDBCUtils.getConnection();
//prepareStatement预加载sql
ps = conn.prepareStatement(sql);
//填充占位符
for (int i = 0; i < args.length; i++) {
ps.setObject(i + 1, args[i]);
}
//执行,并返回结果集
rs = ps.executeQuery();
//获取结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
//获取结果集列数
int columnCount = rsmd.getColumnCount();
//new结果集
ArrayList<T> list = new ArrayList<>();
while (rs.next()) {
T t = tClass.newInstance();
for (int i = 0; i < columnCount; i++) {
//获取每列数值
Object columnValue = rs.getObject(i + 1);
//获取列名/别名
String columnLabel = rsmd.getColumnLabel(i + 1);
//赋值
Field field = tClass.getDeclaredField(columnLabel);
field.setAccessible(true);
field.set(t, columnValue);
}
list.add(t);
}
return list;
} catch (Exception e) {
e.printStackTrace();
} finally {
//关闭资源
JDBCUtils.closeResource(conn, ps, rs);
}
return null;
}
具体应用到编写业务需要考虑事务:
将connect的默认自动提交给取消掉(connect.setAutoCommit(false)),
过程中出现异常,捕获异常中执行connect.rollback方法。
直到最后再connect.submit
四、数据库连接池
引入原因
由于我们开发过程中使用数据库,原理是通过主程序获取数据连接->进行sql操作->断开连接。
这一套操作存在一些问题:
- 普通的JDBC操作数据库通过DriverManager获取连接,每次都要将Connection加载到内存,验证用户名密码还要一些时间,操作完成再断开连接,这种方式会消耗大量资源和时间,使得数据库的连接没有得到很好的重复利用,若是人数特别多的情况下,频繁操作数据库,服务器可能崩溃。
- 每次操作完都要断开连接,若是出现异常,数据库连接未能关闭,会导致数据库系统中内存泄漏。
- 而且开发无法控制创建的连接对象数量,系统资源无限制的被分配出去,也会有内存泄漏隐患。
数据库连接池技术思想
- 基本思想:就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。
- 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是新建立一个。
- 数据库连接池在初始化时创建一定数量的数据库连接放到连接池中,这些数据库连接的数量是由最小数据库连接数来决定的。无论这些数据库连接是否被使用,连接池都将一直保证至少拥有这么多的连接数量。连接池的最大数据库数量限定了这个连接池能占有的最大连接数,当应用程序向连接池请求的连接数超过最大连接数量时,这些请求将被加入到等待队列中。
数据库连接池的优点
1.资源重用
由于数据库连接池得以重用,避免了频繁创建、释放引起得大量性能开销。另一方面也增加了系统运行环境的平稳性。
2.更快的系统反应速度
数据库连接池在初始化过程中,往往已经创建了若干数据库连接置于连接池中备用。此时连接的初始化工作均已完成。对于业务请求处理而言,直接利用现有可用连接,避免了数据库连接初始化和释放过程的时间开销,从而减少了系统的响应时间。
3.新的资源分配手段
对于多应用共享同一数据库而言,可在应用层通过数据库连接池的配置,实现某一应用最大可用数据库连接数的限制,避免某一应用独占所有的数据库资源。
4.统一的连接管理,避免数据库连接泄漏
较为完善的数据库连接池实现中,可根据预先的占用超时设定,强制回收被占用连接,从而避免了常规数据库连接操作中可能出现的资源泄漏。
简单案例:
maven项目添加个pom
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.5</version>
</dependency>
获取连接的方法改一下
public class JDBCUtils {
private static DataSource source;
static {
try {
InputStream is = ClassLoader.getSystemClassLoader().getResourceAsStream("druid.properties");
Properties prop = new Properties();
prop.load(is);
source = DruidDataSourceFactory.createDataSource(prop);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 获取连接
*
* @return
* @throws Exception
*/
public static Connection getConnection() throws Exception {
return source.getConnection();
}
}
配置文件druid.properties
参数有很多,可以根据需求自定义配置一些
url=jdbc:mysql://localhost:3306/test
username=root
password=123456789
driverClassName=com.mysql.cj.jdbc.Driver
initialSize=8
......