在现代世界,没有数据存储就没有出路。使用数据库的历史始于很久以前,随着 JDBC 的出现。我建议记住一些构建在 JDBC 之上的现代框架都离不开的东西。此外,即使与他们一起工作,有时你也可能需要“回归本源”的机会。我希望这篇评论能够作为介绍或帮助您刷新记忆。
该标准由规范“ JSR 221 JDBC 4.1 API ”描述。该规范告诉我们,JDBC API 提供了从 Java 编写的程序对关系数据库的编程访问。它还表明 JDBC API 是 Java 平台的一部分,因此包含在 Java SE 和 Java EE 中。JDBC API 在两个包中提供:java.sql 和 javax.sql。那我们就来认识一下他们吧。
正如我们所看到的,这是合乎逻辑的,数据库是一个外部组件,不是 Java SE 原生的。解释起来很简单——数据库数量巨大,您可以使用任何一个。例如,有PostgreSQL、Oracle、MySQL、H2。这些数据库中的每一个都由称为数据库供应商的独立公司提供。每个数据库都是用自己的编程语言(不一定是 Java)编写的。为了能够从 Java 应用程序使用数据库,数据库提供程序编写了一个特殊的驱动程序,它是它自己的图像适配器。这种兼容 JDBC 的数据库(即具有 JDBC 驱动程序的数据库)也称为“JDBC 兼容数据库”。这里我们可以用计算机设备来类比。例如,在记事本中有一个“打印”按钮。每次按下它时,程序都会告诉操作系统记事本应用程序想要打印。而且你有一台打印机。要教会您的操作系统与 Canon 或 HP 打印机进行统一通信,您将需要不同的驱动程序。但对于作为用户的您来说,什么都不会改变。您仍然会按同一个按钮。与 JDBC 相同。您正在运行相同的代码,只是不同的数据库可能在后台运行。我认为这是一个非常明确的方法。每个这样的 JDBC 驱动程序都是某种工件、库、jar 文件。这是我们项目的依赖项。例如,我们可以选择数据库“ H2 Database ”,然后我们需要添加如下依赖项:
介绍
编程语言的主要目的之一是存储和处理信息。为了更好地理解数据存储的工作原理,值得花一些时间来了解应用程序的理论和架构。例如,您可以阅读文献,即 Joseph Ingeno 所著的《软件架构师手册:通过实施有效的架构成为一名成功的软件架构师...》一书。如前所述,存在一定的数据层或“数据层”。它包括存储数据的位置(例如,SQL 数据库)和用于处理数据存储的工具(例如,我们将讨论的 JDBC)。微软网站上还有一篇文章:《设计基础设施持久层》,描述了从数据层中分离出一个附加层——持久层的架构解决方案。在这种情况下,数据层是数据本身的存储级别,而持久层是用于处理来自数据层级别的存储的数据的某种抽象级别。持久层可以包括“DAO”模板或各种 ORM。但 ORM 是另一个讨论的话题。您可能已经了解,数据层首先出现。从 JDK 1.1 开始,Java 世界就出现了 JDBC(Java DataBase Connectivity - Java 中的数据库连接)。这是 Java 应用程序与各种 DBMS 交互的标准,以 Java SE 中包含的 java.sql 和 javax.sql 包的形式实现:工作开始
要了解 JDBC API 的一般含义,我们需要一个 Java 应用程序。使用项目组装系统之一是最方便的。例如,让我们使用Gradle。您可以在简短的评论中阅读有关 Gradle 的更多信息:“ Gradle 简介”。首先,让我们初始化一个新的 Gradle 项目。由于Gradle的功能是通过插件实现的,所以我们需要使用“ Gradle Build Init Plugin ”进行初始化:gradle init --type java-application
之后,让我们打开构建脚本 - build.gradle文件,它描述了我们的项目以及如何使用它。我们对“依赖项”块感兴趣,其中描述了依赖项 - 即那些库/框架/api,没有它们我们就无法工作并且依赖它们。默认情况下我们会看到类似以下内容:
dependencies {
// This dependency is found on compile classpath of this component and consumers.
implementation 'com.google.guava:guava:26.0-jre'
// Use JUnit test framework
testImplementation 'junit:junit:4.12'
}
为什么我们会在这里看到这个?这些是我们项目的依赖项,是我们创建项目时Gradle自动生成的。还因为 guava 是一个独立的库,不包含在 Java SE 中。JUnit 也不包含在 Java SE 中。但我们有开箱即用的 JDBC,也就是说,它是 Java SE 的一部分。事实证明我们有 JDBC。伟大的。我们还需要什么?有这样一个精彩的图表:
dependencies {
implementation 'com.h2database:h2:1.4.197'
如何找到依赖项以及如何描述它在数据库提供商的官方网站或“ Maven Central ”上都有说明。如您所知,JDBC 驱动程序不是数据库。但他只是一个向导。但是有一种叫做“内存数据库”的东西。这些数据库在应用程序的整个生命周期内都存在于内存中。通常,这通常用于测试或培训目的。这使您可以避免在计算机上安装单独的数据库服务器。这非常适合我们熟悉JDBC。我们的沙箱已准备就绪,我们可以开始了。
联系
所以,我们有一个 JDBC 驱动程序,我们有一个 JDBC API。我们记得,JDBC 代表 Java 数据库连接。因此,这一切都始于连接性——建立连接的能力。连接就是连接。让我们再次翻到JDBC 规范的正文并查看目录。在“ CHAPTER 4 Overview ”(概述)一章中我们翻到“ 4.1Establishing a Connection ”(建立连接)一节,据说有两种方式连接数据库:- 通过驱动管理器
- 通过数据源
Connection con = DriverManager.getConnection(url, user, passwd);
这些参数可以从我们选择的数据库的网站上获取。在我们的例子中,这是 H2 - “ H2 Cheat Sheet ”。让我们继续看 Gradle 准备的 AppTest 类。它包含 JUnit 测试。JUnit 测试是一种用注释标记的方法@Test
。单元测试不是本次审查的主题,因此我们将简单地限制自己的理解,即这些是以某种方式描述的方法,其目的是测试某些东西。根据 JDBC 规范和 H2 网站,我们将检查是否已收到与数据库的连接。我们来写一个获取连接的方法:
private Connection getNewConnection() throws SQLException {
String url = "jdbc:h2:mem:test";
String user = "sa";
String passwd = "sa";
return DriverManager.getConnection(url, user, passwd);
}
现在让我们为此方法编写一个测试,以检查连接是否实际建立:
@Test
public void shouldGetJdbcConnection() throws SQLException {
try(Connection connection = getNewConnection()) {
assertTrue(connection.isValid(1));
assertFalse(connection.isClosed());
}
}
执行此测试时,将验证生成的连接是否有效(正确创建)并且未关闭。通过使用try-with-resources,我们将在不再需要资源时释放它们。这将保护我们免受连接松弛和内存泄漏的影响。由于与数据库的任何操作都需要连接,因此我们在测试开始时提供标记为 @Test 的其余测试方法,并带有 Connection,我们将在测试后发布。为此,我们需要两个注释:@Before 和 @After 让我们向 AppTest 类添加一个新字段,用于存储用于测试的 JDBC 连接:
private static Connection connection;
让我们添加新方法:
@Before
public void init() throws SQLException {
connection = getNewConnection();
}
@After
public void close() throws SQLException {
connection.close();
}
现在,任何测试方法都保证有一个 JDBC 连接,而不必每次都自己创建它。
声明
接下来我们对语句或表达式感兴趣。它们在“第 13 章声明”一章的文档中进行了描述。首先,它说有几种类型或类型的语句:- 语句:不包含参数的SQL表达式
- PreparedStatement :包含输入参数的准备好的 SQL 语句
- CallableStatement:能够从 SQL 存储过程获取返回值的 SQL 表达式。
private int executeUpdate(String query) throws SQLException {
Statement statement = connection.createStatement();
// Для Insert, Update, Delete
int result = statement.executeUpdate(query);
return result;
}
让我们添加一个使用之前的方法创建测试表的方法:
private void createCustomerTable() throws SQLException {
String customerTableQuery = "CREATE TABLE customers " +
"(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)";
String customerEntryQuery = "INSERT INTO customers " +
"VALUES (73, 'Brian', 33)";
executeUpdate(customerTableQuery);
executeUpdate(customerEntryQuery);
}
现在让我们测试一下:
@Test
public void shouldCreateCustomerTable() throws SQLException {
createCustomerTable();
connection.createStatement().execute("SELECT * FROM customers");
}
现在让我们执行请求,甚至带有参数:
@Test
public void shouldSelectData() throws SQLException {
createCustomerTable();
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
}
JDBC 不支持PreparedStatement 的命名参数,因此参数本身由问题指定,通过指定值我们指示问题索引(从1 开始,而不是0)。在上次测试中,我们收到 true 作为是否有结果的指示。但是查询结果在 JDBC API 中是如何表示的呢?它以结果集的形式呈现。
结果集
ResultSet 的概念在 JDBC API 规范的“第 15 章结果集”一章中进行了描述。首先,它说 ResultSet 提供了检索和操作执行查询结果的方法。也就是说,如果execute方法返回true给我们,那么我们就可以获得一个ResultSet。我们将对 createCustomerTable() 方法的调用移至 init 方法中,该方法被标记为 @Before。现在让我们完成 shouldSelectData 测试:@Test
public void shouldSelectData() throws SQLException {
String query = "SELECT * FROM customers WHERE name = ?";
PreparedStatement statement = connection.prepareStatement(query);
statement.setString(1, "Brian");
boolean hasResult = statement.execute();
assertTrue(hasResult);
// Обработаем результат
ResultSet resultSet = statement.getResultSet();
resultSet.next();
int age = resultSet.getInt("age");
assertEquals(33, age);
}
这里值得注意的是,next 是移动所谓“光标”的方法。ResultSet 中的游标指向某行。因此,为了读取一行,您需要将光标放在该行上。当光标移动时,如果光标有效(正确,正确),即指向数据,则光标移动方法返回true。如果返回false,则表示没有数据,即光标没有指向数据。如果我们尝试使用无效游标获取数据,我们将收到错误:没有可用数据。同样有趣的是,通过 ResultSet 您可以更新甚至插入行:
@Test
public void shouldInsertInResultSet() throws SQLException {
Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
ResultSet resultSet = statement.executeQuery("SELECT * FROM customers");
resultSet.moveToInsertRow();
resultSet.updateLong("id", 3L);
resultSet.updateString("name", "John");
resultSet.updateInt("age", 18);
resultSet.insertRow();
resultSet.moveToCurrentRow();
}
行集
除了ResultSet之外,JDBC还引入了RowSet的概念。您可以在此处阅读更多内容:“ JDBC 基础知识:使用 RowSet 对象”。有多种用途。例如,最简单的情况可能如下所示:@Test
public void shouldUseRowSet() throws SQLException {
JdbcRowSet jdbcRs = new JdbcRowSetImpl(connection);
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
jdbcRs.next();
String name = jdbcRs.getString("name");
assertEquals("Brian", name);
}
正如你所看到的,RowSet类似于语句(我们通过它指定命令)和执行命令的共生体。通过它我们控制光标(通过调用 next 方法)并从中获取数据。这种方法不仅有趣,而且还有可能的实现方式。例如,CachedRowSet。它是“断开连接”的(即,它不使用与数据库的持久连接)并且需要与数据库显式同步:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
您可以在 Oracle 网站上的教程中阅读更多内容:“ Using CachedRowSetObjects ”。
元数据
除了查询之外,到数据库的连接(即 Connection 类的实例)还提供对元数据的访问 - 有关如何配置和组织数据库的数据。但首先,我们提几个关键点: 连接数据库的 URL:“jdbc:h2:mem:test”。test 是我们数据库的名称。对于 JDBC API,这是一个目录。并且名称将是大写的,即 TEST。H2的默认模式是 PUBLIC。现在,让我们编写一个显示所有用户表的测试。为什么要定制?因为数据库不仅包含用户表(我们自己使用create table表达式创建的表),还包含系统表。它们对于存储有关数据库结构的系统信息是必要的。每个数据库可以以不同的方式存储此类系统表。例如,在 H2 中,它们存储在“ INFORMATION_SCHEMA ”模式中。有趣的是,INFORMATION SCHEMA 是一种常见的方法,但 Oracle 走了一条不同的路线。您可以在这里阅读更多内容:“ INFORMATION_SCHEMA 和 Oracle ”。让我们编写一个接收用户表元数据的测试:@Test
public void shoudGetMetadata() throws SQLException {
// У нас URL = "jdbc:h2:mem:test", где test - название БД
// Название БД = catalog
DatabaseMetaData metaData = connection.getMetaData();
ResultSet result = metaData.getTables("TEST", "PUBLIC", "%", null);
List<String> tables = new ArrayList<>();
while(result.next()) {
tables.add(result.getString(2) + "." + result.getString(3));
}
assertTrue(tables.contains("PUBLIC.CUSTOMERS"));
}
连接池
JDBC规范中的连接池有一个章节叫做“第11章连接池”。它还提供了需要连接池的主要理由。每个 Coonection 都是到数据库的物理连接。它的创建和关闭是一项相当“昂贵”的工作。JDBC仅提供连接池API。因此,实施的选择仍然是我们的。例如,此类实现包括HikariCP。因此,我们需要向项目依赖项添加一个池:dependencies {
implementation 'com.h2database:h2:1.4.197'
implementation 'com.zaxxer:HikariCP:3.3.1'
testImplementation 'junit:junit:4.12'
}
现在我们需要以某种方式使用这个池。为此,您需要初始化数据源,也称为数据源:
private DataSource getDatasource() {
HikariConfig config = new HikariConfig();
config.setUsername("sa");
config.setPassword("sa");
config.setJdbcUrl("jdbc:h2:mem:test");
DataSource ds = new HikariDataSource(config);
return ds;
}
让我们编写一个测试来从池中接收连接:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
DataSource datasource = getDatasource();
try (Connection con = datasource.getConnection()) {
assertTrue(con.isValid(1));
}
}
交易
JDBC 最有趣的事情之一就是事务。在 JDBC 规范中,它们被分配到“第 10 章事务”一章。首先,有必要了解什么是交易。事务是一组对数据进行逻辑组合的顺序操作,作为一个整体进行处理或取消。使用 JDBC 时事务何时开始?正如规范所述,这是由 JDBC 驱动程序直接处理的。但通常情况下,当当前 SQL 语句需要事务且事务尚未创建时,新事务就会开始。交易什么时候结束?这是由自动提交属性控制的。如果启用自动提交,则事务将在 SQL 语句“完成”后完成。“完成”的含义取决于 SQL 表达式的类型:- 数据操作语言,也称为DML(插入、更新、删除)
操作完成后事务就完成 Select 语句
- 返回多个结果的 CallableStatement 和表达式
当所有关联的 ResultSet 均已关闭且所有输出均已收到(包括更新次数)时
当 ResultSet 关闭时事务完成(ResultSet#close)
@Test
public void shouldCommitTransaction() throws SQLException {
connection.setAutoCommit(false);
String query = "INSERT INTO customers VALUES (1, 'Max', 20)";
connection.createStatement().executeUpdate(query);
connection.commit();
Statement statement = connection.createStatement();
statement.execute("SELECT * FROM customers");
ResultSet resultSet = statement.getResultSet();
int count = 0;
while(resultSet.next()) {
count++;
}
assertEquals(2, count);
}
这很简单。但只要我们只有一笔交易,这就是事实。当有多个时该怎么办?他们需要彼此隔离。因此,我们来谈谈事务隔离级别以及 JDBC 如何处理它们。
绝缘等级
让我们打开 JDBC 规范的“10.2 事务隔离级别”小节。在这里,在进一步讨论之前,我想记住一下 ACID 这样的事情。ACID 描述了事务系统的要求。- 原子性:
任何事务都不会部分提交给系统。要么执行其所有子操作,要么不执行任何子操作。 - 一致性:
根据定义,每笔成功的交易只记录有效的结果。 - 隔离性:
事务运行时,并发事务不应影响其结果。 - 持久性:
如果事务成功完成,对其所做的更改不会因任何失败而撤消。
ext {
h2Version = '1.3.176' // 1.4.177
hikariVersion = '3.3.1'
junitVersion = '4.12'
}
接下来,我们在版本中使用它:
dependencies {
implementation "com.h2database:h2:${h2Version}"
implementation "com.zaxxer:HikariCP:${hikariVersion}"
testImplementation "junit:junit:${junitVersion}"
}
您可能已经注意到,h2 版本变低了。我们稍后会看到原因。那么如何应用隔离级别呢?让我们立即看一个实际的小例子:
@Test
public void shouldGetReadUncommited() throws SQLException {
Connection first = getNewConnection();
assertTrue(first.getMetaData().supportsTransactionIsolationLevel(Connection.TRANSACTION_READ_UNCOMMITTED));
first.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
first.setAutoCommit(false);
// Транзакиця на подключение. Поэтому первая транзакция с ReadUncommited вносит изменения
String insertQuery = "INSERT INTO customers VALUES (5, 'Max', 15)";
first.createStatement().executeUpdate(insertQuery);
// Вторая транзакция пытается их увидеть
int rowCount = 0;
JdbcRowSet jdbcRs = new JdbcRowSetImpl(getNewConnection());
jdbcRs.setCommand("SELECT * FROM customers");
jdbcRs.execute();
while (jdbcRs.next()) {
rowCount++;
}
assertEquals(2, rowCount);
}
有趣的是,此测试可能会在不支持 TRANSACTION_READ_UNCOMMITTED 的供应商(例如 sqlite 或 HSQL)上失败。而且交易级别可能根本不起作用。还记得我们指出了 H2 数据库驱动程序的版本吗?如果我们将其提高到 h2Version = '1.4.177' 及更高版本,那么 READ UNCOMMITTED 将停止工作,尽管我们没有更改代码。这再次证明供应商和驱动程序版本的选择不仅仅是字母,它实际上将决定您的请求将如何执行。您可以在此处阅读有关如何在版本 1.4.177 中修复此行为以及它如何在更高版本中不起作用的信息:“在 MVStore 模式下支持 READ UNCOMMITTED 隔离级别”。
底线
正如我们所看到的,JDBC 是 Java 手中处理数据库的强大工具。我希望这篇简短的评论能为您提供一个起点或帮助您刷新记忆。好吧,作为零食,需要一些额外的材料:- 火灾报告:“交易:神话、惊喜和机遇”,马丁·克莱普曼 (Martin Kleppmann)
- Yuri Tkach:“ JPA. 交易”
- Yurik Tkach:“ JDBC - 测试人员的 Java ”
- Udemy 上的免费课程:“ JDBC 和 MySQL ”
- “处理 CallableStatement 对象”
- IBM 开发人员:“ Java 数据库连接”
- IBM 知识中心:“ JDBC 入门”
GO TO FULL VERSION