JavaRush /Java блог /Random UA /JDBC або з чого все починається
Viacheslav
3 рівень

JDBC або з чого все починається

Стаття з групи Random UA
У світі без зберігання даних ніяк. І історія роботи з базами даних розпочалася вже дуже давно, з появи JDBC. Пропоную згадати те, без чого не обходься жоден сучасний фреймворк, побудований поверх JDBC. Крім того, навіть працюючи з ними часом може знадобитися можливість "повернутися до коріння". Сподіваюся, огляд допоможе як вступне слово або щось освіжити в пам'яті.
JDBC або з чого все починається.

Вступ

Одна з основних цілей мови програмування - зберігання та обробка інформації. Щоб краще зрозуміти роботу зберігання даних, варто трохи часу виділити на теорію та архітектуру додатків. Наприклад, можна ознайомитися з літературою, а саме з книгою " Software Architect's Handbook: Become a successful software architect by implementing effective arch... " авторства Joseph Ingeno. Як сказано, є певний Data Tier або "Шар даних". Він включає місце зберігання даних (наприклад, SQL базу даних) і засоби для роботи зі сховищем даних (наприклад, JDBC, про яке і піде мова). Також на сайті Microsoft є стаття: " Проектування рівня збереження інфраструктури" у якій описується архітектурне рішення виділення з Data Tier додаткового шару — Persistence Layer. У разі Data Tier — це рівень зберігання самих даних, тоді як Persistence Layer — це певний рівень абстракції до роботи з даними зі сховища з рівня Data Tier. До рівня Persistence Layer можна віднести шаблон "DAO" або різні ORM. Але ORM - це тема окремої розмови. Як Ви могли вже зрозуміти, спочатку з'явився Data Tier. Ще з часів JDK 1.1 у Java світі з'явився JDBC (Java DataBase Connectivity - з'єднання з базами даних на Java) Це стандарт взаємодії Java-додатків з різними СУБД, реалізований у вигляді пакетів java.sql і javax.sql, що входять до складу Java SE:
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 , де описується наш проект і те, як з ним потрібно працювати. Нас цікавить блок " dependencies ", де описуються залежності - тобто ті бібліотеки/фреймворки/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 або з чого все починається.
Як ми бачимо, і це логічно, база даних є зовнішнім компонентом, якого немає спочатку Java SE. Це пояснюється просто - існує безліч баз даних і працювати ви можете захотіти з будь-якої. Наприклад, є PostgreSQL, Oracle, MySQL, H2. Кожна з цих баз даних постачається окремою компанією, яка називається постачальниками баз даних або database vendors. Кожна база даних написана якоюсь своєю мовою програмування (не обов'язково Java). Щоб з базою даних можна було працювати з Java програми, постачальник бази даних пише особливий драйвер, який є свого образу адаптером. Такі JDBC сумісні (тобто які мають JDBC драйвер) ще називають "JDBC-Compliant Database". Тут можна здійснити аналогію з комп'ютерними пристроями. Наприклад, у блокноті є кнопка "Друк". Щоразу коли ви її натискаєте програма повідомляє операційну систему, що додаток блокнот хоче надрукувати. І маєте принтер. Щоб навчити розмовляти одночасно вашу операційну систему з принтером Canon або HP, знадобляться різні драйвери. Але для Вас, як користувача, нічого не зміниться. Ви, як і раніше, натискатимете одну і ту ж кнопку. Так само і з JDBC. Ви виконуєте той самий код, просто "під капотом" можуть працювати різні бази даних. Думаю тут дуже зрозумілий підхід. Кожен такий JDBC драйвер – це деякий артефакт, бібліотека, jar файл. Він і є залежністю для нашого проекту. Наприклад, ми можемо вибрати базу даних " Щоб навчити розмовляти одночасно вашу операційну систему з принтером Canon або HP, знадобляться різні драйвери. Але для Вас, як користувача, нічого не зміниться. Ви, як і раніше, натискатимете одну і ту ж кнопку. Так само і з JDBC. Ви виконуєте той самий код, просто "під капотом" можуть працювати різні бази даних. Думаю тут дуже зрозумілий підхід. Кожен такий JDBC драйвер – це деякий артефакт, бібліотека, jar файл. Він і є залежністю для нашого проекту. Наприклад, ми можемо вибрати базу даних " Щоб навчити розмовляти одночасно вашу операційну систему з принтером Canon або HP, знадобляться різні драйвери. Але для Вас, як користувача, нічого не зміниться. Ви, як і раніше, натискатимете одну і ту ж кнопку. Так само і з JDBC. Ви виконуєте той самий код, просто "під капотом" можуть працювати різні бази даних. Думаю тут дуже зрозумілий підхід. Кожен такий JDBC драйвер – це деякий артефакт, бібліотека, jar файл. Він і є залежністю для нашого проекту. Наприклад, ми можемо вибрати базу даних " Кожен такий JDBC драйвер – це деякий артефакт, бібліотека, jar файл. Він і є залежністю для нашого проекту. Наприклад, ми можемо вибрати базу даних " Кожен такий JDBC драйвер – це деякий артефакт, бібліотека, jar файл. Він і є залежністю для нашого проекту. Наприклад, ми можемо вибрати базу даних "H2 Database і тоді нам треба додати залежність наступним чином:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Те, як знайти залежність і як її описати вказано на офіційних сайтах постачальника БД або на Maven Central . JDBC драйвер не є базою даних, як ви зрозуміли. А лише є провідником до неї. Але є таке поняття, як " In memory databases ". Це такі бази даних, які існують у пам'яті на час життя вашої програми. Зазвичай це часто використовують для тестування або для навчальних цілей. Це дозволяє не ставити окремий сервер баз даних на машині. Що нам дуже підійде для знайомств з JDBC. Ось і готова наша пісочниця, і ми приступаємо.
JDBC або з чого все починається - 5

Connection

Отже, ми маємо JDBC драйвер, є JDBC API. Як ми пам'ятаємо, JDBC розшифровується як Java DataBase Connectivity. Тому все починається з Connectivity - можливості встановлювати підключення. А підключення – це Connection. Звернемося знову до тексту специфікації JDBC і подивимося на зміст. У розділі " CHAPTER 4 Overview " (overview - огляд) звернемося до розділу " 4.1 Establishing a Connection " (встановлення з'єднання) сказано, що є два способу підключення до БД:
  • Через DriverManager
  • Через DataSource
Розберемося з DriverManager'ом. Як сказано, DriverManager дозволяє підключитися до бази даних за вказаною URL, а також завантажує JDBC Driver'и, який він знайшов у CLASSPATH (а раніше, до JDBC 4.0 завантажувати клас драйвера треба було самостійно). Для з'єднання з БД є окремий розділ "CHAPTER 9 Connections". Нас цікавить, як отримати з'єднання через DriverManager, тому нам цікавий розділ "9.3 The DriverManager Class". У ньому зазначено, як ми можемо отримати доступ до БД:
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 connection і він не повинен щоразу сам його створювати.
JDBC або з чого все починається - 6

Statements

Далі нас цікавить держава або вирази. Вони описані у документації у розділі " CHAPTER 13 Statements " . По-перше, там сказано, що існує кілька типів чи видів statement'ів:
  • Statement: SQL вираз, який не містить параметрів
  • PreparedStatement : Підготовлений SQL вираз, що містить вхідні параметри
  • CallableStatement : SQL вираз із можливістю отримати повертається значення зі збережених процедур (SQL Stored Procedures).
Отже, маючи підключення, ми можемо в рамках цього підключення виконати запит. Тому, логічно, що екземпляр виразу SQL спочатку ми отримуємо з Connection. Почати потрібно із створення таблиці. Опишемо запит створення таблиці у вигляді змінної типу String. Як це зробити? Скористаємося якимось навчальним керівництвом, на кшталт " sqltutorial.org ", " sqlbolt.com ", " postgresqltutorial.com ", " codecademy.com ". Скористайтеся прикладом з курсу SQL на khanacademy.org . Додамо метод виконання вираження у БД:
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, а не з нуля). В останньому тесті ми отримали true як ознаку того, чи є результат. Але як наведено результат запиту в JDBC API? А представлений він як ResultSet.
JDBC або з чого все починається - 7

ResultSet

Поняття ResultSet описано у специфікації JDBC API у розділі "CHAPTER 15 Result Sets". Перш за все там сказано, що 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, то даних немає, тобто курсор не вказує на дані. Якщо спробувати отримати дані з невалідним курсором, ми отримаємо помилку: No data is available Ще цікаво, що через 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();
}

RowSet

JDBC окрім ResultSet вводить таке поняття, як RowSet. Докладніше можна прочитати тут: " JDBC Basics: Using RowSet Objects ". Існують різні варіації використання. Наприклад, найпростіший випадок може виглядати так:
@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 схожий на симбіоз statement (ми вказали через нього command) і виконали command. Через нього ж ми керуємо курсором (викликаючи метод next) і з нього отримуємо дані. Цікавим є не тільки такий підхід, а й можливі реалізації. Наприклад, CachedRowSet. Він є "відключеним" (тобто не використовує постійне підключення до БД) і вимагає явного синхронізації з БД:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Докладніше можна прочитати в tutorial на сайті Oracle: " Using CachedRowSetObjects ".
JDBC або з чого все починається - 8

Metadata

Окрім запитів, підключення до БД (тобто екземпляр класу Connection) надає доступ до метаданих - даних про те, як налаштована та як влаштована наша база даних. Але спочатку озвучимо кілька ключових моментів: URL підключення до нашої БД: "jdbc:h2:mem:test". test – це назва нашої бази даних. Для JDBC API це каталог. І назва буде у верхньому регістрі, тобто TEST. Схема за замовчуванням ( Default schema) для 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 або з чого все починається.

Пул підключень

Пулу підключень у специфікації JDBC відведено розділ "Chapter 11 Connection Pooling". У ньому ж дається головне обґрунтування необхідності пулу підключень. Кожен Coonection – це фізичне підключення до БД. Його створення та закриття - досить "дорога" робота. JDBC надає лише API для пулу з'єднань. Тому вибір реалізації залишається за нами. Наприклад, до таких реалізацій відноситься HikariCP . Відповідно, нам знадобиться додати пул до нас залежно від проекту:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Тепер треба якось цей пул задіяти. Для цього потрібно виконати ініціалізацію джерела даних, він також може бути виконаний з 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;
}
І напишемо тест на отримання підключення з пулу:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC або з чого все починається - 10

Транзакції

Один із найцікавіших моментів, пов'язаних з JDBC – це транзакції. У специфікації JDBC їм відведено розділ "CHAPTER 10 Transactions". Насамперед варто зрозуміти, що ж таке транзакція. Транзакція — це група логічно об'єднаних послідовних операцій із роботі з даними, оброблювана чи отменяемая цілком. Коли починається транзакція під час використання JDBC? Як свідчить специфікація, це безпосередньо вирішує JDBC Driver. Але зазвичай, нова транзакція починається тоді, коли поточний SQL вираз (SQL statement) вимагатиме транзакцію і транзакції ще створено. Коли закінчується транзакція? Це регулюється атрибутом автокомміту (auto-commit). Якщо автокомміт увімкнено, то транзакція буде завершена після того, як SQL вираз буде "виконано". Що таке "виконано" залежить від типу SQL виразу:
  • Data Manipulation Language, він же DML (Insert, Update, Delete)
    Транзакція завершується як тільки завершилося виконання дії
  • Select Statements
    Транзакція завершується тоді, коли ResultSet буде закрито ( ResultSet#close )
  • CallableStatement і вирази, що повертають кілька результатів
    Коли всі асоційовані ResultSets будуть закриті і всі вихідні дані отримані (включаючи кількість апдейтів)
Так поводиться саме 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

Рівні ізоляції

Відкриємо підрозділ "10.2 Transaction Isolation Levels" специфікації JDBC. Тут перш ніж далі рухатися хочеться сумніватися про таку штуку, як ACID. ACID визначає вимоги до транзакційної системи.
  • Atomicity(Атомарність):
    Ніяка транзакція не буде зафіксована в системі частково. Будуть або виконані її підоперації, або виконано жодної.
  • Consistency(Узгодженість):
    Кожна успішна транзакція за визначенням фіксує лише допустимі результати.
  • Isolation(Ізольованість):
    Під час виконання транзакції паралельні транзакції не повинні впливати на її результат.
  • Durability(Довговічність):
    Якщо транзакція успішно завершена, зроблені в ній зміни не будуть скасовані через будь-який збій.
Говорячи про рівні ізоляції транзакції, ми говоримо саме про вимогу "Isolation". Ізольованість - вимога "дорогою", тому в реальних БД існують режими, що не повністю ізолюють транзакцію (рівні ізольованості Repeatable Read і нижче). На вікіпедії є відмінне пояснення того, які проблеми можуть виникати під час роботи з транзакціями. Докладніше варто прочитати тут: " Проблеми паралельного доступу з використанням транзакцій ". Перш ніж ми напишемо наш тест, давайте трохи змінимо наш Gradle Build Script: додамо блок із properties, тобто з налаштуваннями нашого проекту:
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 Database? Якщо ми піднімемо її до h2Version = '1.4.177' і вище, то READ UNCOMMITTED перестане працювати, хоча код ми не змінювали. Це ще раз доводить, що вибір вендора та версії драйвера - це не просто літери, від цього насправді залежатиме те, як виконуватимуться ваші запити. Про те, як виправити цю поведінку у версії 1.4.177 і як це не працює у версіях вище можна прочитати тут: " Support READ UNCOMMITTED isolation level in MVStore mode ".
JDBC або з чого все починається - 12

Підсумок

Як бачимо, JDBC — це потужний інструмент Java для роботи з базами даних. Сподіваюся, цей невеликий огляд допоможе стати для Вас відправною точкою або допоможе освіжити в пам'яті щось. Ну і на закуску трохи додаткових матеріалів: #Viacheslav
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