ClusterJ - Arbeiten mit MySQL NDB Cluster von Java

Hallo habr In diesem Artikel möchte ich eine Bibliothek für Java wie ClusterJ , die es sehr einfach macht, mit der MySQL NDBCLUSTER Engine aus Java Code zu arbeiten, einer API auf hoher Ebene, deren Konzept JPA und Hibernate ähnelt.


Im Rahmen dieses Artikels erstellen wir eine einfache Anwendung auf SpringBoot und erstellen einen Starter mit ClusterJ an Bord für die bequeme Verwendung in Anwendungen mit ClusterJ . Wir werden einfache Tests mit JUnit5 und JUnit5 schreiben, die die grundlegende Verwendung der API zeigen.
Ich werde auch über einige Mängel sprechen, denen ich bei der Arbeit mit ihr begegnen musste.


Wen kümmert es, willkommen bei Katze.


Einleitung


MySQL NDB Cluster aktiv bei der Arbeit verwendet. In einem der Projekte bestand die Aufgabe aus Gründen der Geschwindigkeit darin, die ClusterJ Bibliothek anstelle der üblichen JDBC , die in ihrer API JPA sehr ähnlich ist und tatsächlich einen Wrapper über die Bibliothek libndbclient.so , die sie über JNI .


Für diejenigen, die sich nicht auskennen, ist MySQL NDB Cluster eine hoch zugängliche und redundante MySQL-Version, die für eine verteilte Computerumgebung angepasst ist, die die NDB Speicher- NDB ( NDBCLUSTER ) für die Arbeit in einem Cluster verwendet. Ich möchte hier nicht im Detail darauf eingehen, Sie können hier und hier mehr lesen

Es gibt zwei Möglichkeiten, mit dieser Datenbank aus Java-Code zu arbeiten:


  • Standard über JDBC und SQL Abfragen
  • Über ClusterJ für leistungsstarken Datenzugriff in der MySQL Cluster .

Bild


ClusterJ basiert auf 4 Schlüsselkonzepten:


  • SessionFactory - ein Analogon zum Verbindungspool, mit dem eine Sitzung SessionFactory . Jede Instanz des Clusters muss über eine eigene SessionFactory verfügen.
  • Session - ist eine direkte Verbindung zu einem MySQL Cluster.
  • Domain Object - Eine mit Anmerkungen versehene Schnittstelle, die eine Zuordnung einer Tabelle zu Java Code darstellt, ähnlich wie JPA .
  • Transaction - ist eine atomare Arbeitseinheit. Zu jedem Zeitpunkt wird in einer Sitzung eine Transaktion ausgeführt. Jede Operation (Empfangen, Einfügen, Aktualisieren, Löschen) wird in einer neuen Transaktion ausgeführt.

ClusterJ-Einschränkungen:


  • Mangel an JOINs
  • Es gibt keine Möglichkeit, eine Tabelle und Indizes zu erstellen. Verwenden Sie dazu JDBC .
  • Kein verzögertes Laden ( Lazy ). Der gesamte Datensatz wird auf einmal heruntergeladen.
  • In Domänenobjekten ist es nicht möglich, Beziehungen zwischen Tabellen zu definieren. Die Ähnlichkeit von OneToMany , ManyToOne , ManyToMany fehlt vollständig.

Übe. Reden ist billig. Zeig mir den Code.


Nun, genug Theorie, lasst uns zur Praxis übergehen.


Das erste Problem ist das Fehlen von ClusterJ im zentralen Maven-Repository. Installieren Sie die Bibliothek mit Stiften im lokalen Repository. Es ist klar, dass es für immer in Nexus oder einem Artifactory liegen sollte, aber für unser Beispiel ist dies unnötig.


Gehen Sie also hierher und wählen Sie Ihr Betriebssystem. Wenn Sie ein Linux ähnliches Betriebssystem verwenden, laden Sie das Paket mit dem Namen mysql-cluster-community-java herunter und installieren Sie dieses rpm / deb-Paket. Wenn Sie Windows , laden Sie das vollständige mysql-cluster-gp Archiv herunter.


Auf die eine oder andere Weise haben wir eine JAR-Datei der Form: clusterj-{version}.jar . Wir setzen es durch maven :


 mvn install:install-file -DgroupId=com.mysql.ndb -DartifactId=clusterj -Dversion={version} -Dpackaging=jar -Dfile=clusterj-{version}.jar -DgeneratePom=true 

Wir benötigen auch die libndbclient Bibliothek, eine Reihe von C++ Funktionen für die Arbeit mit der NDB API , die ClusterJ über die JNI aufruft. Unter Windows diese Bibliothek (.dll) im Archiv ndbclient_{version} mysql-cluster-gp Linux Sie das Paket ndbclient_{version} herunterladen.


Erstellen Sie als Nächstes ein Projekt. Wir werden SpringBoot , JUnit5 + TestContainers für Tests verwenden.


Die endgültige Struktur des Projekts


Das Projekt besteht aus zwei Modulen:


  • clusterj-spring-boot-starter ist ein Starter, der ClusterJ sowie eine Konfiguration enthält. Dank dieses Starters können wir die Verbindung zu MySQL NDB in unserer Datei appliation.yml wie folgt beschreiben:

 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB 

Danach erstellt SpringBoot für uns die für die Verbindung erforderliche SessionFactory Factory.


  • clusterj-app ist die Anwendung selbst, die unser Starter verwenden wird. Lassen Sie uns näher darauf eingehen.

Um zu beginnen, müssen wir ein Domänenmodell wie JPA erstellen. Nur in diesem Fall müssen wir dies in Form einer Schnittstelle tun, deren Implementierung zur clusterj :


 import com.mysql.clusterj.annotation.Column; import com.mysql.clusterj.annotation.PersistenceCapable; import com.mysql.clusterj.annotation.PrimaryKey; @PersistenceCapable(table = "user") public interface User { @PrimaryKey int getId(); void setId(int id); @Column(name = "firstName") String getFirstName(); void setFirstName(String firstName); @Column(name = "lastName") String getLastName(); void setLastName(String lastName); } 

Es gibt sofort ein Problem. Die Annotation PersistenceCapable den Namen des Schemas oder der Datenbank angeben, in der sich die Tabelle befindet. Dies funktioniert jedoch nicht. Absolut. In ClusterJ dies nicht implementiert. Daher sollten sich alle Tabellen, die über ClusterJ , im selben Schema befinden, was zu einem Speicherauszug von Tabellen führt, die sich logischerweise in verschiedenen Schemas befinden sollten.


Versuchen wir nun, diese Schnittstelle zu verwenden. Dazu schreiben wir einen einfachen Test.


Um MySQL Cluster nicht zu installieren, verwenden wir die wunderbare Bibliothek zum Testen der Integration von TestContainers und Docker . Da wir JUnit5 verwenden, werden wir eine einfache Extension schreiben:


Quellcode der Erweiterung
 import com.github.dockerjava.api.model.Network; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.extension.Extension; import org.testcontainers.containers.BindMode; import org.testcontainers.containers.GenericContainer; import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.shaded.com.google.common.collect.ImmutableMap; import java.time.Duration; import java.util.stream.Stream; @Slf4j class MySQLClusterTcExtension implements Extension { private static final String MYSQL_USER = "sys"; private static final String MYSQL_PASSWORD = "qwerty"; private static final String CLUSTERJ_DATABASE = "NDB_DB"; private static Network.Ipam getIpam() { Network.Ipam ipam = new Network.Ipam(); ipam.withDriver("default"); Network.Ipam.Config config = new Network.Ipam.Config(); config.withSubnet("192.168.0.0/16"); ipam.withConfig(config); return ipam; } private static org.testcontainers.containers.Network network = org.testcontainers.containers.Network.builder() .createNetworkCmdModifier(createNetworkCmd -> createNetworkCmd.withIpam(getIpam())) .build(); private static GenericContainer ndbMgmd = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.2")) .withCommand("ndb_mgmd") .withExposedPorts(1186) .waitingFor(Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(150))); private static GenericContainer ndbd1 = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.3")) .withCommand("ndbd"); private static GenericContainer ndbMysqld = new GenericContainer<>("mysql/mysql-cluster") .withNetwork(network) .withCommand("mysqld") .withCreateContainerCmdModifier(createContainerCmd -> createContainerCmd.withIpv4Address("192.168.0.10")) .withClasspathResourceMapping("mysql-cluster.cnf", "/etc/mysql-cluster.cnf", BindMode.READ_ONLY) .withClasspathResourceMapping("my.cnf", "/etc/my.cnf", BindMode.READ_ONLY) .waitingFor(Wait.forListeningPort()) .withEnv(ImmutableMap.of("MYSQL_DATABASE", CLUSTERJ_DATABASE, "MYSQL_USER", MYSQL_USER, "MYSQL_PASSWORD", MYSQL_PASSWORD)) .withExposedPorts(3306) .waitingFor(Wait.forListeningPort()); static { log.info("Start MySQL Cluster testcontainers extension...\n"); Stream.of(ndbMgmd, ndbd1, ndbMysqld).forEach(GenericContainer::start); String ndbUrl = ndbMgmd.getContainerIpAddress() + ":" + ndbMgmd.getMappedPort(1186); String mysqlUrl = ndbMysqld.getContainerIpAddress() + ":" + ndbMysqld.getMappedPort(3306); String mysqlConnectionString = "jdbc:mysql://" + mysqlUrl + "/" + CLUSTERJ_DATABASE + "?useUnicode=true" + "&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&useSSL=false"; System.setProperty("clusterj.connectString", ndbUrl); System.setProperty("clusterj.dataBaseName", CLUSTERJ_DATABASE); System.setProperty("spring.datasource.username", MYSQL_USER); System.setProperty("spring.datasource.password", MYSQL_PASSWORD); System.setProperty("spring.datasource.url", mysqlConnectionString); } } 

In dieser Erweiterung erhöhen wir den Steuerknoten des Clusters, ein Datum für den Knoten und den MySQL Knoten. Danach legen wir die entsprechenden Verbindungseinstellungen für SpringBoot fest, genau die, die wir in der automatischen Starterkonfiguration beschrieben haben:


 System.setProperty("clusterj.connectString", ndbUrl); System.setProperty("clusterj.dataBaseName", CLUSTERJ_DATABASE); System.setProperty("spring.datasource.username", MYSQL_USER); System.setProperty("spring.datasource.password", MYSQL_PASSWORD); System.setProperty("spring.datasource.url", mysqlConnectionString); 

Als Nächstes schreiben wir eine Anmerkung, mit der wir Container in Tests deklarativ anheben können. Hier ist alles sehr einfach, wir verwenden unsere Erweiterung:


 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith(MySQLClusterTcExtension.class) public @interface EnableMySQLClusterContainer { } 

Zum Schluss schreiben wir den Test:


 @Test void shouldGetUserViaClusterJ() { User newUser = session.newInstance(User.class); newUser.setId(1); newUser.setFirstName("John"); newUser.setLastName("Jonson"); session.persist(newUser); User userFromDb = session.find(User.class, 1); assertAll( () -> assertEquals(userFromDb.getId(), 1), () -> assertEquals(userFromDb.getFirstName(), "John"), () -> assertEquals(userFromDb.getLastName(), "Jonson")); } 

Dieser Test zeigt, wie wir den Datensatz per Primärschlüssel erhalten können. Diese Abfrage entspricht der SQL Abfrage:


 SELECT * FROM user WHERE id = 1; 

Lassen Sie uns einen weiteren Test mit komplexerer Logik durchführen:


 @Test void queryBuilderTest() { QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class); // parameter PredicateOperand propertyIdParam = userQueryDomainType.param("lastName"); // property PredicateOperand propertyEntityId = userQueryDomainType.get("lastName"); userQueryDomainType.where(propertyEntityId.equal(propertyIdParam)); Query<User> query = session.createQuery(userQueryDomainType); query.setParameter("lastName", "Jonson"); List<User> foundEntities = query.getResultList(); Optional<User> firstUser = foundEntities.stream().filter(u -> u.getId() == 1).findFirst(); Optional<User> secondUser = foundEntities.stream().filter(u -> u.getId() == 2).findFirst(); assertAll( () -> assertEquals(foundEntities.size(), 2), () -> assertTrue(firstUser.isPresent()), () -> assertTrue(secondUser.isPresent()), () -> assertThat(firstUser.get(), allOf( hasProperty("firstName", equalTo("John")), hasProperty("lastName", equalTo("Jonson")) ) ), () -> assertThat(secondUser.get(), allOf( hasProperty("firstName", equalTo("Alex")), hasProperty("lastName", equalTo("Jonson")) ) ) ); } 

QueryBuilder verwendet, um komplexe Abfragen mit equal QueryBuilder zu erstellen. In diesem Test ziehen wir alle Benutzer heraus, deren Nachname = Jonson ist. Diese Abfrage entspricht der folgenden SQL :


 SELECT * FROM user WHERE lastName = 'Jonson'; 

Auch hier ist ein Problem aufgetreten. Abfrage des Formulars kann nicht erstellt werden:


 SELECT * FROM user WHERE (lastName = 'Jonson' and firstName = 'John') or id = 2; 

Diese Funktion ist derzeit nicht implementiert. Sie können den Test sehen: andOrNotImplemented .


Vollständiges Testbeispiel
 @SpringBootTest @ExtendWith(SpringExtension.class) @EnableAutoConfiguration @EnableMySQLClusterContainer class NdbClusterJTest { @Autowired private JdbcTemplate jdbcTemplate; @Autowired private SessionFactory sessionFactory; private Session session; @BeforeEach void setUp() { jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `user` (id INT NOT NULL PRIMARY KEY," + " firstName VARCHAR(64) DEFAULT NULL," + " lastName VARCHAR(64) DEFAULT NULL) ENGINE=NDBCLUSTER;"); session = sessionFactory.getSession(); } @Test void shouldGetUserViaClusterJ() { User newUser = session.newInstance(User.class); newUser.setId(1); newUser.setFirstName("John"); newUser.setLastName("Jonson"); session.persist(newUser); User userFromDb = session.find(User.class, 1); assertAll( () -> assertEquals(userFromDb.getId(), 1), () -> assertEquals(userFromDb.getFirstName(), "John"), () -> assertEquals(userFromDb.getLastName(), "Jonson")); } @Test void queryBuilderTest() { User newUser1 = session.newInstance(User.class); newUser1.setId(1); newUser1.setFirstName("John"); newUser1.setLastName("Jonson"); User newUser2 = session.newInstance(User.class); newUser2.setId(2); newUser2.setFirstName("Alex"); newUser2.setLastName("Jonson"); session.persist(newUser1); session.persist(newUser2); QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class); // parameter PredicateOperand propertyIdParam = userQueryDomainType.param("lastName"); // property PredicateOperand propertyEntityId = userQueryDomainType.get("lastName"); userQueryDomainType.where(propertyEntityId.equal(propertyIdParam)); Query<User> query = session.createQuery(userQueryDomainType); query.setParameter("lastName", "Jonson"); List<User> foundEntities = query.getResultList(); Optional<User> firstUser = foundEntities.stream().filter(u -> u.getId() == 1).findFirst(); Optional<User> secondUser = foundEntities.stream().filter(u -> u.getId() == 2).findFirst(); assertAll( () -> assertEquals(foundEntities.size(), 2), () -> assertTrue(firstUser.isPresent()), () -> assertTrue(secondUser.isPresent()), () -> assertThat(firstUser.get(), allOf( hasProperty("firstName", equalTo("John")), hasProperty("lastName", equalTo("Jonson")) ) ), () -> assertThat(secondUser.get(), allOf( hasProperty("firstName", equalTo("Alex")), hasProperty("lastName", equalTo("Jonson")) ) ) ); } @Test void andOrNotImplemented() { QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class); // parameter PredicateOperand firstNameParam = userQueryDomainType.param("firstName"); // property PredicateOperand firstName = userQueryDomainType.get("firstName"); // parameter PredicateOperand lastNameParam = userQueryDomainType.param("lastName"); // property PredicateOperand lastName = userQueryDomainType.get("lastName"); // parameter PredicateOperand idParam = userQueryDomainType.param("id"); // property PredicateOperand id = userQueryDomainType.get("id"); Executable executable = () -> userQueryDomainType.where(firstNameParam.equal(firstName) .and(lastNameParam.equal(lastName)) .or(idParam.equal(id))); UnsupportedOperationException exception = assertThrows(UnsupportedOperationException.class, executable); assertEquals("Not implemented.", exception.getMessage()); } @AfterEach void tearDown() { session.deletePersistentAll(User.class); session.close(); } } 

Dank unserer Anmerkung @EnableMySQLClusterContainer haben wir die Details der Vorbereitung der Umgebung für Tests @EnableMySQLClusterContainer . Dank unseres Starters können wir SessionFactory einfach in unseren Test einfügen und für unsere Anforderungen verwenden, ohne uns Gedanken darüber machen zu müssen, dass es manuell erstellt werden muss.
All dies konzentriert uns darauf, die Geschäftslogik der Tests zu schreiben und nicht die bedienende Infrastruktur.


Ich möchte auch darauf achten, dass Sie eine Anwendung ClusterJ müssen, die ClusterJ mit dem folgenden Parameter verwendet:


 -Djava.library.path=/usr/lib/x86_64-linux-gnu/ 

Hier wird der Pfad zu libndbclient.so . Ohne es wird nichts funktionieren.


Fazit


Für mich ist ClusterJ gute Sache in Systemen, die für die Geschwindigkeit des Datenzugriffs von entscheidender Bedeutung sind, aber kleinere Mängel und Einschränkungen beeinträchtigen den Gesamteindruck. Wenn Sie die Möglichkeit haben zu wählen und sich nicht um die Geschwindigkeit des Zugriffs kümmern, ist es meiner Meinung nach besser, JDBC zu verwenden.


Der Artikel hat nicht in Betracht gezogen, mit Transaktionen und Sperren zu arbeiten, und so stellte sich heraus, dass es ziemlich viel war.


Das war's, Happy Coding!


Nützliche Links:


Der gesamte Code mit dem Projekt liegt hier
Seite herunterladen
Informationen zu ClusterJ
Arbeiten Sie mit Java und NDB Cluster
Pro MySQL NDB-Clusterbuch
Mehr über MySQL NDB Cluster hier und hier


Noch mehr Testfälle im MySQL Repository .

Source: https://habr.com/ru/post/de472468/


All Articles