JavaRush /Java 博客 /Random-ZH /JDBC 或一切的开始
Viacheslav
第 3 级

JDBC 或一切的开始

已在 Random-ZH 群组中发布
在现代世界,没有数据存储就没有出路。使用数据库的历史始于很久以前,随着 JDBC 的出现。我建议记住一些构建在 JDBC 之上的现代框架都离不开的东西。此外,即使与他们一起工作,有时你也可能需要“回归本源”的机会。我希望这篇评论能够作为介绍或帮助您刷新记忆。
JDBC 或一切的开始 - 1

介绍

编程语言的主要目的之一是存储和处理信息。为了更好地理解数据存储的工作原理,值得花一些时间来了解应用程序的理论和架构。例如,您可以阅读文献,即 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 或一切的开始 - 2
该标准由规范“ JSR 221 JDBC 4.1 API ”描述。该规范告诉我们,JDBC API 提供了从 Java 编写的程序对关系数据库的编程访问。它还表明 JDBC API 是 Java 平台的一部分,因此包含在 Java SE 和 Java EE 中。JDBC API 在两个包中提供:java.sql 和 javax.sql。那我们就来认识一下他们吧。
JDBC 或一切的开始 - 3

工作开始

要了解 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。伟大的。我们还需要什么?有这样一个精彩的图表:
JDBC 或一切的开始 - 4
正如我们所看到的,这是合乎逻辑的,数据库是一个外部组件,不是 Java SE 原生的。解释起来很简单——数据库数量巨大,您可以使用任何一个。例如,有PostgreSQL、Oracle、MySQL、H2。这些数据库中的每一个都由称为数据库供应商的独立公司提供。每个数据库都是用自己的编程语言(不一定是 Java)编写的。为了能够从 Java 应用程序使用数据库,数据库提供程序编写了一个特殊的驱动程序,它是它自己的图像适配器。这种兼容 JDBC 的数据库(即具有 JDBC 驱动程序的数据库)也称为“JDBC 兼容数据库”。这里我们可以用计算机设备来类比。例如,在记事本中有一个“打印”按钮。每次按下它时,程序都会告诉操作系统记事本应用程序想要打印。而且你有一台打印机。要教会您的操作系统与 Canon 或 HP 打印机进行统一通信,您将需要不同的驱动程序。但对于作为用户的您来说,什么都不会改变。您仍然会按同一个按钮。与 JDBC 相同。您正在运行相同的代码,只是不同的数据库可能在后台运行。我认为这是一个非常明确的方法。每个这样的 JDBC 驱动程序都是某种工件、库、jar 文件。这是我们项目的依赖项。例如,我们可以选择数据库“ H2 Database ”,然后我们需要添加如下依赖项:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
如何找到依赖项以及如何描述它在数据库提供商的官方网站或“ Maven Central ”上都有说明。如您所知,JDBC 驱动程序不是数据库。但他只是一个向导。但是有一种叫做“内存数据库”的东西。这些数据库在应用程序的整个生命周期内都存在于内存中。通常,这通常用于测试或培训目的。这使您可以避免在计算机上安装单独的数据库服务器。这非常适合我们熟悉JDBC。我们的沙箱已准备就绪,我们可以开始了。
JDBC 或一切的开始 - 5

联系

所以,我们有一个 JDBC 驱动程序,我们有一个 JDBC API。我们记得,JDBC 代表 Java 数据库连接。因此,这一切都始于连接性——建立连接的能力。连接就是连接。让我们再次翻到JDBC 规范的正文并查看目录。在“ CHAPTER 4 Overview ”(概述)一章中我们翻到“ 4.1Establishing a Connection ”(建立连接)一节,据说有两种方式连接数据库:
  • 通过驱动管理器
  • 通过数据源
