JavaRush /Java-Blog /Random-DE /JDBC oder wo alles beginnt
Viacheslav
Level 3

JDBC oder wo alles beginnt

Veröffentlicht in der Gruppe Random-DE
In der modernen Welt gibt es keinen Weg ohne Datenspeicherung. Und die Geschichte der Arbeit mit Datenbanken begann vor sehr langer Zeit, mit dem Aufkommen von JDBC. Ich schlage vor, mich an etwas zu erinnern, auf das kein modernes, auf JDBC basierendes Framework verzichten kann. Darüber hinaus benötigen Sie manchmal auch bei der Zusammenarbeit mit ihnen die Möglichkeit, „zu Ihren Wurzeln zurückzukehren“. Ich hoffe, dass diese Rezension als Einführung oder zur Auffrischung Ihres Gedächtnisses hilfreich sein wird.
JDBC oder wo alles beginnt – 1

Einführung

Einer der Hauptzwecke einer Programmiersprache ist das Speichern und Verarbeiten von Informationen. Um die Funktionsweise der Datenspeicherung besser zu verstehen, lohnt es sich, ein wenig Zeit in die Theorie und Architektur von Anwendungen zu investieren. Sie können zum Beispiel die Literatur lesen, nämlich das Buch „ Software Architect's Handbook: Become a Successful Software Architect by Implementating Effective Arch... “ von Joseph Ingeno. Wie gesagt, es gibt eine bestimmte Datenschicht oder „Datenschicht“. Es umfasst einen Ort zum Speichern von Daten (z. B. eine SQL-Datenbank) und Tools für die Arbeit mit einem Datenspeicher (z. B. JDBC, das noch besprochen wird). Auf der Microsoft-Website gibt es auch einen Artikel: „ Designing an Infrastructure Persistence Layer “, der die architektonische Lösung der Trennung einer zusätzlichen Schicht von der Datenschicht beschreibt – die Persistenzschicht. In diesem Fall ist die Datenschicht die Speicherebene der Daten selbst, während die Persistenzschicht eine Abstraktionsebene für die Arbeit mit Daten aus dem Speicher der Datenebene ist. Der Persistence Layer kann das „DAO“-Template oder verschiedene ORMs beinhalten. Aber ORM ist ein Thema für eine andere Diskussion. Wie Sie vielleicht bereits verstanden haben, erschien die Datenschicht zuerst. Seit der Zeit von JDK 1.1 taucht JDBC (Java DataBase Connectivity – Verbindung zu Datenbanken in Java) in der Java-Welt auf. Dies ist ein Standard für die Interaktion von Java-Anwendungen mit verschiedenen DBMS, implementiert in Form der in Java SE enthaltenen Pakete java.sql und javax.sql:
JDBC oder wo alles beginnt – 2
Dieser Standard wird durch die Spezifikation „ JSR 221 JDBC 4.1 API “ beschrieben . Diese Spezifikation sagt uns, dass die JDBC-API programmgesteuerten Zugriff auf relationale Datenbanken über in Java geschriebene Programme ermöglicht. Außerdem heißt es, dass die JDBC-API Teil der Java-Plattform ist und daher in Java SE und Java EE enthalten ist. Die JDBC-API wird in zwei Paketen bereitgestellt: java.sql und javax.sql. Dann lasst uns sie kennenlernen.
JDBC oder wo alles beginnt – 3

Beginn der Arbeiten

Um zu verstehen, was die JDBC-API im Allgemeinen ist, benötigen wir eine Java-Anwendung. Am bequemsten ist es, eines der Projektmontagesysteme zu verwenden. Verwenden wir zum Beispiel Gradle . Mehr über Gradle können Sie in einer kurzen Rezension lesen: „ Eine kurze Einführung in Gradle “. Lassen Sie uns zunächst ein neues Gradle-Projekt initialisieren. Da die Gradle-Funktionalität über Plugins implementiert wird, müssen wir zur Initialisierung das „ Gradle Build Init Plugin “ verwenden:
gradle init --type java-application
Danach öffnen wir das Build-Skript – die Datei build.gradle , die unser Projekt und die Arbeit damit beschreibt. Uns interessiert der Block „ dependencies “, in dem Abhängigkeiten beschrieben werden – also jene Bibliotheken/Frameworks/APIs, ohne die wir nicht arbeiten können und von denen wir abhängig sind. Standardmäßig sehen wir so etwas wie:
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'
}
Warum sehen wir das hier? Dies sind die Abhängigkeiten unseres Projekts, die Gradle beim Erstellen des Projekts automatisch für uns generiert hat. Und auch, weil guava eine separate Bibliothek ist, die nicht in Java SE enthalten ist. JUnit ist auch nicht in Java SE enthalten. Aber wir haben JDBC sofort einsatzbereit, das heißt, es ist Teil von Java SE. Es stellt sich heraus, dass wir JDBC haben. Großartig. Was brauchen wir noch? Es gibt so ein wunderbares Diagramm:
JDBC oder wo alles beginnt – 4
Wie wir sehen können und das ist logisch, handelt es sich bei der Datenbank um eine externe Komponente, die nicht nativ in Java SE enthalten ist. Das lässt sich einfach erklären: Es gibt eine große Anzahl von Datenbanken und Sie können mit jeder davon arbeiten. Es gibt zum Beispiel PostgreSQL, Oracle, MySQL, H2. Jede dieser Datenbanken wird von einem separaten Unternehmen namens Datenbankanbieter bereitgestellt. Jede Datenbank ist in einer eigenen Programmiersprache geschrieben (nicht unbedingt Java). Um aus einer Java-Anwendung heraus mit der Datenbank arbeiten zu können, schreibt der Datenbankanbieter einen speziellen Treiber, bei dem es sich um einen eigenen Image-Adapter handelt. Solche JDBC-kompatiblen Datenbanken (also solche, die über einen JDBC-Treiber verfügen) werden auch „JDBC-kompatible Datenbanken“ genannt. Hier können wir eine Analogie zu Computergeräten ziehen. In einem Notizblock gibt es beispielsweise eine Schaltfläche „Drucken“. Jedes Mal, wenn Sie darauf drücken, teilt das Programm dem Betriebssystem mit, dass die Notizblockanwendung drucken möchte. Und Sie haben einen Drucker. Um Ihrem Betriebssystem beizubringen, einheitlich mit einem Canon- oder HP-Drucker zu kommunizieren, benötigen Sie unterschiedliche Treiber. Für Sie als Nutzer ändert sich jedoch nichts. Sie werden immer noch dieselbe Taste drücken. Das Gleiche gilt für JDBC. Sie führen denselben Code aus, nur dass unter der Haube möglicherweise unterschiedliche Datenbanken laufen. Ich denke, das ist ein sehr klarer Ansatz. Jeder dieser JDBC-Treiber ist eine Art Artefakt, Bibliothek oder JAR-Datei. Dies ist die Abhängigkeit für unser Projekt. Wir können beispielsweise die Datenbank „ H2 Database “ auswählen und müssen dann eine Abhängigkeit wie diese hinzufügen:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
Wie man eine Abhängigkeit findet und wie man sie beschreibt, erfahren Sie auf den offiziellen Webseiten des Datenbankanbieters oder auf „ Maven Central “. Wie Sie wissen, ist der JDBC-Treiber keine Datenbank. Aber er ist nur ein Leitfaden dazu. Aber es gibt so etwas wie „ In-Memory-Datenbanken “. Hierbei handelt es sich um Datenbanken, die für die gesamte Lebensdauer Ihrer Anwendung im Speicher verbleiben. Typischerweise wird dies häufig zu Test- oder Schulungszwecken verwendet. Dadurch können Sie die Installation eines separaten Datenbankservers auf dem Computer vermeiden. Das ist für uns sehr gut geeignet, um JDBC kennenzulernen. Unsere Sandbox ist also fertig und es kann losgehen.
JDBC oder wo alles beginnt – 5

Verbindung

Wir haben also einen JDBC-Treiber und eine JDBC-API. Wie wir uns erinnern, steht JDBC für Java DataBase Connectivity. Daher beginnt alles mit der Konnektivität – der Fähigkeit, eine Verbindung herzustellen. Und Verbindung ist Verbindung. Wenden wir uns noch einmal dem Text der JDBC-Spezifikation zu und schauen uns das Inhaltsverzeichnis an. Im Kapitel „ KAPITEL 4 Überblick “ (Übersicht) wenden wir uns dem Abschnitt „ 4.1 Aufbau einer Verbindung “ (Herstellen einer Verbindung) zu. Dort heißt es, dass es zwei Möglichkeiten gibt, eine Verbindung zur Datenbank herzustellen:
  • Über DriverManager
  • Über DataSource
Kommen wir zum DriverManager. Wie bereits erwähnt, können Sie mit DriverManager eine Verbindung zur Datenbank unter der angegebenen URL herstellen und auch JDBC-Treiber laden, die im CLASSPATH gefunden wurden (und davor, vor JDBC 4.0, mussten Sie die Treiberklasse selbst laden). Über die Verbindung zur Datenbank gibt es ein separates Kapitel „KAPITEL 9 Verbindungen“. Uns interessiert, wie man eine Verbindung über den DriverManager herstellt, daher interessiert uns der Abschnitt „9.3 Die DriverManager-Klasse“. Es zeigt an, wie wir auf die Datenbank zugreifen können:
Connection con = DriverManager.getConnection(url, user, passwd);
Die Parameter können der Website der von uns gewählten Datenbank entnommen werden. In unserem Fall ist das H2 – „ H2 Cheat Sheet “. Fahren wir mit der von Gradle vorbereiteten AppTest-Klasse fort. Es enthält JUnit-Tests. Ein JUnit-Test ist eine Methode, die mit einer Annotation gekennzeichnet ist @Test. Unit-Tests sind nicht das Thema dieser Rezension, daher beschränken wir uns lediglich auf das Verständnis, dass es sich um auf eine bestimmte Weise beschriebene Methoden handelt, deren Zweck darin besteht, etwas zu testen. Gemäß der JDBC-Spezifikation und der H2-Website werden wir prüfen, ob wir eine Verbindung zur Datenbank erhalten haben. Schreiben wir eine Methode zum Herstellen einer Verbindung:
private Connection getNewConnection() throws SQLException {
	String url = "jdbc:h2:mem:test";
	String user = "sa";
	String passwd = "sa";
	return DriverManager.getConnection(url, user, passwd);
}
Schreiben wir nun einen Test für diese Methode, der überprüft, ob die Verbindung tatsächlich hergestellt wurde:
@Test
public void shouldGetJdbcConnection() throws SQLException {
	try(Connection connection = getNewConnection()) {
		assertTrue(connection.isValid(1));
		assertFalse(connection.isClosed());
	}
}
Wenn dieser Test ausgeführt wird, wird überprüft, ob die resultierende Verbindung gültig (korrekt erstellt) und nicht geschlossen ist. Durch die Verwendung von Try-with-Resources geben wir Ressourcen frei, sobald wir sie nicht mehr benötigen. Dies schützt uns vor Verbindungsabbrüchen und Speicherverlusten. Da für alle Aktionen mit der Datenbank eine Verbindung erforderlich ist, versehen wir die verbleibenden mit @Test gekennzeichneten Testmethoden zu Beginn des Tests mit einer Verbindung, die wir nach dem Test freigeben. Dazu benötigen wir zwei Annotationen: @Before und @After. Fügen wir der AppTest-Klasse ein neues Feld hinzu, das die JDBC-Verbindung für Tests speichert:
private static Connection connection;
Und fügen wir neue Methoden hinzu:
@Before
public void init() throws SQLException {
	connection = getNewConnection();
}
@After
public void close() throws SQLException {
	connection.close();
}
Jetzt verfügt jede Testmethode garantiert über eine JDBC-Verbindung und muss diese nicht jedes Mal selbst erstellen.
JDBC oder wo alles beginnt – 6

Aussagen

Als nächstes interessieren uns Aussagen oder Ausdrücke. Sie sind in der Dokumentation im Kapitel „ KAPITEL 13 Statements “ beschrieben. Erstens heißt es, dass es mehrere Arten bzw. Arten von Aussagen gibt:
  • Anweisung: SQL-Ausdruck, der keine Parameter enthält
  • PreparedStatement: Vorbereitete SQL-Anweisung mit Eingabeparametern
  • CallableStatement: SQL-Ausdruck mit der Möglichkeit, einen Rückgabewert aus gespeicherten SQL-Prozeduren abzurufen.
Wenn wir also eine Verbindung haben, können wir im Rahmen dieser Verbindung eine Anfrage ausführen. Daher ist es logisch, dass wir zunächst eine Instanz des SQL-Ausdrucks von Connection erhalten. Sie müssen mit der Erstellung einer Tabelle beginnen. Beschreiben wir die Anforderung zur Tabellenerstellung als String-Variable. Wie kann man das machen? Lassen Sie uns ein Tutorial wie „ sqltutorial.org “, „ sqlbolt.com “, „ postgresqltutorial.com “, „ codecademy.com “ verwenden. Nehmen wir zum Beispiel ein Beispiel aus dem SQL-Kurs auf khanacademy.org . Fügen wir eine Methode zum Ausführen eines Ausdrucks in der Datenbank hinzu:
private int executeUpdate(String query) throws SQLException {
	Statement statement = connection.createStatement();
	// Для Insert, Update, Delete
	int result = statement.executeUpdate(query);
	return result;
}
Fügen wir eine Methode zum Erstellen einer Testtabelle mit der vorherigen Methode hinzu:
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);
}
Jetzt testen wir das:
@Test
public void shouldCreateCustomerTable() throws SQLException {
	createCustomerTable();
	connection.createStatement().execute("SELECT * FROM customers");
}
Jetzt führen wir die Anfrage aus, und zwar sogar mit einem Parameter:
@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 unterstützt keine benannten Parameter für PreparedStatement, daher werden die Parameter selbst durch Fragen angegeben, und durch die Angabe des Werts geben wir den Fragenindex an (beginnend bei 1, nicht bei Null). Im letzten Test haben wir true als Hinweis darauf erhalten, ob ein Ergebnis vorliegt. Doch wie wird das Abfrageergebnis in der JDBC-API dargestellt? Und es wird als ResultSet dargestellt.
JDBC oder wo alles beginnt – 7

ResultSet

Das Konzept eines ResultSets ist in der JDBC-API-Spezifikation im Kapitel „KAPITEL 15 Result Sets“ beschrieben. Zunächst heißt es, dass ResultSet Methoden zum Abrufen und Bearbeiten der Ergebnisse ausgeführter Abfragen bereitstellt. Das heißt, wenn die Methode „true“ für uns zurückgegeben hat, können wir ein ResultSet erhalten. Verschieben wir den Aufruf der Methode createCustomerTable() in die Methode init, die als @Before markiert ist. Lassen Sie uns nun unseren ShouldSelectData-Test abschließen:
@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);
}
Es ist hier erwähnenswert, dass es sich bei next um eine Methode handelt, die den sogenannten „Cursor“ bewegt. Der Cursor im ResultSet zeigt auf eine Zeile. Um also eine Zeile lesen zu können, müssen Sie genau diesen Cursor darauf platzieren. Wenn der Cursor bewegt wird, gibt die Cursorbewegungsmethode „true“ zurück, wenn der Cursor gültig (richtig, richtig) ist, d. h. auf Daten zeigt. Wenn „false“ zurückgegeben wird, sind keine Daten vorhanden, d. h. der Cursor zeigt nicht auf die Daten. Wenn wir versuchen, Daten mit einem ungültigen Cursor abzurufen, erhalten wir die Fehlermeldung: Es sind keine Daten verfügbar. Interessant ist auch, dass Sie über ResultSet Zeilen aktualisieren oder sogar einfügen können:
@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

Zusätzlich zu ResultSet führt JDBC das Konzept von RowSet ein. Weitere Informationen finden Sie hier: „ JDBC-Grundlagen: Verwendung von RowSet-Objekten “. Es gibt verschiedene Nutzungsvarianten. Der einfachste Fall könnte beispielsweise so aussehen:
@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);
}
Wie Sie sehen können, ähnelt RowSet einer Symbiose aus einer Anweisung (wir haben den Befehl dadurch angegeben) und einem ausgeführten Befehl. Dadurch steuern wir den Cursor (durch Aufruf der nächsten Methode) und erhalten Daten daraus. Nicht nur dieser Ansatz ist interessant, sondern auch mögliche Umsetzungen. Beispiel: CachedRowSet. Es ist „getrennt“ (d. h. es verwendet keine dauerhafte Verbindung zur Datenbank) und erfordert eine explizite Synchronisierung mit der Datenbank:
CachedRowSet jdbcRsCached = new CachedRowSetImpl();
jdbcRsCached.acceptChanges(connection);
Weitere Informationen finden Sie im Tutorial auf der Oracle-Website: „ Using CachedRowSetObjects “.
JDBC oder wo alles beginnt – 8

Metadaten

Zusätzlich zu Abfragen bietet eine Verbindung zur Datenbank (d. h. eine Instanz der Connection-Klasse) Zugriff auf Metadaten – Daten darüber, wie unsere Datenbank konfiguriert und organisiert ist. Aber lassen Sie uns zunächst ein paar wichtige Punkte erwähnen: Die URL für die Verbindung zu unserer Datenbank: „jdbc:h2:mem:test“. test ist der Name unserer Datenbank. Für die JDBC-API ist dies ein Verzeichnis. Und der Name wird in Großbuchstaben geschrieben, also TEST. Das Standardschema für H2 ist PUBLIC. Schreiben wir nun einen Test, der alle Benutzertabellen anzeigt. Warum kundenspezifisch? Denn Datenbanken enthalten nicht nur Benutzertabellen (die wir selbst mit „create table expressions“ erstellt haben), sondern auch Systemtabellen. Sie sind notwendig, um Systeminformationen über die Struktur der Datenbank zu speichern. Jede Datenbank kann solche Systemtabellen unterschiedlich speichern. In H2 werden sie beispielsweise im Schema „ INFORMATION_SCHEMA “ gespeichert. Interessanterweise ist INFORMATION SCHEMA ein gängiger Ansatz, Oracle ging jedoch einen anderen Weg. Mehr können Sie hier lesen: „ INFORMATION_SCHEMA und Oracle “. Schreiben wir einen Test, der Metadaten zu Benutzertabellen empfängt:
@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 oder wo alles beginnt – 9

Verbindungspool

Der Verbindungspool in der JDBC-Spezifikation verfügt über einen Abschnitt namens „Kapitel 11 Verbindungspooling“. Es liefert auch die Hauptbegründung für die Notwendigkeit eines Verbindungspools. Jede Verbindung ist eine physische Verbindung zur Datenbank. Seine Erstellung und Schließung ist eine recht „kostspielige“ Arbeit. JDBC stellt nur eine Verbindungspooling-API bereit. Daher liegt die Wahl der Implementierung bei uns. Zu solchen Implementierungen gehört beispielsweise HikariCP . Dementsprechend müssen wir unserer Projektabhängigkeit einen Pool hinzufügen:
dependencies {
    implementation 'com.h2database:h2:1.4.197'
    implementation 'com.zaxxer:HikariCP:3.3.1'
    testImplementation 'junit:junit:4.12'
}
Jetzt müssen wir diesen Pool irgendwie nutzen. Dazu müssen Sie die Datenquelle, auch Datasource genannt, initialisieren:
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;
}
Und schreiben wir einen Test, um eine Verbindung aus dem Pool zu empfangen:
@Test
public void shouldGetConnectionFromDataSource() throws SQLException {
	DataSource datasource = getDatasource();
	try (Connection con = datasource.getConnection()) {
		assertTrue(con.isValid(1));
	}
}
JDBC oder wo alles beginnt – 10

Transaktionen

Eines der interessantesten Dinge an JDBC sind Transaktionen. In der JDBC-Spezifikation ist ihnen das Kapitel „KAPITEL 10 Transaktionen“ zugeordnet. Zunächst lohnt es sich zu verstehen, was eine Transaktion ist. Eine Transaktion ist eine Gruppe logisch kombinierter sequenzieller Operationen an Daten, die als Ganzes verarbeitet oder abgebrochen werden. Wann beginnt eine Transaktion bei Verwendung von JDBC? Wie in der Spezifikation angegeben, wird dies direkt vom JDBC-Treiber erledigt. Aber normalerweise beginnt eine neue Transaktion, wenn die aktuelle SQL-Anweisung eine Transaktion erfordert und die Transaktion noch nicht erstellt wurde. Wann endet die Transaktion? Dies wird durch das Auto-Commit-Attribut gesteuert. Wenn Autocommit aktiviert ist, wird die Transaktion abgeschlossen, nachdem die SQL-Anweisung „abgeschlossen“ ist. Was „erledigt“ bedeutet, hängt von der Art des SQL-Ausdrucks ab:
  • Datenmanipulationssprache, auch bekannt als DML (Einfügen, Aktualisieren, Löschen).
    Die Transaktion wird abgeschlossen, sobald die Aktion abgeschlossen ist
  • Select Statements
    Die Transaktion ist abgeschlossen, wenn das ResultSet geschlossen wird ( ResultSet#close )
  • CallableStatement und Ausdrücke, die mehrere Ergebnisse zurückgeben
    , wenn alle zugehörigen ResultSets geschlossen wurden und alle Ausgaben empfangen wurden (einschließlich der Anzahl der Aktualisierungen)
Genau so verhält sich die JDBC-API. Schreiben wir dazu wie gewohnt einen Test:
@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);
}
Es ist einfach. Dies gilt jedoch, solange wir nur eine Transaktion haben. Was tun, wenn es mehrere davon gibt? Sie müssen voneinander isoliert werden. Sprechen wir daher über Transaktionsisolationsstufen und wie JDBC damit umgeht.
JDBC oder wo alles beginnt – 11

Isolationsniveaus

Öffnen wir den Unterabschnitt „10.2 Transaction Isolation Levels“ der JDBC-Spezifikation. Bevor ich weitermache, möchte ich mich hier an etwas wie ACID erinnern. ACID beschreibt die Anforderungen an ein Transaktionssystem.
  • Atomarität:
    Keine Transaktion wird teilweise an das System übergeben. Entweder werden alle Unteroperationen ausgeführt oder keine.
  • Konsistenz:
    Jede erfolgreiche Transaktion zeichnet per Definition nur gültige Ergebnisse auf.
  • Isolation:
    Während eine Transaktion ausgeführt wird, sollten gleichzeitige Transaktionen keinen Einfluss auf das Ergebnis haben.
  • Haltbarkeit:
    Wenn eine Transaktion erfolgreich abgeschlossen wird, werden die daran vorgenommenen Änderungen aufgrund eines Fehlers nicht rückgängig gemacht.
Wenn wir über Transaktionsisolationsstufen sprechen, sprechen wir von der „Isolations“-Anforderung. Isolation ist eine kostspielige Anforderung, daher gibt es in echten Datenbanken Modi, die eine Transaktion nicht vollständig isolieren (Repeatable Read-Isolationsstufen und niedriger). Welche Probleme bei der Arbeit mit Transaktionen auftreten können, ist bei Wikipedia hervorragend erklärt. Es lohnt sich, hier mehr zu lesen: „ Probleme des parallelen Zugriffs mithilfe von Transaktionen “. Bevor wir unseren Test schreiben, ändern wir unser Gradle Build Script leicht: Fügen Sie einen Block mit Eigenschaften hinzu, also mit den Einstellungen unseres Projekts:
ext {
    h2Version = '1.3.176' // 1.4.177
    hikariVersion = '3.3.1'
    junitVersion = '4.12'
}
Als nächstes verwenden wir dies in folgenden Versionen:
dependencies {
    implementation "com.h2database:h2:${h2Version}"
    implementation "com.zaxxer:HikariCP:${hikariVersion}"
    testImplementation "junit:junit:${junitVersion}"
}
Möglicherweise ist Ihnen aufgefallen, dass die h2-Version niedriger geworden ist. Wir werden später sehen, warum. Wie wenden Sie Isolationsstufen an? Schauen wir uns gleich ein kleines Praxisbeispiel an:
@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);
}
Interessanterweise kann dieser Test bei einem Anbieter fehlschlagen, der TRANSACTION_READ_UNCOMMITTED nicht unterstützt (z. B. SQLite oder HSQL). Und die Transaktionsebene funktioniert möglicherweise einfach nicht. Erinnern Sie sich, dass wir die Version des H2-Datenbanktreibers angegeben haben? Wenn wir es auf h2Version = '1.4.177' und höher erhöhen, funktioniert READ UNCOMMITTED nicht mehr, obwohl wir den Code nicht geändert haben. Dies beweist einmal mehr, dass die Wahl des Anbieters und der Treiberversion nicht nur aus Buchstaben besteht, sondern tatsächlich darüber entscheidet, wie Ihre Anforderungen ausgeführt werden. Wie Sie dieses Verhalten in Version 1.4.177 beheben können und wie es in höheren Versionen nicht funktioniert, können Sie hier lesen: „ Unterstützt die Isolationsstufe READ UNCOMMITTED im MVStore-Modus “.
JDBC oder wo alles beginnt – 12

Endeffekt

Wie wir sehen können, ist JDBC in den Händen von Java ein leistungsstarkes Werkzeug für die Arbeit mit Datenbanken. Ich hoffe, dass diese kurze Rezension Ihnen einen Ausgangspunkt geben oder Ihr Gedächtnis auffrischen wird. Nun, als Snack noch ein paar zusätzliche Materialien: #Wjatscheslaw
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION