JavaRush /Blog Java /Random-VI /JDBC hoặc nơi tất cả bắt đầu
Viacheslav
Mức độ

JDBC hoặc nơi tất cả bắt đầu

Xuất bản trong nhóm
Trong thế giới hiện đại, không thể thiếu việc lưu trữ dữ liệu. Và lịch sử làm việc với cơ sở dữ liệu đã bắt đầu từ rất lâu trước đây, với sự ra đời của JDBC. Tôi đề nghị ghi nhớ một điều mà không có khung công tác hiện đại nào được xây dựng dựa trên JDBC có thể làm được nếu không có. Ngoài ra, ngay cả khi làm việc với họ, đôi khi bạn cũng có thể cần có cơ hội “trở về cội nguồn”. Tôi hy vọng bài đánh giá này sẽ giúp ích như một lời giới thiệu hoặc giúp làm mới trí nhớ của bạn.
JDBC hay nơi mọi chuyện bắt đầu - 1

Giới thiệu

Một trong những mục đích chính của ngôn ngữ lập trình là lưu trữ và xử lý thông tin. Để hiểu rõ hơn về cách hoạt động của việc lưu trữ dữ liệu, bạn nên dành một chút thời gian cho lý thuyết và kiến ​​trúc của ứng dụng. Ví dụ: bạn có thể đọc tài liệu, cụ thể là cuốn sách " Sổ tay kiến ​​trúc sư phần mềm: Trở thành kiến ​​trúc sư phần mềm thành công bằng cách triển khai kiến ​​trúc hiệu quả... " của Joseph Ingeno. Như đã nói, có một Cấp dữ liệu hoặc “Lớp dữ liệu” nhất định. Nó bao gồm một nơi để lưu trữ dữ liệu (ví dụ: cơ sở dữ liệu SQL) và các công cụ để làm việc với kho dữ liệu (ví dụ: JDBC, sẽ được thảo luận). Ngoài ra còn có một bài viết trên trang web của Microsoft: “ Thiết kế lớp bền vững cơ sở hạ tầng ”, trong đó mô tả giải pháp kiến ​​trúc tách một lớp bổ sung khỏi Cấp dữ liệu - Lớp kiên trì. Trong trường hợp này, Cấp dữ liệu là cấp độ lưu trữ của chính dữ liệu đó, trong khi Lớp liên tục là một mức độ trừu tượng nào đó để làm việc với dữ liệu từ bộ lưu trữ từ cấp Cấp dữ liệu. Lớp kiên trì có thể bao gồm mẫu "DAO" hoặc các ORM khác nhau. Nhưng ORM là một chủ đề cho một cuộc thảo luận khác. Như bạn có thể đã hiểu, Cấp dữ liệu xuất hiện đầu tiên. Kể từ thời JDK 1.1, JDBC (Java DataBase Connectivity - kết nối tới cơ sở dữ liệu trong Java) đã xuất hiện trong thế giới Java. Đây là một tiêu chuẩn để tương tác giữa các ứng dụng Java với nhiều DBMS khác nhau, được triển khai dưới dạng các gói java.sql và javax.sql có trong Java SE:
JDBC hay nơi mọi chuyện bắt đầu - 2
Tiêu chuẩn này được mô tả bằng đặc tả " JSR 221 JDBC 4.1 API ". Thông số kỹ thuật này cho chúng ta biết rằng API JDBC cung cấp quyền truy cập theo chương trình vào cơ sở dữ liệu quan hệ từ các chương trình được viết bằng Java. Nó cũng cho biết rằng API JDBC là một phần của nền tảng Java và do đó được bao gồm trong Java SE và Java EE. API JDBC được cung cấp trong hai gói: java.sql và javax.sql. Chúng ta hãy làm quen với họ sau đó.
JDBC hay nơi mọi chuyện bắt đầu - 3

Bắt đầu công việc

Để hiểu API JDBC nói chung là gì, chúng ta cần một ứng dụng Java. Sẽ thuận tiện nhất khi sử dụng một trong các hệ thống lắp ráp dự án. Ví dụ: hãy sử dụng Gradle . Bạn có thể đọc thêm về Gradle trong bài đánh giá ngắn: " Giới thiệu ngắn gọn về Gradle ". Đầu tiên, hãy khởi tạo một dự án Gradle mới. Vì chức năng của Gradle được triển khai thông qua các plugin nên chúng ta cần sử dụng “ Plugin Gradle Build init ” để khởi tạo:
gradle init --type java-application
Sau này, hãy mở tập lệnh xây dựng - tệp build.gradle , mô tả dự án của chúng tôi và cách làm việc với nó. Chúng tôi quan tâm đến khối " phụ thuộc ", trong đó các phụ thuộc được mô tả - nghĩa là các thư viện/khung/api đó, nếu không có chúng thì chúng tôi không thể làm việc và phụ thuộc vào chúng. Theo mặc định, chúng ta sẽ thấy một cái gì đó như:
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'
}
Tại sao chúng ta lại thấy điều này ở đây? Đây là những phần phụ thuộc của dự án mà Gradle tự động tạo cho chúng tôi khi tạo dự án. Và cũng bởi vì ổi là một thư viện riêng biệt không có trong Java SE. JUnit cũng không có trong Java SE. Nhưng chúng tôi đã có sẵn JDBC, tức là nó là một phần của Java SE. Hóa ra chúng ta có JDBC. Tuyệt vời. Chúng ta cần thứ gì khác nữa? Có một sơ đồ tuyệt vời như vậy:
JDBC hay nơi mọi chuyện bắt đầu - 4
Như chúng ta có thể thấy và điều này là hợp lý, cơ sở dữ liệu là một thành phần bên ngoài không có nguồn gốc từ Java SE. Điều này được giải thích một cách đơn giản - có một số lượng lớn cơ sở dữ liệu và bạn có thể làm việc với bất kỳ cơ sở dữ liệu nào. Ví dụ có PostgreSQL, Oracle, MySQL, H2. Mỗi cơ sở dữ liệu này được cung cấp bởi một công ty riêng biệt gọi là nhà cung cấp cơ sở dữ liệu. Mỗi cơ sở dữ liệu được viết bằng ngôn ngữ lập trình riêng (không nhất thiết phải là Java). Để có thể làm việc với cơ sở dữ liệu từ một ứng dụng Java, nhà cung cấp cơ sở dữ liệu viết một trình điều khiển đặc biệt, đó là bộ điều hợp hình ảnh của chính nó. Những cái tương thích với JDBC như vậy (nghĩa là những cái có trình điều khiển JDBC) còn được gọi là “Cơ sở dữ liệu tuân thủ JDBC”. Ở đây chúng ta có thể rút ra sự tương tự với các thiết bị máy tính. Ví dụ: trong sổ ghi chú có nút "In". Mỗi lần bạn nhấn nó, chương trình sẽ thông báo cho hệ điều hành rằng ứng dụng notepad muốn in. Và bạn có một máy in. Để dạy hệ điều hành của bạn giao tiếp đồng nhất với máy in Canon hoặc HP, bạn sẽ cần các trình điều khiển khác nhau. Nhưng đối với bạn, với tư cách là người dùng, sẽ không có gì thay đổi. Bạn vẫn sẽ nhấn nút tương tự. Tương tự với JDBC Bạn đang chạy cùng một mã, chỉ là các cơ sở dữ liệu khác nhau có thể đang chạy ngầm. Tôi nghĩ đây là một cách tiếp cận rất rõ ràng. Mỗi trình điều khiển JDBC như vậy là một loại tạo phẩm, thư viện, tệp jar nào đó. Đây là sự phụ thuộc cho dự án của chúng tôi. Ví dụ: chúng ta có thể chọn cơ sở dữ liệu " H2 Database " và sau đó chúng ta cần thêm một phần phụ thuộc như thế này:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Cách tìm phần phụ thuộc và cách mô tả nó được chỉ ra trên các trang web chính thức của nhà cung cấp cơ sở dữ liệu hoặc trên " Maven Central ". Trình điều khiển JDBC không phải là cơ sở dữ liệu như bạn hiểu. Nhưng anh ấy chỉ là người hướng dẫn cho nó. Nhưng có một thứ gọi là " Trong cơ sở dữ liệu bộ nhớ ". Đây là những cơ sở dữ liệu tồn tại trong bộ nhớ trong suốt thời gian tồn tại của ứng dụng của bạn. Thông thường, điều này thường được sử dụng cho mục đích thử nghiệm hoặc đào tạo. Điều này cho phép bạn tránh cài đặt một máy chủ cơ sở dữ liệu riêng trên máy. Điều này rất phù hợp để chúng ta làm quen với JDBC. Vậy là sandbox của chúng ta đã sẵn sàng và chúng ta bắt đầu.
JDBC hay nơi mọi thứ bắt đầu - 5

Sự liên quan

Vì vậy, chúng tôi có trình điều khiển JDBC, chúng tôi có API JDBC. Như chúng ta đã nhớ, JDBC là viết tắt của Java DataBase Connectivity. Vì vậy, tất cả đều bắt đầu từ Connectivity - khả năng thiết lập kết nối. Và kết nối là Kết nối. Chúng ta hãy quay lại phần nội dung đặc tả JDBC và xem mục lục. Trong chương “ CHAPTER 4 Tổng quan ” (tổng quan) chúng ta chuyển sang phần “ 4.1 Thiết lập kết nối ” (thiết lập kết nối) người ta nói rằng có hai cách để kết nối với cơ sở dữ liệu:
  • Thông qua Trình quản lý trình điều khiển
  • Thông qua nguồn dữ liệu
Hãy giải quyết với DriverManager. Như đã nói, DriverManager cho phép bạn kết nối với cơ sở dữ liệu tại URL đã chỉ định, đồng thời tải Trình điều khiển JDBC mà nó tìm thấy trong CLASSPATH (và trước JDBC 4.0, bạn phải tự tải lớp trình điều khiển). Có một chương riêng “CHƯƠNG 9 Kết nối” về kết nối với cơ sở dữ liệu. Chúng tôi quan tâm đến cách kết nối thông qua DriverManager, vì vậy chúng tôi quan tâm đến phần "9.3 Lớp DriverManager". Nó chỉ ra cách chúng ta có thể truy cập cơ sở dữ liệu:
Connection con = DriverManager.getConnection(url, user, passwd);
Các thông số có thể được lấy từ trang web của cơ sở dữ liệu mà chúng tôi đã chọn. Trong trường hợp của chúng tôi, đây là H2 - " H2 Cheat Sheet ". Hãy chuyển sang lớp AppTest do Gradle chuẩn bị. Nó chứa các bài kiểm tra JUnit. Kiểm thử JUnit là một phương pháp được đánh dấu bằng chú thích @Test. Các bài kiểm tra đơn vị không phải là chủ đề của bài đánh giá này, vì vậy chúng tôi sẽ chỉ giới hạn ở mức hiểu rằng đây là những phương pháp được mô tả theo một cách nhất định, mục đích của nó là để kiểm tra điều gì đó. Theo đặc tả JDBC và trang web H2, chúng tôi sẽ kiểm tra xem chúng tôi đã nhận được kết nối tới cơ sở dữ liệu chưa. Hãy viết một phương thức để có được kết nối:
private Connection getNewConnection() throws SQLException {
	String url = "jdbc:h2:mem:test";
	String user = "sa";
	String passwd = "sa";
	return DriverManager.getConnection(url, user, passwd);
}
Bây giờ hãy viết một bài kiểm tra cho phương pháp này để kiểm tra xem kết nối đã thực sự được thiết lập chưa:
@Test
public void shouldGetJdbcConnection() throws SQLException {
	try(Connection connection = getNewConnection()) {
		assertTrue(connection.isValid(1));
		assertFalse(connection.isClosed());
	}
}
Kiểm tra này, khi được thực hiện, sẽ xác minh rằng kết nối kết quả là hợp lệ (được tạo chính xác) và nó không bị đóng. Bằng cách sử dụng tài nguyên dùng thử, chúng tôi sẽ giải phóng tài nguyên khi không còn cần đến chúng nữa. Điều này sẽ bảo vệ chúng ta khỏi các kết nối bị chùng và rò rỉ bộ nhớ. Vì bất kỳ hành động nào với cơ sở dữ liệu đều yêu cầu kết nối, hãy cung cấp các phương thức kiểm tra còn lại được đánh dấu @Test bằng Kết nối khi bắt đầu kiểm tra, chúng tôi sẽ phát hành sau khi kiểm tra. Để làm điều này, chúng ta cần hai chú thích: @Before và @After Hãy thêm một trường mới vào lớp AppTest để lưu trữ kết nối JDBC cho các bài kiểm tra:
private static Connection connection;
Và hãy thêm các phương thức mới:
@Before
public void init() throws SQLException {
	connection = getNewConnection();
}
@After
public void close() throws SQLException {
	connection.close();
}
Giờ đây, mọi phương pháp thử nghiệm đều được đảm bảo có kết nối JDBC và không phải tự tạo kết nối đó mỗi lần.
JDBC hay nơi mọi chuyện bắt đầu - 6

Các câu lệnh

Tiếp theo chúng ta quan tâm đến Tuyên bố hoặc biểu thức. Chúng được mô tả trong tài liệu ở chương " CHƯƠNG 13 Tuyên bố ". Thứ nhất, nó nói rằng có một số loại hoặc loại câu lệnh:
  • Câu lệnh: Biểu thức SQL không chứa tham số
  • preparedStatement : Câu lệnh SQL được chuẩn bị sẵn có chứa các tham số đầu vào
  • CallableStatement: Biểu thức SQL có khả năng lấy giá trị trả về từ Thủ tục lưu trữ SQL.
Vì vậy, khi có kết nối, chúng ta có thể thực hiện một số yêu cầu trong khuôn khổ kết nối này. Do đó, điều hợp lý là ban đầu chúng ta lấy một phiên bản của biểu thức SQL từ Connection. Bạn cần bắt đầu bằng cách tạo một bảng. Hãy mô tả yêu cầu tạo bảng dưới dạng biến Chuỗi. Làm thế nào để làm nó? Hãy sử dụng một số hướng dẫn như " sqltutorial.org ", " sqlbolt.com ", " postgresqltutorial.com ", " codecademy.com ". Ví dụ: hãy sử dụng một ví dụ từ khóa học SQL trên khanacademy.org . Hãy thêm một phương thức để thực thi một biểu thức trong cơ sở dữ liệu:
private int executeUpdate(String query) throws SQLException {
	Statement statement = connection.createStatement();
	// Для Insert, Update, Delete
	int result = statement.executeUpdate(query);
	return result;
}
Hãy thêm một phương thức để tạo bảng thử nghiệm bằng phương thức trước đó:
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);
}
Bây giờ hãy kiểm tra điều này:
@Test
public void shouldCreateCustomerTable() throws SQLException {
	createCustomerTable();
	connection.createStatement().execute("SELECT * FROM customers");
}
Bây giờ hãy thực hiện yêu cầu và thậm chí với một tham số:
@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 không hỗ trợ các tham số được đặt tên cho ReadyStatement, do đó, bản thân các tham số được chỉ định bởi các câu hỏi và bằng cách chỉ định giá trị, chúng tôi chỉ ra chỉ mục câu hỏi (bắt đầu từ 1, không phải 0). Trong thử nghiệm trước, chúng tôi nhận được giá trị đúng như một dấu hiệu cho biết liệu có kết quả hay không. Nhưng kết quả truy vấn được thể hiện như thế nào trong API JDBC? Và nó được trình bày dưới dạng Tập kết quả.
JDBC hay nơi mọi chuyện bắt đầu - 7

Bộ kết quả

Khái niệm về Tập kết quả được mô tả trong đặc tả API JDBC trong chương "CHƯƠNG 15 Tập kết quả". Trước hết, nó nói rằng ResultSet cung cấp các phương thức để truy xuất và xử lý kết quả của các truy vấn được thực hiện. Tức là, nếu phương thức thực thi trả về true cho chúng ta thì chúng ta có thể nhận được một ResultSet. Hãy chuyển lệnh gọi phương thức createCustomerTable() sang phương thức init, được đánh dấu là @Before. Bây giờ hãy hoàn tất bài kiểm tra nênSelectData của chúng ta:
@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);
}
Điều đáng chú ý ở đây là next là một phương thức di chuyển cái gọi là “con trỏ”. Con trỏ trong Bộ kết quả trỏ đến một hàng nào đó. Vì vậy, để đọc một dòng, bạn cần đặt con trỏ này lên dòng đó. Khi con trỏ được di chuyển, phương thức di chuyển con trỏ trả về true nếu con trỏ hợp lệ (đúng, đúng), tức là nó trỏ đến dữ liệu. Nếu trả về sai thì không có dữ liệu, nghĩa là con trỏ không trỏ đến dữ liệu. Nếu chúng ta cố gắng lấy dữ liệu bằng một con trỏ không hợp lệ, chúng ta sẽ gặp lỗi: Không có dữ liệu. Điều thú vị là thông qua ResultSet bạn có thể cập nhật hoặc thậm chí chèn các hàng:
@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();
}

Bộ hàng

Ngoài ResultSet, JDBC còn giới thiệu khái niệm RowSet. Bạn có thể đọc thêm tại đây: " Cơ bản về JDBC: Sử dụng đối tượng RowSet ". Có nhiều biến thể sử dụng khác nhau. Ví dụ: trường hợp đơn giản nhất có thể trông như thế này:
@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);
}
Như bạn có thể thấy, RowSet tương tự như sự cộng sinh của câu lệnh (chúng tôi đã chỉ định lệnh thông qua nó) và lệnh được thực thi. Thông qua nó, chúng ta điều khiển con trỏ (bằng cách gọi phương thức tiếp theo) và lấy dữ liệu từ nó. Cách tiếp cận này không chỉ thú vị mà còn có thể triển khai được. Ví dụ: CachedRowSet. Nó bị "ngắt kết nối" (nghĩa là nó không sử dụng kết nối liên tục đến cơ sở dữ liệu) và yêu cầu đồng bộ hóa rõ ràng với cơ sở dữ liệu:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Bạn có thể đọc thêm trong phần hướng dẫn trên trang web của Oracle: " Using CachedRowSetObjects ".
JDBC hay nơi mọi chuyện bắt đầu - 8

metadata

Ngoài các truy vấn, kết nối tới cơ sở dữ liệu (tức là một phiên bản của lớp Kết nối) cung cấp quyền truy cập vào siêu dữ liệu - dữ liệu về cách cơ sở dữ liệu của chúng tôi được định cấu hình và tổ chức. Nhưng trước tiên, hãy đề cập đến một số điểm chính: URL để kết nối với cơ sở dữ liệu của chúng tôi: “jdbc:h2:mem:test”. test là tên cơ sở dữ liệu của chúng tôi. Đối với API JDBC, đây là một thư mục. Và tên sẽ viết hoa, tức là TEST. Lược đồ mặc định cho H2 là CÔNG KHAI. Bây giờ, hãy viết một bài kiểm tra hiển thị tất cả các bảng của người dùng. Tại sao tùy chỉnh? Bởi vì cơ sở dữ liệu không chỉ chứa các bảng người dùng (những bảng mà chúng tôi tự tạo bằng cách sử dụng biểu thức tạo bảng) mà còn chứa các bảng hệ thống. Chúng cần thiết để lưu trữ thông tin hệ thống về cấu trúc của cơ sở dữ liệu. Mỗi cơ sở dữ liệu có thể lưu trữ các bảng hệ thống như vậy một cách khác nhau. Ví dụ: trong H2 chúng được lưu trữ trong lược đồ " INFORMATION_SCHema ". Điều thú vị là Lược đồ THÔNG TIN là một cách tiếp cận phổ biến, nhưng Oracle đã đi theo một con đường khác. Bạn có thể đọc thêm tại đây: " INFORMATION_SCHEMA and Oracle ". Hãy viết một bài kiểm tra nhận siêu dữ liệu trên bảng người dùng:
@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 hay nơi mọi chuyện bắt đầu - 9

Nhóm kết nối

Nhóm kết nối trong đặc tả JDBC có một phần được gọi là "Nhóm kết nối Chương 11". Nó cũng cung cấp lý do chính cho sự cần thiết của một nhóm kết nối. Mỗi Coonection là một kết nối vật lý tới cơ sở dữ liệu. Việc tạo ra và đóng nó là một công việc khá “đắt”. JDBC chỉ cung cấp API tổng hợp kết nối. Vì vậy, sự lựa chọn thực hiện vẫn là của chúng tôi. Ví dụ: các triển khai như vậy bao gồm HikariCP . Theo đó, chúng tôi sẽ cần thêm một nhóm vào phần phụ thuộc dự án của mình:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Bây giờ chúng ta cần bằng cách nào đó sử dụng nhóm này. Để làm được điều này, bạn cần khởi tạo nguồn dữ liệu hay còn gọi là Datasource:
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;
}
Và hãy viết một bài kiểm tra để nhận kết nối từ nhóm:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC hay nơi mọi thứ bắt đầu - 10

Giao dịch

Một trong những điều thú vị nhất về JDBC là các giao dịch. Trong đặc tả JDBC, chúng được gán cho chương "CHƯƠNG 10 Giao dịch". Trước hết, cần hiểu giao dịch là gì. Giao dịch là một nhóm các hoạt động tuần tự được kết hợp một cách hợp lý trên dữ liệu, được xử lý hoặc hủy bỏ toàn bộ. Khi nào giao dịch bắt đầu khi sử dụng JDBC? Như thông số kỹ thuật nêu rõ, việc này được Trình điều khiển JDBC xử lý trực tiếp. Nhưng thông thường, một giao dịch mới bắt đầu khi câu lệnh SQL hiện tại yêu cầu một giao dịch và giao dịch đó vẫn chưa được tạo. Khi nào giao dịch kết thúc? Điều này được kiểm soát bởi thuộc tính tự động cam kết. Nếu tính năng tự động cam kết được bật, giao dịch sẽ được hoàn thành sau khi câu lệnh SQL được "hoàn thành". "Xong" nghĩa là gì tùy thuộc vào loại biểu thức SQL:
  • Ngôn ngữ thao tác dữ liệu hay còn gọi là DML (Chèn, cập nhật, xóa)
    Giao dịch được hoàn thành ngay khi hành động được hoàn thành
  • Chọn câu lệnh
    Giao dịch được hoàn thành khi Bộ kết quả được đóng ( ResultSet#close )
  • CallableStatement và các biểu thức trả về nhiều kết quả
    Khi tất cả các Bộ kết quả liên quan đã được đóng và tất cả đầu ra đã được nhận (bao gồm cả số lượng cập nhật)
Đây chính xác là cách API JDBC hoạt động. Như thường lệ, hãy viết một bài kiểm tra cho việc này:
@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);
}
Nó đơn giản. Nhưng điều này đúng miễn là chúng ta chỉ có một giao dịch. Phải làm gì khi có một vài trong số họ? Họ cần phải được cách ly với nhau. Do đó, hãy nói về mức độ cô lập giao dịch và cách JDBC xử lý chúng.
JDBC hay nơi mọi chuyện bắt đầu - 11

Mức độ cách nhiệt

Hãy mở tiểu mục "10.2 Mức cách ly giao dịch" của đặc tả JDBC. Ở đây, trước khi tiến xa hơn, tôi muốn nhớ về một thứ như ACID. ACID mô tả các yêu cầu đối với một hệ thống giao dịch.
  • Tính nguyên tử:
    Sẽ không có giao dịch nào được cam kết một phần vào hệ thống. Hoặc tất cả các hoạt động phụ của nó sẽ được thực hiện hoặc không có hoạt động nào được thực hiện.
  • Tính nhất quán:
    Theo định nghĩa, mỗi giao dịch thành công chỉ ghi lại kết quả hợp lệ.
  • Cô lập:
    Trong khi một giao dịch đang chạy, các giao dịch đồng thời sẽ không ảnh hưởng đến kết quả của nó.
  • Độ bền:
    Nếu một giao dịch được hoàn thành thành công, những thay đổi được thực hiện đối với nó sẽ không được hoàn tác do bất kỳ lỗi nào.
Khi nói về mức độ cô lập giao dịch, chúng ta đang nói đến yêu cầu “Cô lập”. Cách ly là một yêu cầu tốn kém, vì vậy trong cơ sở dữ liệu thực có các chế độ không cách ly hoàn toàn một giao dịch (Mức cách ly Đọc lặp lại và thấp hơn). Wikipedia có lời giải thích tuyệt vời về các vấn đề có thể phát sinh khi làm việc với các giao dịch. Bạn nên đọc thêm ở đây: “ Các vấn đề về truy cập song song bằng cách sử dụng giao dịch ”. Trước khi viết bài kiểm thử, hãy thay đổi một chút Tập lệnh xây dựng Gradle: thêm một khối có thuộc tính, nghĩa là với các cài đặt của dự án của chúng ta:
ext {
    h2Version = '1.3.176' // 1.4.177
    hikariVersion = '3.3.1'
    junitVersion = '4.12'
}
Tiếp theo, chúng tôi sử dụng điều này trong các phiên bản:
dependencies {
    implementation "com.h2database:h2:${h2Version}"
    implementation "com.zaxxer:HikariCP:${hikariVersion}"
    testImplementation "junit:junit:${junitVersion}"
}
Bạn có thể nhận thấy rằng phiên bản h2 đã trở nên thấp hơn. Chúng ta sẽ biết tại sao sau. Vậy bạn áp dụng các mức cách ly như thế nào? Hãy cùng xem ngay một ví dụ thực tế nhỏ:
@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);
}
Điều thú vị là thử nghiệm này có thể thất bại đối với nhà cung cấp không hỗ trợ TRANSACTION_READ_UNCOMMITTED (ví dụ: sqlite hoặc HSQL). Và mức độ giao dịch có thể không hoạt động. Hãy nhớ rằng chúng tôi đã chỉ ra phiên bản của trình điều khiển Cơ sở dữ liệu H2? Nếu chúng ta nâng nó lên h2Version = '1.4.177' trở lên thì READ UNCOMMITTED sẽ ngừng hoạt động, mặc dù chúng ta không thay đổi mã. Điều này một lần nữa chứng minh rằng việc lựa chọn nhà cung cấp và phiên bản trình điều khiển không chỉ là các chữ cái, nó thực sự sẽ xác định cách thức các yêu cầu của bạn sẽ được thực hiện. Bạn có thể đọc về cách khắc phục hành vi này trong phiên bản 1.4.177 và cách nó không hoạt động ở các phiên bản cao hơn tại đây: " Support READ UNCOMMITTED mức cô lập trong chế độ MVStore ".
JDBC hay nơi mọi chuyện bắt đầu - 12

Điểm mấu chốt

Như chúng ta có thể thấy, JDBC là một công cụ mạnh mẽ trong Java để làm việc với cơ sở dữ liệu. Tôi hy vọng bài đánh giá ngắn này sẽ giúp bạn có điểm khởi đầu hoặc giúp làm mới trí nhớ của bạn. Vâng, đối với một bữa ăn nhẹ, một số tài liệu bổ sung: #Viacheslav
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION