ClusterJ - trabajando con MySQL NDB Cluster de Java

Hola Habr! En este art铆culo quiero considerar una biblioteca para Java como ClusterJ , que hace que sea muy f谩cil trabajar con el motor MySQL NDBCLUSTER partir de c贸digo Java , que es una API de alto nivel similar en concepto a JPA e Hibernate .


En el marco de este art铆culo, crearemos una aplicaci贸n simple en SpringBoot , y tambi茅n haremos un iniciador con ClusterJ a bordo para un uso conveniente en aplicaciones que utilizan la autoconfiguraci贸n. Escribiremos pruebas simples usando JUnit5 y TestContainers , que mostrar谩n el uso b谩sico de la API.
Tambi茅n hablar茅 sobre varias deficiencias que tuve que enfrentar en el proceso de trabajar con ella.


A qui茅n le importa, bienvenido al gato.


Introduccion


MySQL NDB Cluster usa activamente en el trabajo y en uno de los proyectos, por razones de velocidad, la tarea era usar la biblioteca ClusterJ lugar del JDBC habitual, que por su API es muy similar a JPA , y de hecho, es un contenedor sobre la biblioteca libndbclient.so , que usa a trav茅s de JNI .


Para aquellos que no est谩n al tanto, MySQL NDB Cluster es una versi贸n MySQL altamente accesible y redundante adaptada para un entorno inform谩tico distribuido que utiliza el NDB almacenamiento NDB ( NDBCLUSTER ) para operar en un cl煤ster. No quiero detenerme en esto aqu铆 en detalle, puedes leer m谩s aqu铆 y aqu铆

Hay dos formas de trabajar desde el c贸digo Java con esta base de datos:


  • Est谩ndar, a trav茅s de consultas JDBC y SQL
  • V铆a ClusterJ , para acceso a datos de alto rendimiento en la MySQL Cluster .

imagen


ClusterJ se basa en 4 conceptos clave:


  • SessionFactory : un an谩logo del grupo de conexiones, utilizado para obtener una sesi贸n. Cada instancia del cl煤ster debe tener su propia SessionFactory.
  • Session : es una conexi贸n directa a un cl煤ster MySQL .
  • Domain Object : una interfaz anotada que representa un mapeo de una tabla en c贸digo Java , similar a JPA .
  • Transaction : es una unidad at贸mica de trabajo. En cualquier momento, en una sesi贸n, se ejecuta una transacci贸n. Cualquier operaci贸n (recibir, insertar, actualizar, eliminar) se realiza en una nueva transacci贸n.

Limitaciones de ClusterJ:


  • Falta de uniones
  • No hay forma de crear una tabla e 铆ndices. Para hacer esto, use JDBC .
  • Sin carga demorada ( Lazy ). Todo el registro se descarga al mismo tiempo.
  • En objetos de dominio, no es posible definir relaciones entre tablas. La similitud de OneToMany , ManyToOne , ManyToMany completamente ausente.

Practica Hablar es barato. Mu茅strame el c贸digo.


Bueno, suficiente teor铆a, pasemos a practicar.


El primer problema a enfrentar es la falta de ClusterJ en el repositorio central de Maven. Instale la biblioteca con l谩pices en el repositorio local. Est谩 claro que para bien, deber铆a estar en Nexus o en alg煤n Artifactory , pero para nuestro ejemplo esto es innecesario.


Entonces, vaya aqu铆 y elija su sistema operativo. Si est谩 en un Linux operativo similar a Linux, descargue el paquete llamado mysql-cluster-community-java e instale este paquete rpm / deb. Si tiene Windows , descargue el archivo completo mysql-cluster-gp .


De una forma u otra, tendremos un archivo jar de la forma: clusterj-{version}.jar . Lo pasamos por maven :


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

Tambi茅n necesitamos la biblioteca libndbclient , que es un conjunto de funciones de C++ para trabajar con la NDB API que ClusterJ llama a trav茅s de JNI . Para Windows esta biblioteca (.dll) est谩 en el archivo mysql-cluster-gp , para Linux necesita descargar el ndbclient_{version} .


Luego, crea un proyecto. Utilizaremos SpringBoot , JUnit5 + TestContainers para las pruebas.


La estructura final del proyecto.


El proyecto consta de dos m贸dulos:


  • clusterj-spring-boot-starter es un iniciador que contiene el ClusterJ , as铆 como tambi茅n una configuraci贸n. Gracias a este iniciador, podemos describir la conexi贸n a MySQL NDB en nuestro archivo appliation.yml la siguiente manera:

 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB 

Despu茅s de eso, SpringBoot crear谩 para nosotros la f谩brica de SessionFactory necesaria para la conexi贸n.


  • clusterj-app es la aplicaci贸n en s铆, que utilizar谩 nuestro iniciador. Deteng谩monos en ello con m谩s detalle.

Para comenzar, necesitamos crear un modelo de dominio, como JPA . Solo en este caso necesitamos hacer esto en forma de una interfaz, cuya implementaci贸n en clusterj de clusterj ser谩 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); } 

Hay un problema de inmediato. La anotaci贸n PersistenceCapable tiene la capacidad de especificar el nombre del esquema o la base de datos en la que se encuentra la tabla, sin embargo, esto no funciona. Absolutamente En ClusterJ esto no est谩 implementado. Por lo tanto, todas las tablas que funcionan a trav茅s de ClusterJ deben estar en el mismo esquema, lo que resulta en un volcado de tablas que l贸gicamente deber铆an estar en esquemas diferentes.


Ahora intentemos usar esta interfaz. Para hacer esto, escribimos una prueba simple.


Para no molestarnos con la instalaci贸n de MySQL Cluster , utilizaremos la maravillosa biblioteca para pruebas de integraci贸n TestContainers y Docker . Como estamos usando JUnit5 , escribiremos una Extension simple:


C贸digo fuente de extensi贸n
 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); } } 

En esta extensi贸n, elevamos el nodo de control del cl煤ster, una fecha para el nodo y el nodo MySQL . Despu茅s de eso, establecemos las configuraciones de conexi贸n apropiadas para que SpringBoot las use, solo las que describimos en la configuraci贸n autom谩tica de arranque:


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

Luego, escribimos una anotaci贸n que nos permitir谩 levantar contenedores declarativamente en las pruebas. Aqu铆 todo es muy simple, usamos nuestra extensi贸n:


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

Finalmente, escribimos la prueba:


 @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")); } 

Esta prueba muestra c贸mo podemos obtener el registro por clave primaria. Esta consulta es equivalente a la consulta SQL :


 SELECT * FROM user WHERE id = 1; 

Hagamos otra prueba, con una l贸gica m谩s compleja:


 @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 utiliza para crear consultas complejas con condiciones in , where , equal , like . En esta prueba, sacamos a todos los usuarios cuyo apellido = Jonson. Esta consulta es equivalente al siguiente SQL :


 SELECT * FROM user WHERE lastName = 'Jonson'; 

Aqu铆, tambi茅n, se encontr贸 con un problema. No se puede componer una consulta del formulario:


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

Esta caracter铆stica no est谩 implementada actualmente. Puede ver la prueba: andOrNotImplemented .


Ejemplo de prueba completa
 @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(); } } 

Gracias a nuestra anotaci贸n @EnableMySQLClusterContainer , @EnableMySQLClusterContainer los detalles de la preparaci贸n del entorno para las pruebas. Adem谩s, gracias a nuestro iniciador, simplemente podemos inyectar SessionFactory en nuestra prueba y usarlo para nuestras necesidades, sin preocuparnos por el hecho de que debe crearse manualmente.
Todo esto nos concentra en escribir la l贸gica empresarial de las pruebas, en lugar de la infraestructura de servicio.


Tambi茅n quiero prestar atenci贸n al hecho de que necesita ejecutar una aplicaci贸n que use ClusterJ con el par谩metro:


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

que muestra la ruta a libndbclient.so . Sin eso, nada funcionar谩.


Conclusi贸n


En cuanto a m铆, ClusterJ bueno en aquellos sistemas que son cr铆ticos para la velocidad de acceso a datos, pero fallas y limitaciones menores estropean la impresi贸n general. Si tiene la oportunidad de elegir y no le importa la velocidad de acceso, creo que es mejor usar JDBC .


El art铆culo no consider贸 trabajar con transacciones y bloqueos, por lo que result贸 bastante.


隆Eso es, feliz codificaci贸n!


Enlaces utiles:


Todo el c贸digo con el proyecto se encuentra aqu铆.
Descargar p谩gina
Informaci贸n sobre ClusterJ
Trabajar con Java y NDB Cluster
Pro MySQL NDB Cluster Book
M谩s informaci贸n sobre MySQL NDB Cluster aqu铆 y aqu铆


Incluso m谩s casos de prueba en el repositorio MySQL .

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


All Articles