概述
JDBC是Java数据库编程的核心基础。是Java提供的一个核心类库。它通过提供一套接口规范,确保Java通过JDBC,正确地访问数据库。本文在介绍JDBC基础的同时,还将引入数据库连接池相关的概念,以及关于DBCP连接池的使用。(点此获取示例代码)
核心类和接口
- Driver 接口,定义了各个驱动程序都必须要实现的功能,是驱动程序的抽象。
- DriverManager 是Driver的管理类。通过Class.forname(DriverName)的方式,就可以注册一个驱动程序。
- Connection 通过DriverManager.getCoonection(DB_URL,USER,PASS)的方式,获取建立到数据库的物理链接。
- Statement sql的容器。可以进行数据的增删查改。
- ResultSet sql查询的结果。内部有指针,默认指向第一行记录。
JDBC URL
JDBC URL是后端数据库的唯一标识符。它是由“协议:子协议://子名称”这样格式的字符串组成的。而子名称中又包含了主机、端口和数据库,如下:
构建步骤
1、 装载驱动程序
2、 建立数据库连接
3、 执行sql语句
4、 获取执行结果
5、 清理环境
案例演示
在开始案例演示之前,我们先建立一个表,内容如下图:
接着,我们在pom文件中引入mysql的驱动依赖,只有引入了驱动依赖,才能连接mysql数据库。
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
代码如下:
public class HelloJDBC {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void databaseOperation() {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 1. 装载驱动程序
Class.forName(JDBC_DRIVER);
// 2. 建立数据库连接
connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);
// 3. 执行sql语句
statement = connection.createStatement();
resultSet = statement.executeQuery("select name from user");
// 4. 获取执行结果
while (resultSet.next()) {
System.out.println("Hello " + resultSet.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 清理环境
try {
if (connection != null) {
connection.close();
}
if (statement != null) {
statement.close();
}
if (resultSet != null) {
resultSet.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
databaseOperation();
}
}
运行结果如下:
JDBC进阶
通过游标按批次读取数据
我们通常有这种应用场景,比如我们想要读取数据库表中的所有记录,对所有记录进行分析统计。但是如果一次性读取全部数据,在企业开发过程中,数据量可能达到千万级,如果全部读入到内存中,很容易内存溢出。因此我们可以每次读取一部分数据进行处理,处理完之后再读取下一部分的数据。JDBC提供对游标的支持,支持批次读取数据。
JDBC使用游标的方式
- DB_URL后缀增加“?useCursorFetch=true”,开启对游标的支持。
- 使用PreparedStatement接口替换Statement接口,通过PreparedStatement对象.setFetchSize()方法,设置每次查询的数量。
代码修改如下:
public class HelloJDBC {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
//useCursorFetch=true表示使用游标
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study?useCursorFetch=true";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void databaseOperation() {
Connection connection = null;
//Statement statement = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
try {
// 1. 装载驱动程序
Class.forName(JDBC_DRIVER);
// 2. 建立数据库连接
connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);
// 3. 执行sql语句
//statement = connection.createStatement();
//resultSet = statement.executeQuery("select name from user");
preparedStatement = connection.prepareStatement("select name from user");
preparedStatement.setFetchSize(1);
resultSet = preparedStatement.executeQuery();
// 4. 获取执行结果
while (resultSet.next()) {
System.out.println("Hello " + resultSet.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 清理环境
try {
if (connection != null) {
connection.close();
}
// if (statement != null) {
// statement.close();
// }
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
databaseOperation();
}
}
JDBC使用流方式读取大字段数据
实际开发过程中,数据库可能需要存储大字段,比如存储博客数据。但是如果直接从数据库中取出大字段数据,会占据太多的内存,太大的话,同样也会造成内存溢出。因此,我们可以使用流方式来读取大字段数据。
JDBC流方式读取大字段数据的思路
将大字段的数据,按照二进制的方式,按照区间加以划分,划分为多个区间。每次读取一个区间的内容,处理完毕之后,再处理下一个区间的内容,直到所有去区间的数据都处理完毕为止。 在这里我们先定义一个表:
其中info字段存储的是当前博客的一些数据。 Java代码如下:
public class StreamJDBC {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study";
private static final String USER = "root";
private static final String PASSWORD = "123456";
private static final String FILE_PREFIX = "D:\\info";
private static final String FILE_SUFFIX = ".txt";
public static void databaseOperation() {
Connection connection = null;
PreparedStatement preparedStatement = null;
ResultSet resultSet = null;
InputStream in = null;
OutputStream out = null;
try {
// 1. 装载驱动程序
Class.forName(JDBC_DRIVER);
// 2. 建立数据库连接
connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);
// 3. 执行sql语句
preparedStatement = connection.prepareStatement("select info from info_message");
resultSet = preparedStatement.executeQuery();
int i = 1;
// 4. 获取执行结果
while (resultSet.next()) {
// 5.获取流对象
in = resultSet.getBinaryStream("info");
// 6.将对象流写入文件
File f = new File(FILE_PREFIX + i + FILE_SUFFIX);
out = new FileOutputStream(f);
int temp = 0;
//边读编写
while ((temp = in.read()) != -1) {
out.write(temp);
}
i++;
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 6. 清理环境
closeResource(connection, preparedStatement, resultSet,in,out);
}
}
public static void closeResource(Connection connection, PreparedStatement preparedStatement, ResultSet resultSet,
InputStream in, OutputStream out) {
try {
if (out != null) {
out.close();
}
if (in != null) {
in.close();
}
if (connection != null) {
connection.close();
}
if (preparedStatement != null) {
preparedStatement.close();
}
if (resultSet != null) {
resultSet.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
databaseOperation();
}
}
JDBC批处理插入数据
批量插入数据能够减少连接的个数,从而减少系统开销,提高效率。
- Statement (preparedStatement继承了Statement,所以也自动拥有了下面的方法)
- addBatch() 将多条sql组成一个处理单元
- executeBatch() 进行批处理操作
- clearBatch() 清空sql语句,准备下次执行
接着我们批量插入三个用户,代码如下:
public class BatchJDBC {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void insertUser(List<String> userNames) {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 1. 装载驱动程序
Class.forName(JDBC_DRIVER);
// 2. 建立数据库连接
connection = DriverManager.getConnection(DB_URL, USER, PASSWORD);
// 3. 执行sql语句
statement = connection.createStatement();
for (String userName : userNames) {
statement.addBatch("insert into user(name) values('"+userName+"')");
}
statement.executeBatch();
statement.clearBatch();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 清理环境
StreamJDBC.closeResource(connection, statement, resultSet, null, null);
}
}
public static void main(String[] args) {
insertUser(Arrays.asList("xiaobai","xiaohei","xiaohuang"));
}
}
这时我们查看数据库,应该就能看到三条新增的用户了。(这里需要注意的是,需要设置id为自增id,否则会报主键不存在的错误)
数据库连接池
由于JDBC获取的一次数据库连接,就需要客户端与服务端进行4次的网络传输,而且一般来说,客户端和服务端不在同一台机器上,所以建立连接时间开销较大。同时,又由于数据库的最大连接数往往是有限的,如果不限制地频繁地建立数据库连接,一旦连接个数超过限值,就容易造成数据库的崩溃,从而影响正常业务的开发。在企业级的java web 开发中,数据库查询是最频繁的操作,因此,有必要对数据库连接做必要的管理。
连接池的作用
- 连接复用:从“创建连接”改变为“租借”,避免数据库连接的频繁创建。
- 限制最大并发连接数:在客户端实现最大并发连接数的限制,以及线程排队等功能。
DBCP连接池
DBCP是Apache开源的一个连接池项目,也是tomcat使用的连接池组件,在java开发中被广泛使用。其他用途广泛的连接池有hikari(日本开发的一个连接池,Sprintboot2.x默认支持的连接池)、druid(阿里开发的一个开源连接池)等。我们这里只介绍DBCP连接池。DBCP连接池的核心类库如下:
- commons-dbcp.jar
- commons-pool.jar
- commons-logging.jar
接下来,我们来演示如何使用DBCP连接池管理连接。首先,我们先引入DBCP依赖:
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-dbcp2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
<version>2.7.0</version>
</dependency>
创建DBCP连接池需要使用BasicDataSource类,它有以下几个核心方法:
代码如下:
public class DbcpJDBC {
private static BasicDataSource dbPool = null;
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study";
private static final String USER = "root";
private static final String PASSWORD = "123456";
public static void dbPoolInit(){
// 1.初始化Dbcp连接池
dbPool = new BasicDataSource();
dbPool.setUrl(DB_URL);
dbPool.setDriverClassName(JDBC_DRIVER);
dbPool.setUsername(USER);
dbPool.setPassword(PASSWORD);
}
public static void databaseOperation() {
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 2. 建立数据库连接
connection = dbPool.getConnection();
// 3. 执行sql语句
statement = connection.createStatement();
resultSet = statement.executeQuery("select name from user");
// 4. 获取执行结果
while (resultSet.next()) {
System.out.println("Hello " + resultSet.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 清理环境
StreamJDBC.closeResource(connection, statement, resultSet,null,null);
}
}
public static void main(String[] args)
{
dbPoolInit();
databaseOperation();
}
}
DBCP的高级配置与定期检查
DBCP有相关的方法,用于配置连接池的相关参数,比如最大连接数,最大空闲连接数等;DBCP还有几个关于定期检查的方法,用于定期检查服务端连接是否失效,从而防止租借到失效的连接。具体的方法使用参见如下的代码:
public static void dbPoolInit(){
// 1.初始化Dbcp连接池
dbPool = new BasicDataSource();
dbPool.setUrl(DB_URL);
dbPool.setDriverClassName(JDBC_DRIVER);
dbPool.setUsername(USER);
dbPool.setPassword(PASSWORD);
//高级设置
//设置初始连接数为1
dbPool.setInitialSize(1);
//设置最大连接数为10
dbPool.setMaxTotal(10);
//设置队列的最大等待时间为10秒
dbPool.setMaxWaitMillis(10000);
//设置最大空闲连接数为1,超过部分的连接,则自动被回收
dbPool.setMaxIdle(1);
//设置最小空闲连接数为1,小于它,则自动创建连接,一般来说,为了避免频繁地创建或者释放连接,建议将maxIdle和minIdle都设置为1
dbPool.setMinIdle(1);
//定期检查
//如果没有设置,mysql服务器会自动关闭空闲时间超过8小时的连接,这时候客户端却并不清楚连接已经被服务端关闭了
//当应用程序向连接池租借连接时,连接池可能会将失效的连接直接租借给客户端,客户端使用这个连接操作时,就会报相应的sql异常
//通过定期对连接池的空闲连接进行检查,在服务端关闭连接之前,我们保证将这些连接销毁掉,重新补充新的连接
//当连接空闲时,进行检查
dbPool.setTestWhileIdle(true);
//最小的空闲时间,当空闲时间超过该值时,自动销毁连接
dbPool.setMinEvictableIdleTimeMillis(10000);
//检查空闲时间的时间间隔,建议设置为小于服务器自动关闭连接的阈值时间,也就是说,如果没有设置空闲时间,mysql的默认空闲时间是8个小时,所以设置需要小于8个小时
dbPool.setTimeBetweenEvictionRunsMillis(1000);
}
我们可以测试我们配置地连接池参数有没有生效。为了演示方便,排除干扰参数,这里我们只设置了DBCP连接池地最大连接数为2。在代码里面,我们定义了两个方法,一个是jdbcTest()方法,通过普通地jdbc的方式查询数据;另外一个是dbcpTest()方法,通过dbcp连接池获取连接的方式查询数据库。两者都是在10秒内不断地进行查询。然后在main方法中,我们启动10个线程,去调用jdbcTest()方法。代码如下图:
public class ThreadJDBC {
private static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
private static final String DB_URL = "jdbc:mysql://localhost:3306/cloud_study";
private static final String USER = "root";
private static final String PASSWORD = "123456";
private static BasicDataSource dbPool = null;
public static void dbPoolInit() {
// 1.初始化Dbcp连接池
dbPool = new BasicDataSource();
dbPool.setUrl(DB_URL);
dbPool.setDriverClassName(JDBC_DRIVER);
dbPool.setUsername(USER);
dbPool.setPassword(PASSWORD);
//设置最大连接数为10
dbPool.setMaxTotal(2);
}
/**
* 纯粹JDBC查询
*/
public static void jdbcTest() {
//在10秒内不断地进行查询
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10000){
try {
Class.forName(JDBC_DRIVER);
databaseOperation(DriverManager.getConnection(DB_URL, USER, PASSWORD));
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* dbcp查询
*/
public static void dbcpTest() {
//在10秒内不断地进行查询
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10000){
try {
databaseOperation(dbPool.getConnection());
} catch (SQLException e) {
e.printStackTrace();
}
}
}
public static void databaseOperation(Connection connection) {
Statement statement = null;
ResultSet resultSet = null;
try {
// 3. 执行sql语句
statement = connection.createStatement();
resultSet = statement.executeQuery("select name from user");
// 4. 获取执行结果
while (resultSet.next()) {
System.out.println("Hello " + resultSet.getString("name"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// 5. 清理环境
StreamJDBC.closeResource(connection, statement, resultSet, null, null);
}
}
public static void main(String[] args) {
dbPoolInit();
for(int i=0;i<10;i++){
Thread thread = new Thread(() -> {
jdbcTest();
});
thread.start();
}
}
}
运行main方法,我们立即用show processlist命令查询创建的连接数。
可以看出,创建了10个连接。 接着我们以同样的方式,调用dbcpTest()方法,通过show processlist命令,我们可以看出,这时候创建出来的连接只有两个,所以说明我们的连接池配置成功了。