JAVA入门基础_JDBC和数据库连接池_基于MYSQL
JDBC 概述
JDBC 是什么?
-
JDBC 实际上就是为了能够访问不同的数据库,而提供的一套接口规范。
-
各个数据库厂商实现这套接口规范再提供相对应的 jar 包让 JAVA 程序能够操作对应的数据库。
-
学习 JDBC 就是在学习这一套接口规范,变成面向接口编程。
-
既然是外部为 JAVA 提供这些 jar 包,因此这些接口规范一般都在 java.sql 和 javax.sql 包中
JDBC 快速入门
- 既然 MYSQL 实现了 JDBC 这套接口的规范,那么当然我们需要用到 MYSQL 实现这套接口的 jar 包
mysql-connector-java-5.1.40.jar
使用 JDBC 连接数据库并进行操作的 5 个步骤
-
(1)注册相对应的数据库驱动
-
(2)获取连接对象
-
(3)获取到命令对象(操作数据库的对象)
-
(4)使用命令对象对数据库进行操作(若是查询操作会返回结果集)
-
(5)关闭连接(包括结果集、命令对象、连接对象)
获取数据库连接的最原始的方式
import com.mysql.jdbc.Driver;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.Statement;
/**
-
@author codeStars
@date 2022/8/17 9:35
*/
public class Test {public static void main(String[] args) throws Exception{
// 准备连接数据库需要的数据
String url = "jdbc:mysql://localhost:13306/mydb";
String user = "root";
String pwd = "abc123";<span class="hljs-comment">// 1. 获取到驱动对象并进行驱动注册</span> <span class="hljs-type">Driver</span> <span class="hljs-variable">driver</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">com</span>.mysql.jdbc.Driver(); DriverManager.registerDriver(driver); <span class="hljs-comment">// 2. 获取连接对象</span> <span class="hljs-type">Connection</span> <span class="hljs-variable">connection</span> <span class="hljs-operator">=</span> DriverManager.getConnection(url, user, pwd); <span class="hljs-comment">// 3. 获取到命令对象</span> <span class="hljs-type">Statement</span> <span class="hljs-variable">statement</span> <span class="hljs-operator">=</span> connection.createStatement(); <span class="hljs-comment">// 4. 对数据库进行操作</span> <span class="hljs-type">int</span> <span class="hljs-variable">affectedCount</span> <span class="hljs-operator">=</span> statement.executeUpdate(<span class="hljs-string">"INSERT INTO animal VALUES(2,'王五')"</span>); System.out.println(<span class="hljs-string">"影响的语句数量:"</span> + affectedCount); <span class="hljs-comment">// 5. 关闭连接</span> statement.close(); connection.close();
}
}
该方式的弊端
-
(1) 在创建 Driver 对象时,其中的静态代码块就会完成驱动的注册(我们相当于多此一举)
- 解决方法:只创建一个 Driver 对象,但是不再进行驱动的注册,但是还有如下的问题。
- (2)虽然只创建了一个 Driver 对象,但是 Driver 类中的静态代码块有这么一句
DriverManager.registerDriver(new Driver());
-
由此可以看出,在 Driver 类加载时,就会创建一个新的 Driver 对象来进行驱动的注册。
-
而我们又 new 了一个就会导致创建了 2 次 Driver 对象,造成了资源的浪费。
-
因此我们可以采用:
Class.forName("com.mysql.jdbc.Driver");
的方式完成驱动的注册。
-
- (3)可以看到我们连接数据库的参数都是直接编写在 JAVA 代码中的,这很不利于程序的扩展与维护。因此我们需要把配置信息提取出来。
- (4)扩展:在 MYSQL 驱动 5.1.6 之后,在程序运行时会在 MYSQL 提供的 jar 包的 META-INF\services\java.sql.Driver 文本中找到驱动的全限定类名,调用反射进行自动注册。但还是建议加上
Class.forName("com.mysql.jdbc.Driver");
常用的 API 介绍
DriverManager 驱动管理类
getConnection(String url,String user, String password)
:获取一个连接对象
Connection 接口
-
createStatement()
:获取一个 Statement 对象 -
prepareStatement(String sql)
:获取一个 PrepareStatement 对象,传入一个 sql 进行预编译
Statement 接口
-
exexuteUpdate(sql)
:执行增删改语句,返回影响的行数 -
executeQuery(sql)
:执行查询,返回 ResultSet 对象 -
execute(sql)
:执行任意的 sql 命令,返回 boolean 值
PrepareStatement 接口
-
exexuteUpdate()
:执行增删改语句,返回影响的行数 -
executeQuery()
:执行查询,返回 ResultSet 对象 -
execute(sql)
:执行任意的 sql 命令,返回 boolean 值 -
setXxx(占位符索引,占位符的值)
:为占位符赋对应的值,索引从 1 开始 -
setObject(占位符索引,占位符的值)
:为占位符赋对应的值,索引从 1 开始
为什么相较于 Statement 接口,我们要使用 PreparedStatement 接口
-
(1)Statement 接口有个致命的缺陷,就是 SQL 注入问题
-
(2)PreparedStatement 解决了 SQL 注入的问题。
-
(3)PreparedStatement 会提前对 sql 命令进行预编译,使得 sql 命令的执行效率提升
封装 JDBCUtils
为什么要封装一个 JDBCUtils 呢
-
(1)若是每个方法中都把获取连接等重复代码全都写一遍,会造成大量代码的冗余。
-
(2)其实就是为了减少代码的冗余
一个非常简单的 JDBCUtils(附代码)
- (1)提供一个配置文件,放在 src 目录下
url=jdbc:mysql://localhost:13306/mydb
user=root
pwd=abc123
driver=com.mysql.jdbc.Driver
- (2)编写 JDBCUtils 工具类
import java.io.FileInputStream;
import java.sql.*;
import java.util.Properties;
public class JDBCUtils {
private static String user; // 用户名
private static String pwd; // 密码
private static String url; //url
private static String driver; // 驱动名
<span class="hljs-keyword">static</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// 1. 获取配置文件信息</span>
<span class="hljs-type">Properties</span> <span class="hljs-variable">props</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Properties</span>();
props.load(<span class="hljs-keyword">new</span> <span class="hljs-title class_">FileInputStream</span>(<span class="hljs-string">"src\\jdbc.Properties"</span>));
<span class="hljs-comment">// 2. 为静态变量赋值</span>
user = props.getProperty(<span class="hljs-string">"user"</span>);
pwd = props.getProperty(<span class="hljs-string">"pwd"</span>);
url = props.getProperty(<span class="hljs-string">"url"</span>);
driver = props.getProperty(<span class="hljs-string">"driver"</span>);
<span class="hljs-comment">// 3. 注册驱动(其实不写也行)</span>
Class.forName(driver);
} <span class="hljs-keyword">catch</span> (Exception e) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">RuntimeException</span>(e);
}
}
<span class="hljs-comment">/**
* 获取连接
* <span class="hljs-doctag">@return</span>
* <span class="hljs-doctag">@throws</span> SQLException
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> Connection <span class="hljs-title function_">getConnection</span><span class="hljs-params">()</span> <span class="hljs-keyword">throws</span> SQLException {
<span class="hljs-keyword">return</span> DriverManager.getConnection(url, user, pwd);
}
<span class="hljs-comment">/**
* 关闭连接
*/</span>
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">close</span><span class="hljs-params">(ResultSet resultSet, Statement statement, Connection connection)</span> {
<span class="hljs-keyword">try</span> {
<span class="hljs-keyword">if</span> (resultSet != <span class="hljs-literal">null</span>) {
resultSet.close();
}
<span class="hljs-keyword">if</span> (statement != <span class="hljs-literal">null</span>) {
statement.close();
}
<span class="hljs-keyword">if</span> (connection != <span class="hljs-literal">null</span>) {
connection.close();
}
} <span class="hljs-keyword">catch</span> (SQLException throwables) {
<span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">RuntimeException</span>(throwables);
}
}
}
事物(数据一致性问题)
-
只需要在一个事务的最开头,将自动提交设置为 false
-
在事务结束位置,自动提交数据
-
在 catch 中进行数据的回滚
举个小例子
public class Test {
<span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String[] args)</span> <span class="hljs-keyword">throws</span> Exception{
<span class="hljs-comment">// 1.获取连接</span>
<span class="hljs-type">Connection</span> <span class="hljs-variable">connection</span> <span class="hljs-operator">=</span> JDBCUtils.getConnection();
<span class="hljs-comment">// 2. 获取到命令对象</span>
<span class="hljs-type">String</span> <span class="hljs-variable">sql1</span> <span class="hljs-operator">=</span> <span class="hljs-string">"UPDATE account SET money = money - 100 WHERE id = 1"</span>;
<span class="hljs-type">String</span> <span class="hljs-variable">sql2</span> <span class="hljs-operator">=</span> <span class="hljs-string">"UPDATE account SET money = money + 100 WHERE id = 2"</span>;
<span class="hljs-type">PreparedStatement</span> <span class="hljs-variable">preparedStatement1</span> <span class="hljs-operator">=</span> connection.prepareStatement(sql1);
<span class="hljs-type">PreparedStatement</span> <span class="hljs-variable">preparedStatement2</span> <span class="hljs-operator">=</span> connection.prepareStatement(sql2);
<span class="hljs-keyword">try</span> {
<span class="hljs-comment">// 设置自动提交为false</span>
connection.setAutoCommit(<span class="hljs-literal">false</span>);
<span class="hljs-comment">// 事务开始</span>
preparedStatement1.execute();
<span class="hljs-type">int</span> <span class="hljs-variable">i</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>/<span class="hljs-number">0</span>;
preparedStatement2.execute();
<span class="hljs-comment">// 事务结束,提交数据</span>
connection.commit();
}<span class="hljs-keyword">catch</span> (Exception e) {
<span class="hljs-comment">// 出现异常,回滚</span>
connection.rollback();
e.printStackTrace();
}
}
}
批处理
准备步骤
-
连接数据库的 URL 需要添加上
rewriteBatchedStatements
参数,示例如下: -
url=jdbc:mysql://localhost:13306/mydb?rewriteBatchedStatements=true
批处理的原理
-
addBatch():第一次时会创建一个 ArrayList<PreparedStatement>,赋值给 PreparedStatement 对象中的 batchedArgs。往后执行的每次 addBatch() 都会不断的往开始创建的 ArrayList 集合中存储一个个的 PreparedStatement。
-
executeBatch():发送一次请求给 MYSQL 进行批量的处理。
-
clearBatch():清空 batchedArgs 集合,这样之前添加的批处理对象就释放了。
使用实例
// 3. 批处理添加 1000 条数据
for (int i = 0; i < 1000; i++) {
// 3.1 添加参数
preparedStatement.setInt(1,i);
<span class="hljs-comment">// 3.2 添加到批处理中</span>
preparedStatement<span class="hljs-selector-class">.addBatch</span>();
}
<span class="hljs-comment">// 3.3 一次性批量处理</span>
preparedStatement<span class="hljs-selector-class">.executeBatch</span>();
<span class="hljs-comment">// 3.4 清空批量处理</span>
preparedStatement<span class="hljs-selector-class">.clearBatch</span>();
数据库连接池(解决了反复连接占用大量的网络资源)
为什么要使用数据库连接池
-
(1)就目前而言,每次连接数据库都需要获取连接,并且使用后就直接关闭了与数据库的连接
-
(2)会占用大量的网络资源,如果有大量的 JAVA 程序同时获取数据库的连接,那么数据库很可能不堪重负直接崩溃。
-
(3)针对如上问题,提出了线程池的思想。
-
在程序刚运行时,就获取一定量的数据库连接对象,将其放在一个容器当中。
-
在使用完了该数据库连接对象时,不直接关闭该对象与数据库的连接,而是将其放回容器当中。
-
综上就可以完成数据库连接对象的复用,减少了反复获取连接而造成的资源占用。
-
C3P0 线程池
准备工作
- 既然想要使用的是其他人写好的线程池,那么当然就需要获取到对应的 jar 包了
c3p0-0.9.2.1.jar
mchange-commons-java-0.2.20.jar
mysql-connector-java-5.1.40.jar
- 在 scr 目录下添加一个配置文件 c3p0-config.xml,创建 ComboPooledDataSource 对象时自动读取该配置文件
<c3p0-config>
<!-- 使用默认的配置读取数据库连接池对象 -->
<default-config>
<!-- 连接参数 -->
<property name="driverClass">com.mysql.jdbc.Driver</property>
<property name="jdbcUrl">jdbc:mysql://localhost:13306/mydb?useSSL=false</property>
<property name="user">root</property>
<property name="password">abc123</property>
<span class="hljs-comment"><!-- 连接池参数 --></span>
<span class="hljs-comment"><!--初始化申请的连接数量--></span>
<span class="hljs-tag"><<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"initialPoolSize"</span>></span>5<span class="hljs-tag"></<span class="hljs-name">property</span>></span>
<span class="hljs-comment"><!--最大的连接数量--></span>
<span class="hljs-tag"><<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"maxPoolSize"</span>></span>10<span class="hljs-tag"></<span class="hljs-name">property</span>></span>
<span class="hljs-comment"><!--最小的连接数量,当线程池空闲时,会将线程池中的连接释放到只剩下5个 --></span>
<span class="hljs-tag"><<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"minPoolSize"</span>></span>5<span class="hljs-tag"></<span class="hljs-name">property</span>></span>
<span class="hljs-comment"><!--超时时间,如果超过该时间没有获取到数据库连接,就抛出异常,单位为毫秒 --></span>
<span class="hljs-tag"><<span class="hljs-name">property</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"checkoutTimeout"</span>></span>3000<span class="hljs-tag"></<span class="hljs-name">property</span>></span>
<span class="hljs-tag"></<span class="hljs-name">default-config</span>></span>
<!-- 注意:在创建 ComboPooledDataSource 时,可以指定配置的名称,这里做一个示例 -->
<!-- <named-config name="other_c3p0">-->
<!-- <property name="driverClass">com.mysql.jdbc.Driver</property>-->
<!-- <property name="jdbcUrl">jdbc:mysql://localhost:13306/mydb?useSSL=false</property>-->
<!-- <property name="user">root</property>-->
<!-- <property name="password">root</property>-->
<span class="hljs-comment"><!-- <property name="initialPoolSize">5</property>--></span>
<span class="hljs-comment"><!-- <property name="maxPoolSize">10</property>--></span>
<span class="hljs-comment"><!-- <property name="checkoutTimeout">3000</property>--></span>
<span class="hljs-comment"><!-- </named-config>--></span>
</c3p0-config>
获取连接的示例
// ComboPooledDataSource dataSource = new ComboPooledDataSource("other_c3p0");
ComboPooledDataSource dataSource = new ComboPooledDataSource();
// 获取连接
Connection connection = dataSource.getConnection();
// 该行其实就是将连接放回线程池当中,并不是切断与 MYSQL 的连接。注意!!!
connection.close();
Druid 线程池(推荐)
准备工作
- 既然想要使用的是其他人写好的线程池,那么当然就需要获取到对应的 jar 包了
c3p0-0.9.2.1.jar
mchange-commons-java-0.2.20.jar
mysql-connector-java-5.1.40.jar
- 在 scr 目录下添加一个配置文件 c3p0-config.xml,创建 ComboPooledDataSource 对象时自动读取该配置文件
driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:13306/mydb?useSSL=false
username=root
password=abc123
# 初始线程数
initialSize=5
# 最大线程数
maxActive=10
# 超时时间
maxWait=3000
获取连接示例
public static void main(String[] args) throws Exception{
// 获取到配置文件
Properties props = new Properties();
props.load(new FileInputStream("src\\druid.properties"));
<span class="hljs-comment">// 1. 获取数据源</span>
<span class="hljs-type">DataSource</span> <span class="hljs-variable">dataSource</span> <span class="hljs-operator">=</span> DruidDataSourceFactory.createDataSource(props);
<span class="hljs-comment">// 2. 获取连接</span>
<span class="hljs-type">Connection</span> <span class="hljs-variable">connection</span> <span class="hljs-operator">=</span> dataSource.getConnection();
System.out.println(connection);
<span class="hljs-comment">// 3. 关闭连接(其实是将连接对象放回线程池)</span>
connection.close();
}
Apache-DBUtils 工具类(ORM 对象关系映射)
ResultSet 有什么弊端?
-
(1)ResultSet 结果集与 Connection 连接是关联的,一旦 Connection 连接关闭,将会导致 ResultSet 不可用
-
(2)ResultSet 不利于数据管理(因为只能使用一次,毕竟不可能一直不关闭连接)
-
(3)ResultSet 返回的信息难以处理,只能通过 getXxx 这种方式获取,不利于维护。
那么我们应该如何解决呢?
先想一想,ResultSet 返回的是数据表中一行行的数据,那么我们是不是可以定义一个 JAVA 类与其字段进行匹配呢,这个思想称之为:ORM 对象关系映射,这样的类称之为 JaveBean 或 pojo 或 domain
-
(1)获取到一个 ResultSet 集合,我们将其中的一行行数据分别存储到一个个对应的 JavaBean 当中,再使用一个集合(例如 ArrayList)来进行存储
-
(2)完成了上述步骤后,Connection 连接对象即便关闭了,也不会影响到 ArrayList 容器中已经存储好的数据
-
(3)而 Apache-DBUtils 就很好的帮我们封装了上述的内容,非常好用。
Apache-DBUtils 使用的准备工作
- 准备 jar 包:
commons-dbutils-1.7.jar
Apache-DBUtils 常用 API
QueryRunner 指令执行类
-
update()
:增删改 -
query()
:查询
ResultSetHandler 各处理器的作用
处理器的类名 | 作用 |
---|---|
ArrayHandler | 把结果集中的第一行数据转成对象数组 |
ArrayListHandler | 把结果集中的每一行数据都转成一个对象数组,再存放到 List 中 |
BeanHandler | 将结果集中的第一行数据封装到一个对应的 JavaBean 中 |
BeanListHandler | 将结果集中的每一行数据都封装到一个对应的 JavaBean 中,存放到 List |
MapHandler | 将结果集中的第一行数据封装到一个 Map 里,key 是列名,value 就是对应的值 |
MapListHandler | 将结果集中的每一行数据都封装到一个 Map 里,然后再存放到 List |
ColumnListHandler | 将结果集中某一列的数据存放到 List 中 |
KeyedHandler(name) | 将结果集中的每一行数据都封装到一个 Map 里 (List<Map>),再把这些 map 再存到一个 map 里,其 key 为指定的列 |
ScalarHandler | 将结果集第一行的某一列放到某个对象中 |
DAO 和增删改查(提供通用的数据库访问对象,减少代码的冗余)
什么是 DAO,为什么要使用 DAO
-
DAO 的英文全程是 Data Access Object 数据访问对象。理解为直接用于访问数据库的对象。
-
思考:我们在编写代码时,如果需要获取多个不同数据表当中的数据,我们应该怎么办?
传统的解决思维:不同的表,就对应不同的 JavaBean。不过每次数据的获取,都要编写不同的代码,比如编写 sql 时,A 表就写 select A 表,B 表就写 select B 表。
如上的思维的问题在于:如果有相同的功能,比如说都是想获取表中的所有数据,都是想要通过 id 取得一条数据,都是想进行一个普通的增删改,那么依然要编写不同的代码,造成代码的大量冗余,也不便于管理
这个时候我们就可以将通用的方法,抽取为一个BasisDao,实现相对应的通用方法。再编写其余访问不同数据表的 Dao来继承 BasisDao,其中只需要编写专属于自己的特有方法。
小提示
编写工具类处理异常的小建议
-
可以不抛出编译时异常,直接抛出运行时异常。
-
这样的好处是:调用者可以选择捕获处理或者默认处理。