JavaRush /Blog Java /Random-PL /JDBC, czyli tam, gdzie wszystko się zaczyna
Viacheslav
Poziom 3

JDBC, czyli tam, gdzie wszystko się zaczyna

Opublikowano w grupie Random-PL
We współczesnym świecie nie ma mowy o przechowywaniu danych. A historia pracy z bazami danych rozpoczęła się bardzo dawno temu, wraz z pojawieniem się JDBC. Proponuję pamiętać o czymś, bez czego nie obejdzie się żaden nowoczesny framework zbudowany na bazie JDBC. Poza tym nawet pracując z nimi, czasami możesz potrzebować możliwości „powrotu do swoich korzeni”. Mam nadzieję, że ta recenzja będzie pomocna jako wprowadzenie lub odświeżenie pamięci.
JDBC, czyli tam, gdzie wszystko się zaczyna – 1

Wstęp

Jednym z głównych celów języka programowania jest przechowywanie i przetwarzanie informacji. Aby lepiej zrozumieć, jak działa przechowywanie danych, warto poświęcić trochę czasu na teorię i architekturę aplikacji. Na przykład możesz przeczytać literaturę, a mianowicie książkę „ Podręcznik architekta oprogramowania: Zostań architektem oprogramowania odnoszącym sukcesy, wdrażając efektywne arch... ” autorstwa Josepha Ingeno. Jak powiedziano, istnieje pewna warstwa danych lub „warstwa danych”. Zawiera miejsce do przechowywania danych (na przykład bazę danych SQL) oraz narzędzia do pracy z magazynem danych (na przykład JDBC, co zostanie omówione). Na stronie Microsoftu znajduje się także artykuł: „ Projektowanie warstwy trwałości infrastruktury ”, który opisuje rozwiązanie architektoniczne polegające na wydzieleniu dodatkowej warstwy z Warstwy Danych – Warstwy Trwałości. W tym przypadku warstwa danych to poziom przechowywania samych danych, natomiast warstwa trwałości to pewien poziom abstrakcji umożliwiający pracę z danymi z magazynu z poziomu warstwy danych. Warstwa trwałości może zawierać szablon „DAO” lub różne ORM. Ale ORM to temat na inną dyskusję. Jak być może już zrozumiałeś, warstwa danych pojawiła się jako pierwsza. Od czasów JDK 1.1 w świecie Java pojawiło się JDBC (Java DataBase Connectivity - połączenie z bazami danych w Javie). Jest to standard interakcji aplikacji Java z różnymi systemami DBMS, zaimplementowany w postaci pakietów java.sql i javax.sql zawartych w Java SE:
JDBC, czyli tam, gdzie wszystko się zaczyna - 2
Standard ten opisuje specyfikacja „ JSR 221 JDBC 4.1 API ”. Specyfikacja ta mówi nam, że JDBC API zapewnia programowy dostęp do relacyjnych baz danych z programów napisanych w Javie. Informuje również, że interfejs API JDBC jest częścią platformy Java i dlatego jest zawarty w Java SE i Java EE. Interfejs API JDBC jest dostępny w dwóch pakietach: java.sql i javax.sql. Poznajmy je zatem.
JDBC, czyli tam, gdzie wszystko się zaczyna – 3

Początek pracy

Aby ogólnie zrozumieć, czym jest API JDBC, potrzebujemy aplikacji Java. Najwygodniej jest skorzystać z jednego z systemów montażu projektowego. Na przykład użyjmy Gradle . Więcej o Gradle możesz przeczytać w krótkiej recenzji: „ Krótkie wprowadzenie do Gradle ”. Najpierw zainicjujmy nowy projekt Gradle. Ponieważ funkcjonalność Gradle jest implementowana poprzez wtyczki, do inicjalizacji musimy użyć „ Wtyczki Gradle Build Init ”:
gradle init --type java-application
Następnie otwórzmy skrypt budujący - plik build.gradle , który opisuje nasz projekt i sposób pracy z nim. Nas interesuje blok „ zależności ”, w którym opisane są zależności – czyli te biblioteki/frameworki/api, bez których nie możemy pracować i na których polegamy. Domyślnie zobaczymy coś takiego:
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'
}
Dlaczego to tutaj widzimy? Są to zależności naszego projektu, które Gradle automatycznie wygenerował dla nas podczas tworzenia projektu. A także dlatego, że guava to osobna biblioteka, która nie jest dołączona do Java SE. JUnit również nie jest dołączony do Java SE. Ale mamy JDBC od razu po wyjęciu z pudełka, to znaczy jest częścią Java SE. Okazuje się, że mamy JDBC. Świetnie. Czego jeszcze potrzebujemy? Jest taki wspaniały diagram:
JDBC, czyli tam, gdzie wszystko się zaczyna – 4
Jak widzimy i jest to logiczne, baza danych jest komponentem zewnętrznym, który nie jest natywny dla Java SE. Wyjaśnia się to prosto - istnieje ogromna liczba baz danych i można pracować z dowolną. Na przykład istnieje PostgreSQL, Oracle, MySQL, H2. Każda z tych baz danych jest dostarczana przez odrębną firmę zwaną dostawcami baz danych. Każda baza danych jest napisana w swoim własnym języku programowania (niekoniecznie Java). Aby móc pracować z bazą danych z poziomu aplikacji Java, dostawca bazy danych pisze specjalny sterownik, będący własnym adapterem obrazu. Takie bazy danych kompatybilne z JDBC (to znaczy te, które mają sterownik JDBC) nazywane są także „bazą danych zgodną z JDBC”. Można tu dokonać analogii z urządzeniami komputerowymi. Na przykład w notatniku znajduje się przycisk „Drukuj”. Za każdym razem, gdy go naciśniesz, program informuje system operacyjny, że aplikacja notatnik chce drukować. I masz drukarkę. Aby nauczyć system operacyjny jednolitej komunikacji z drukarką Canon lub HP, będziesz potrzebować różnych sterowników. Ale dla Ciebie, jako użytkownika, nic się nie zmieni. Nadal będziesz naciskać ten sam przycisk. To samo z JDBC. Używasz tego samego kodu, po prostu pod maską mogą działać różne bazy danych. Myślę, że jest to bardzo jasne podejście. Każdy taki sterownik JDBC jest pewnego rodzaju artefaktem, biblioteką, plikiem jar. To jest zależność dla naszego projektu. Na przykład możemy wybrać bazę danych „ Baza danych H2 ”, a następnie dodać następującą zależność:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Jak znaleźć zależność i jak ją opisać, wskazano na oficjalnych stronach dostawcy bazy danych lub w „ Maven Central ”. Jak rozumiesz, sterownik JDBC nie jest bazą danych. Ale on jest po nim jedynie przewodnikiem. Ale istnieje coś takiego jak „ Bazy danych w pamięci ”. Są to bazy danych, które istnieją w pamięci przez cały okres istnienia aplikacji. Zwykle jest to często wykorzystywane do celów testowych lub szkoleniowych. Pozwala to uniknąć instalowania na komputerze osobnego serwera bazy danych. Co jest dla nas bardzo odpowiednie do zapoznania się z JDBC. Nasza piaskownica jest gotowa i zaczynamy.
JDBC, czyli tam, gdzie wszystko się zaczyna – 5

Połączenie

Mamy więc sterownik JDBC i API JDBC. Jak pamiętamy, JDBC oznacza Java DataBase Connectivity. Dlatego wszystko zaczyna się od Łączności - możliwości nawiązania połączenia. A połączenie to połączenie. Wróćmy jeszcze raz do tekstu specyfikacji JDBC i spójrzmy na spis treści. W rozdziale „ ROZDZIAŁ 4 Przegląd ” (przegląd) przechodzimy do sekcji „ 4.1 Nawiązanie połączenia ” (nawiązanie połączenia) jest powiedziane, że istnieją dwa sposoby połączenia z bazą danych:
  • Przez DriverManager
  • Za pośrednictwem źródła danych
Zajmijmy się DriverManagerem. Jak powiedziano, DriverManager umożliwia połączenie się z bazą danych pod określonym adresem URL, a także ładuje sterowniki JDBC, które znalazł w CLASSPATH (a wcześniej, przed JDBC 4.0, trzeba było samodzielnie załadować klasę sterownika). Osobny rozdział „ROZDZIAŁ 9 Połączenia” poświęcony jest łączeniu się z bazą danych. Interesuje nas, jak uzyskać połączenie poprzez DriverManager, dlatego interesuje nas sekcja „9.3 Klasa DriverManager”. Wskazuje, w jaki sposób możemy uzyskać dostęp do bazy danych:
Connection con = DriverManager.getConnection(url, user, passwd);
Parametry można pobrać ze strony internetowej wybranej przez nas bazy danych. W naszym przypadku jest to H2 - „ Ściągawka H2 ”. Przejdźmy do klasy AppTest przygotowanej przez Gradle. Zawiera testy JUnit. Test JUnit to metoda oznaczona adnotacją @Test. Testy jednostkowe nie są tematem tej recenzji, dlatego ograniczymy się po prostu do zrozumienia, że ​​są to metody opisane w określony sposób, których celem jest coś przetestować. Zgodnie ze specyfikacją JDBC i stroną H2 sprawdzimy czy otrzymaliśmy połączenie z bazą danych. Napiszmy metodę uzyskania połączenia:
private Connection getNewConnection() throws SQLException {
	String url = "jdbc:h2:mem:test";
	String user = "sa";
	String passwd = "sa";
	return DriverManager.getConnection(url, user, passwd);
}
Napiszmy teraz test dla tej metody, który sprawdzi, czy połączenie faktycznie zostało nawiązane:
@Test
public void shouldGetJdbcConnection() throws SQLException {
	try(Connection connection = getNewConnection()) {
		assertTrue(connection.isValid(1));
		assertFalse(connection.isClosed());
	}
}
Po wykonaniu tego testu sprawdza się, czy powstałe połączenie jest prawidłowe (poprawnie utworzone) i czy nie zostało zamknięte. Korzystając z funkcji try-with-resources, zwolnimy zasoby, gdy nie będziemy już ich potrzebować. To ochroni nas przed zwiotczeniem połączeń i wyciekami pamięci. Ponieważ wszelkie akcje z bazą danych wymagają połączenia, na początku testu udostępnijmy pozostałym metodom testowym oznaczonym @Test połączenie z połączeniem, które udostępnimy po teście. Do tego potrzebne są nam dwie adnotacje: @Before i @After Dodajmy do klasy AppTest nowe pole, które będzie przechowywać połączenie JDBC na potrzeby testów:
private static Connection connection;
I dodajmy nowe metody:
@Before
public void init() throws SQLException {
	connection = getNewConnection();
}
@After
public void close() throws SQLException {
	connection.close();
}
Teraz każda metoda testowa ma gwarancję posiadania połączenia JDBC i nie musi go tworzyć za każdym razem.
JDBC, czyli tam, gdzie wszystko się zaczyna – 6

Sprawozdania

Następnie interesują nas Instrukcje lub wyrażenia. Zostały one opisane w dokumentacji w rozdziale „ ROZDZIAŁ 13 Oświadczenia ”. Po pierwsze, mówi, że istnieje kilka typów lub typów instrukcji:
  • Instrukcja: wyrażenie SQL, które nie zawiera parametrów
  • PrzygotowaneStatement : Przygotowana instrukcja SQL zawierająca parametry wejściowe
  • CallableStatement: wyrażenie SQL z możliwością uzyskania wartości zwracanej z procedur składowanych SQL.
Zatem mając połączenie możemy wykonać jakieś żądanie w ramach tego połączenia. Dlatego logiczne jest, że początkowo uzyskamy instancję wyrażenia SQL od Connection. Musisz zacząć od utworzenia tabeli. Opiszmy żądanie utworzenia tabeli jako zmienną typu String. Jak to zrobić? Skorzystajmy z samouczka, takiego jak „ sqltutorial.org ”, „ sqlbolt.com ”, „ postgresqltutorial.com ”, „ codecademy.com ”. Posłużmy się np. przykładem z kursu SQL na khanacademy.org . Dodajmy metodę wykonania wyrażenia w bazie danych:
private int executeUpdate(String query) throws SQLException {
	Statement statement = connection.createStatement();
	// Для Insert, Update, Delete
	int result = statement.executeUpdate(query);
	return result;
}
Dodajmy metodę tworzenia tabeli testowej przy użyciu poprzedniej metody:
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);
}
Teraz przetestujmy to:
@Test
public void shouldCreateCustomerTable() throws SQLException {
	createCustomerTable();
	connection.createStatement().execute("SELECT * FROM customers");
}
Teraz wykonajmy żądanie, nawet z parametrem:
@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 nie obsługuje nazwanych parametrów dla PreparedStatement, dlatego same parametry określamy pytaniami, a podając wartość wskazujemy indeks pytania (zaczynając od 1, a nie od zera). W ostatnim teście otrzymaliśmy wartość true jako wskazówkę, czy istnieje wynik. Ale w jaki sposób wynik zapytania jest reprezentowany w interfejsie API JDBC? Jest on prezentowany jako zestaw wyników.
JDBC, czyli tam, gdzie wszystko się zaczyna – 7

Zestaw wyników

Koncepcja zestawu wyników została opisana w specyfikacji API JDBC w rozdziale „ROZDZIAŁ 15 Zestawy wyników”. Przede wszystkim mówi, że ResultSet udostępnia metody pobierania i manipulowania wynikami wykonanych zapytań. Oznacza to, że jeśli metoda wykonania zwróciła nam wartość true, wówczas możemy otrzymać zestaw wyników. Przenieśmy wywołanie metody createCustomerTable() do metody init, która jest oznaczona jako @Before. Zakończmy teraz nasz test 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);
}
Warto tutaj zaznaczyć, że obok znajduje się metoda przesuwająca tzw. „kursor”. Kursor w zestawie wyników wskazuje jakiś wiersz. Aby więc przeczytać linię, należy umieścić na niej ten właśnie kursor. Po przesunięciu kursora metoda przesuwania kursora zwraca wartość true, jeśli kursor jest prawidłowy (poprawny, poprawny), to znaczy wskazuje na dane. Jeśli zwróci false, oznacza to, że nie ma danych, to znaczy kursor nie wskazuje danych. Jeśli spróbujemy pobrać dane za pomocą nieprawidłowego kursora, otrzymamy błąd: Brak danych. Ciekawe jest również to, że za pomocą ResultSet możesz aktualizować, a nawet wstawiać wiersze:
@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();
}

Zestaw wierszy

Oprócz ResultSet, JDBC wprowadza koncepcję RowSet. Więcej informacji można przeczytać tutaj: „ Podstawy JDBC: używanie obiektów RowSet ”. Istnieją różne warianty użycia. Na przykład najprostszy przypadek może wyglądać następująco:
@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);
}
Jak widać, RowSet przypomina symbiozę instrukcji (przez nią określiliśmy polecenie) i wykonanego polecenia. Za jego pośrednictwem sterujemy kursorem (wywołując kolejną metodę) i pobieramy z niego dane. Ciekawe jest nie tylko to podejście, ale także możliwe wdrożenia. Na przykład CachedRowSet. Jest „rozłączony” (to znaczy nie korzysta ze stałego połączenia z bazą danych) i wymaga jawnej synchronizacji z bazą danych:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Więcej informacji można znaleźć w samouczku na stronie Oracle: „ Korzystanie z CachedRowSetObjects ”.
JDBC, czyli tam, gdzie wszystko się zaczyna – 8

Metadane

Oprócz zapytań połączenie z bazą danych (czyli instancją klasy Connection) zapewnia dostęp do metadanych – danych o tym, jak skonfigurowana i zorganizowana jest nasza baza danych. Ale najpierw wspomnijmy o kilku kluczowych punktach: Adres URL umożliwiający połączenie z naszą bazą danych: „jdbc:h2:mem:test”. test to nazwa naszej bazy danych. W przypadku interfejsu API JDBC jest to katalog. A nazwa będzie pisana wielkimi literami, czyli TEST. Domyślny schemat dla H2 to PUBLIC. Napiszmy teraz test, który pokaże wszystkie tabele użytkowników. Dlaczego niestandardowe? Ponieważ bazy danych zawierają nie tylko tabele użytkownika (te, które sami stworzyliśmy za pomocą wyrażeń tabelowych create), ale także tabele systemowe. Są niezbędne do przechowywania informacji systemowych o strukturze bazy danych. Każda baza danych może inaczej przechowywać takie tabele systemowe. Przykładowo w H2 są one zapisane w schemacie „ INFORMATION_SCHEMA ”. Co ciekawe, SCHEMA INFORMACYJNY jest powszechnym podejściem, ale Oracle poszedł inną drogą. Więcej możesz przeczytać tutaj: „ INFORMATION_SCHEMA i Oracle ”. Napiszmy test, który otrzyma metadane z tabel użytkownika:
@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, czyli tam, gdzie wszystko się zaczyna – 9

Pula połączeń

Pula połączeń w specyfikacji JDBC zawiera sekcję o nazwie „Rozdział 11 Pula połączeń”. Podaje także główne uzasadnienie potrzeby posiadania puli połączeń. Każde Coonection to fizyczne połączenie z bazą danych. Jego utworzenie i zamknięcie jest dość „kosztownym” zajęciem. JDBC udostępnia jedynie interfejs API łączenia połączeń. Dlatego wybór wdrożenia pozostaje nasz. Na przykład takie implementacje obejmują HikariCP . W związku z tym będziemy musieli dodać pulę do zależności naszego projektu:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Teraz trzeba jakoś wykorzystać tę pulę. Aby to zrobić, musisz zainicjować źródło danych, zwane także źródłem danych:
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;
}
Oraz napiszmy test na odebranie połączenia z puli:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC, czyli tam, gdzie wszystko się zaczyna – 10

Transakcje

Jedną z najciekawszych rzeczy w JDBC są transakcje. W specyfikacji JDBC przypisany jest im rozdział „ROZDZIAŁ 10 Transakcje”. Przede wszystkim warto zrozumieć, czym jest transakcja. Transakcja to grupa logicznie połączonych, sekwencyjnych operacji na danych, przetwarzanych lub anulowanych jako całość. Kiedy rozpoczyna się transakcja w przypadku korzystania z JDBC? Jak podaje specyfikacja, jest to obsługiwane bezpośrednio przez sterownik JDBC. Zwykle jednak nowa transakcja rozpoczyna się, gdy bieżąca instrukcja SQL wymaga transakcji, a transakcja nie została jeszcze utworzona. Kiedy transakcja się kończy? Jest to kontrolowane przez atrybut automatycznego zatwierdzania. Jeśli włączone jest automatyczne zatwierdzanie, transakcja zostanie zakończona po „ukończeniu” instrukcji SQL. Znaczenie słowa „gotowe” zależy od typu wyrażenia SQL:
  • Język manipulacji danymi, znany również jako DML (Insert, Update, Delete)
    Transakcja zostaje zakończona w momencie zakończenia akcji
  • Wybierz wyciągi
    Transakcja zostaje zakończona po zamknięciu zestawu wyników ( ResultSet#close )
  • CallableStatement i wyrażenia zwracające wiele wyników
    Gdy wszystkie powiązane zestawy wyników zostały zamknięte i odebrane zostały wszystkie dane wyjściowe (w tym liczba aktualizacji)
Dokładnie tak zachowuje się interfejs API JDBC. Jak zwykle napiszmy test na to:
@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);
}
To proste. Ale to prawda, o ile mamy tylko jedną transakcję. Co zrobić, gdy jest ich kilka? Muszą być odizolowani od siebie. Porozmawiajmy zatem o poziomach izolacji transakcji i o tym, jak JDBC sobie z nimi radzi.
JDBC, czyli tam, gdzie wszystko się zaczyna – 11

Poziomy izolacji

Otwórzmy podsekcję „10.2 Poziomy izolacji transakcji” specyfikacji JDBC. Tutaj, zanim przejdę dalej, chciałbym pamiętać o czymś takim jak ACID. ACID opisuje wymagania stawiane systemowi transakcyjnemu.
  • Atomowość:
    Żadna transakcja nie zostanie częściowo zatwierdzona w systemie. Albo zostaną wykonane wszystkie jego podoperacje, albo nie zostanie wykonana żadna.
  • Spójność:
    każda udana transakcja z definicji rejestruje tylko prawidłowe wyniki.
  • Izolacja:
    Podczas trwania transakcji równoczesne transakcje nie powinny mieć wpływu na jej wynik.
  • Trwałość:
    Jeśli transakcja zostanie pomyślnie zakończona, wprowadzone w niej zmiany nie zostaną cofnięte z powodu jakiegokolwiek niepowodzenia.
Mówiąc o poziomach izolacji transakcji, mówimy o wymogu „Izolacji”. Izolacja jest kosztownym wymogiem, dlatego w prawdziwych bazach danych istnieją tryby, które nie izolują całkowicie transakcji (poziomy izolacji Odczytu powtarzalnego i niższe). Wikipedia ma doskonałe wyjaśnienie problemów, które mogą pojawić się podczas pracy z transakcjami. Więcej warto przeczytać tutaj: „ Problemy dostępu równoległego za pomocą transakcji ”. Zanim napiszemy nasz test, zmieńmy nieco nasz Gradle Build Script: dodajmy blok z właściwościami, czyli ustawieniami naszego projektu:
ext {
    h2Version = '1.3.176' // 1.4.177
    hikariVersion = '3.3.1'
    junitVersion = '4.12'
}
Następnie używamy tego w wersjach:
dependencies {
    implementation "com.h2database:h2:${h2Version}"
    implementation "com.zaxxer:HikariCP:${hikariVersion}"
    testImplementation "junit:junit:${junitVersion}"
}
Być może zauważyłeś, że wersja h2 stała się niższa. Zobaczymy dlaczego później. Jak zatem zastosować poziomy izolacji? Spójrzmy od razu na mały praktyczny przykład:
@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);
}
Co ciekawe, ten test może zakończyć się niepowodzeniem w przypadku dostawcy, który nie obsługuje TRANSACTION_READ_UNCOMMITTED (na przykład sqlite lub HSQL). A poziom transakcji może po prostu nie działać. Pamiętasz, że wskazaliśmy wersję sterownika bazy danych H2? Jeśli podniesiemy go do h2Version = '1.4.177' i wyżej, to READ UNCOMMITTED przestanie działać, chociaż nie zmieniliśmy kodu. To po raz kolejny udowadnia, że ​​wybór dostawcy i wersji sterownika to nie tylko litery, ale tak naprawdę determinuje sposób, w jaki Twoje żądania będą realizowane. O tym, jak naprawić to zachowanie w wersji 1.4.177 i jak to nie działa w wyższych wersjach, możesz przeczytać tutaj: „ Wsparcie poziomu izolacji READ UNCOMMITTED w trybie MVStore ”.
JDBC, czyli tam, gdzie wszystko się zaczyna – 12

Konkluzja

Jak widzimy JDBC to potężne narzędzie w rękach Javy do pracy z bazami danych. Mam nadzieję, że ta krótka recenzja pomoże Ci uzyskać punkt wyjścia lub odświeżyć pamięć. Cóż, na przekąskę, kilka dodatkowych materiałów: #Wiaczesław
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION