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: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:
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.
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
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.
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.
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ả.
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 ".
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"));
}
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));
}
}
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
- 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)
Giao dịch được hoàn thành khi Bộ kết quả được đóng ( 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);
}
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.
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.
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 ".
Đ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:- Báo cáo vụ cháy: " Giao dịch: huyền thoại, bất ngờ và cơ hội " từ Martin Kleppmann
- Yury Tkach: " JPA. Giao dịch "
- Yurik Tkach: " JDBC - Java dành cho người thử nghiệm "
- Khóa học miễn phí trên Udemy: " JDBC và MySQL "
- " Xử lý các đối tượng CallableStatement "
- Nhà phát triển IBM: " Kết nối cơ sở dữ liệu Java "
- Trung tâm Kiến thức IBM: " Bắt đầu với JDBC "
GO TO FULL VERSION