让我们来处理DriverManager。如前所述,DriverManager 允许您连接到指定 URL 处的数据库,并且还加载它在 CLASSPATH 中找到的 JDBC 驱动程序(在 JDBC 4.0 之前,您必须自己加载驱动程序类)。有一个单独的章节“第 9 章连接”介绍连接到数据库的内容。我们感兴趣的是如何通过 DriverManager 获取连接,因此我们对“9.3 DriverManager 类”部分感兴趣。它指示我们如何访问数据库:
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 连接,而不必每次都自己创建它。
JDBC 或一切的开始 - 6

声明

接下来我们对语句或表达式感兴趣。它们在“第 13 章声明”一章的文档中进行了描述。首先,它说有几种类型或类型的语句:
  • 语句:不包含参数的SQL表达式
  • PreparedStatement :包含输入参数的准备好的 SQL 语句
  • CallableStatement:能够从 SQL 存储过程获取返回值的 SQL 表达式。
因此,有了连接,我们就可以在该连接的框架内执行一些请求。因此,我们最初从 Connection 获取 SQL 表达式的实例是合乎逻辑的。您需要从创建一个表开始。让我们将表创建请求描述为字符串变量。怎么做?让我们使用一些教程,例如“ sqltutorial.org ”、“ sqlbolt.com ”、“ postgresqltutorial.com ”、“ codecademy.com ”。例如,我们使用khanacademy.org上的 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 中是如何表示的呢?它以结果集的形式呈现。
JDBC 或一切的开始 - 7

结果集

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 ”。
JDBC 或一切的开始 - 8

元数据

除了查询之外,到数据库的连接(即 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 或一切的开始 - 9

连接池

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 或一切的开始 - 10

交易

JDBC 最有趣的事情之一就是事务。在 JDBC 规范中,它们被分配到“第 10 章事务”一章。首先,有必要了解什么是交易。事务是一组对数据进行逻辑组合的顺序操作,作为一个整体进行处理或取消。使用 JDBC 时事务何时开始?正如规范所述,这是由 JDBC 驱动程序直接处理的。但通常情况下,当当前 SQL 语句需要事务且事务尚未创建时,新事务就会开始。交易什么时候结束?这是由自动提交属性控制的。如果启用自动提交,则事务将在 SQL 语句“完成”后完成。“完成”的含义取决于 SQL 表达式的类型:
  • 数据操作语言,也称为DML(插入、更新、删除)
    操作完成后事务就完成
  • Select 语句
    当 ResultSet 关闭时事务完成(ResultSet#close
  • 返回多个结果的 CallableStatement 和表达式
    当所有关联的 ResultSet 均已关闭且所有输出均已收到(包括更新次数)时
这正是 JDBC API 的行为方式。像往常一样,让我们​​为此编写一个测试:
@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 或一切的开始 - 11

绝缘等级

让我们打开 JDBC 规范的“10.2 事务隔离级别”小节。在这里,在进一步讨论之前,我想记住一下 ACID 这样的事情。ACID 描述了事务系统的要求。
  • 原子性:
    任何事务都不会部分提交给系统。要么执行其所有子操作,要么不执行任何子操作。
  • 一致性:
    根据定义,每笔成功的交易只记录有效的结果。
  • 隔离性:
    事务运行时,并发事务不应影响其结果。
  • 持久性:
    如果事务成功完成,对其所做的更改不会因任何失败而撤消。
当谈论事务隔离级别时,我们谈论的是“隔离”要求。隔离是一项昂贵的要求,因此在实际数据库中存在不能完全隔离事务的模式(可重复读隔离级别及更低级别)。维基百科对处理事务时可能出现的问题有很好的解释。这里值得阅读更多内容:“使用事务进行并行访问的问题”。在编写测试之前,让我们稍微更改一下 Gradle 构建脚本:添加一个带有属性的块,即我们项目的设置:
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 或一切的开始 - 12

底线

正如我们所看到的,JDBC 是 Java 手中处理数据库的强大工具。我希望这篇简短的评论能为您提供一个起点或帮助您刷新记忆。好吧,作为零食,需要一些额外的材料: #维亚切斯拉夫
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION