ClusterJ - travailler avec MySQL NDB Cluster de Java

Bonjour, Habr! Dans cet article, je veux considérer une bibliothèque pour Java telle que ClusterJ , qui rend très facile de travailler avec le moteur MySQL NDBCLUSTER à partir du code Java , qui est une API de haut niveau similaire dans son concept à JPA et Hibernate .


Dans le cadre de cet article, nous allons créer une application simple sur SpringBoot , et également réaliser un démarreur avec ClusterJ embarqué pour une utilisation pratique dans les applications utilisant l'autoconfiguration. Nous allons écrire des tests simples en utilisant JUnit5 et TestContainers , qui montreront l'utilisation de base de l'API.
Je parlerai également de plusieurs lacunes auxquelles j'ai dû faire face en travaillant avec elle.


Peu importe, bienvenue au chat.


Présentation


MySQL NDB Cluster activement utilisé au travail et dans un des projets, par souci de rapidité, la tâche consistait à utiliser la bibliothèque ClusterJ au lieu du JDBC habituel, qui par son API est très similaire à JPA , et en fait, est un wrapper sur la bibliothèque libndbclient.so , qu'elle utilise via JNI .


Pour ceux qui ne sont pas au courant, MySQL NDB Cluster est une version MySQL hautement accessible et redondante adaptée à un environnement informatique distribué qui utilise le NDB stockage NDB ( NDBCLUSTER ) pour travailler dans un cluster. Je ne veux pas m'étendre là-dessus en détail, vous pouvez en lire plus ici et ici

Il existe deux façons de travailler à partir du code Java avec cette base de données:


  • Standard, via SQL requêtes JDBC et SQL
  • Via ClusterJ , pour un accès aux données hautes performances dans la MySQL Cluster .

image


ClusterJ est construit autour de 4 concepts clés:


  • SessionFactory - un analogue du pool de connexions, utilisé pour obtenir une session. Chaque instance du cluster doit avoir sa propre SessionFactory.
  • Session - est une connexion directe à un cluster MySQL .
  • Domain Object - Une interface annotée représentant un mappage d'une table en code Java , similaire à JPA .
  • Transaction - est une unité de travail atomique. À tout moment, en une seule session, une transaction est exécutée. Toute opération (réception, insertion, mise à jour, suppression) est effectuée dans une nouvelle transaction.

Limitations de ClusterJ:


  • Manque de JOIN
  • Il n'y a aucun moyen de créer une table et des index. Pour ce faire, utilisez JDBC .
  • Aucun chargement retardé ( Lazy ). L'enregistrement entier est téléchargé en une seule fois.
  • Dans les objets de domaine, il n'est pas possible de définir des relations entre les tables. La similitude de OneToMany , ManyToOne , ManyToMany complètement absente.

Pratique. Parler est bon marché. Montrez-moi le code.


Eh bien, assez de théorie, passons à la pratique.


Le premier problème à résoudre est le manque de ClusterJ dans le référentiel central Maven. Installez la bibliothèque avec des plumes dans le référentiel local. Il est clair que pour de bon, il devrait appartenir à Nexus ou à un Artifactory , mais pour notre exemple, cela n'est pas nécessaire.


Alors, allez ici et choisissez votre système d'exploitation. Si vous Linux un système d'exploitation de type Linux , téléchargez le package appelé mysql-cluster-community-java et installez ce package rpm / deb. Si vous avez Windows , téléchargez l'archive complète mysql-cluster-gp .


D'une manière ou d'une autre, nous aurons un fichier jar de la forme: clusterj-{version}.jar . Nous l'avons mis à travers maven :


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

Nous avons également besoin de la bibliothèque libndbclient , qui est un ensemble de fonctions C++ pour travailler avec l' NDB API que ClusterJ appelle via JNI . Pour Windows cette bibliothèque (.dll) se trouve dans l'archive mysql-cluster-gp , pour Linux vous devez télécharger le ndbclient_{version} .


Ensuite, créez un projet. Nous utiliserons SpringBoot , JUnit5 + TestContainers pour les tests.


La structure finale du projet


Le projet se compose de deux modules:


  • clusterj-spring-boot-starter est un démarreur qui contient ClusterJ - ClusterJ , ainsi qu'une atoconfiguration. Grâce à ce démarreur, nous pouvons décrire la connexion à MySQL NDB dans notre fichier appliation.yml comme suit:

 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB 

Après cela, SpringBoot créera pour nous la fabrique SessionFactory nécessaire pour la connexion.


  • clusterj-app est l'application elle-même, que notre démarreur utilisera. Arrêtons-nous dessus plus en détail.

Pour commencer, nous devons créer un modèle de domaine, comme JPA . Seulement dans ce cas, nous devons le faire sous la forme d'une interface, dont l'implémentation sera 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); } 

Il y a un problème tout de suite. L'annotation PersistenceCapable a la possibilité de spécifier le nom du schéma ou de la base de données dans laquelle se trouve la table, mais cela ne fonctionne pas. Absolument. Dans ClusterJ cela n'est pas implémenté. Par conséquent, toutes les tables qui fonctionnent via ClusterJ doivent être dans le même schéma, ce qui entraîne un vidage des tables, qui devraient logiquement être dans des schémas différents.


Essayons maintenant d'utiliser cette interface. Pour ce faire, nous écrivons un test simple.


Afin de ne pas prendre la peine d'installer MySQL Cluster , nous utiliserons la merveilleuse bibliothèque pour tester les tests TestContainers et Docker . Puisque nous utilisons JUnit5, nous allons écrire une Extension simple:


Code source d'extension
 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); } } 

Dans cette extension, nous élevons le nœud de contrôle du cluster, une date pour le nœud et le nœud MySQL . Après cela, nous avons défini les paramètres de connexion appropriés à utiliser par SpringBoot, uniquement ceux que nous avons décrits dans la configuration automatique du démarreur:


 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); 

Ensuite, nous écrivons une annotation qui nous permettra d'augmenter de manière déclarative les conteneurs dans les tests. Tout est très simple ici, nous utilisons notre extension:


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

Enfin, nous écrivons le 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")); } 

Ce test montre comment obtenir l'enregistrement par clé primaire. Cette requête est équivalente à la requête SQL :


 SELECT * FROM user WHERE id = 1; 

Faisons un autre test, avec une logique plus complexe:


 @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 utilisé pour créer des requêtes complexes avec des conditions in , where , equal . Dans ce test, nous retirons tous les utilisateurs dont le nom de famille = Jonson. Cette requête est équivalente au SQL suivant:


 SELECT * FROM user WHERE lastName = 'Jonson'; 

Ici aussi, nous avons rencontré un problème. Impossible de composer une requête du formulaire:


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

Cette fonctionnalité n'est pas actuellement implémentée. Vous pouvez voir le test: andOrNotImplemented .


Exemple de test complet
 @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(); } } 

Grâce à notre annotation @EnableMySQLClusterContainer , nous avons caché les détails de la préparation de l'environnement pour les tests. De plus, grâce à notre démarreur, nous pouvons simplement injecter SessionFactory dans notre test, et l'utiliser pour nos besoins, sans se soucier du fait qu'il doit être créé manuellement.
Tout cela nous concentre sur l'écriture de la logique métier des tests, plutôt que sur l'infrastructure de service.


Je veux également faire attention au fait que vous devez exécuter une application qui utilise ClusterJ avec le paramètre:


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

qui montre le chemin vers libndbclient.so . Sans cela, rien ne fonctionnera.


Conclusion


Quant à moi, ClusterJ bonne chose dans les systèmes qui sont essentiels à la vitesse d'accès aux données, mais des défauts et limitations mineurs gâchent l'impression générale. Si vous avez la possibilité de choisir et que vous ne vous souciez pas de la vitesse d'accès, je pense qu'il est préférable d'utiliser JDBC .


L'article n'a pas envisagé de travailler avec des transactions et des verrous, et il s'est donc avéré beaucoup.


Voilà, Happy Coding!


Liens utiles:


Tout le code avec le projet se trouve ici
Page de téléchargement
Informations sur ClusterJ
Travailler avec Java et NDB Cluster
Livre de cluster Pro MySQL NDB
En savoir plus sur MySQL NDB Cluster ici et ici


Encore plus de cas de test dans le référentiel MySQL lui-même.

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


All Articles