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
.

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