ClusterJ - working with MySQL NDB Cluster from Java

Hello, Habr! In this article I want to consider a library for Java such as ClusterJ



, which makes it very easy to work with the MySQL NDBCLUSTER



engine from Java



code, which is a high-level API similar in concept to JPA



and Hibernate



.







In the framework of this article, we will create a simple application on SpringBoot



, and also make a starter with ClusterJ



on board for convenient use in applications using autoconfiguration. We will write simple tests using JUnit5



and TestContainers



, which will show the basic use of the API.

I will also talk about several shortcomings that I had to face in the process of working with her.







Who cares, welcome to cat.







Introduction



MySQL NDB Cluster



actively used at work, and in one of the projects, for the sake of speed, the task was to use the ClusterJ



library instead of the usual JDBC



, which in its API is very similar to JPA



, and, in fact, it is a wrapper over the libndbclient.so



library that it uses through JNI



.







For those who are not in the know, MySQL NDB Cluster is a highly accessible and redundant MySQL version adapted for a distributed computing environment that uses the NDB



storage NDB



( NDBCLUSTER



) to operate in a cluster. I donโ€™t want to dwell on this here in detail, you can read more here and here

There are two ways to work from Java code with this database:









image







ClusterJ is built around 4 key concepts:









ClusterJ limitations:









Practice. Talk is cheap. Show me the code.



Well, enough theory, let's move on to practice.







The first problem to be faced is the lack of ClusterJ



in the central Maven repository. Install the library with pens in the local repository. It is clear that for good, it should lie in Nexus



or some Artifactory



, but for our example this is unnecessary.







So, go here and choose your operating system. If you are on a Linux



like OS, download the package called mysql-cluster-community-java



and install the given rpm / deb package. If you have Windows



, download the full mysql-cluster-gp



archive.







One way or another, we will have a jar file of the form: clusterj-{version}.jar



. We put it through maven



:







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





We also need the libndbclient



library, which is a set of C++



functions for working with the NDB API



that ClusterJ



calls through the JNI



. For Windows



this library (.dll) is in the mysql-cluster-gp



archive; for Linux



you need to download the ndbclient_{version}



package.







Next, create a project. We will use SpringBoot



, JUnit5



+ TestContainers



for tests.







The final structure of the project







The project consists of two modules:









 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB
      
      





After that, SpringBoot



will create for us the necessary SessionFactory



factory for the connection.









To get started, we need to create a domain model, like JPA



. Only in this case we need to do this in the form of an interface, the implementation of which in clusterj



will be 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); }
      
      





There is a problem right away. The PersistenceCapable



annotation has the ability to specify the name of the schema or database in which the table lies, however this does not work. Absolutely. In ClusterJ



this is not implemented. Therefore, all the tables that are working through ClusterJ



should be in the same schema, which results in a dump of tables that logically should be in different schemas.







Now let's try to use this interface. To do this, we write a simple test.







In order not to bother with installing MySQL Cluster



, we will use the wonderful library for integration testing TestContainers and Docker . Since we are using JUnit5 we will write a simple Extension



:







Extension source code
 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 this Extension, we raise the control node of the cluster, one date for the node, and the MySQL



node. After that, we set the appropriate connection settings for use by SpringBoot, just those that we described in the starter auto-configuration:







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





Next, we write an annotation that will allow us to declaratively raise containers in tests. Everything is very simple here, we use our Extension:







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





Finally, we write the 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")); }
      
      





This test shows how we can get the record by primary key. This query is equivalent to SQL



query:







 SELECT * FROM user WHERE id = 1;
      
      





Let's do another test, with more complex logic:







 @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



used to build complex queries with in



, where



, equal



, like



QueryBuilder



. In this test, we pull out all users whose last name = Jonson. This query is equivalent to the following SQL



:







 SELECT * FROM user WHERE lastName = 'Jonson';
      
      





Here, too, ran into a problem. Unable to compile a query of the form







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





This feature is not currently implemented. You can see the test: andOrNotImplemented



.







Full test example
 @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(); } }
      
      





Thanks to our annotation @EnableMySQLClusterContainer



, we hid the details of preparing the environment for tests. Also, thanks to our starter, we can simply inject SessionFactory into our test, and use it for our needs, without worrying about the fact that it needs to be created manually.

All this concentrates us on writing the business logic of the tests, rather than the serving infrastructure.







I also want to pay attention to the fact that you need to run an application that uses ClusterJ



with the parameter:







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





which shows the path to libndbclient.so



. Without it, nothing will work.







Conclusion



As for me, ClusterJ



good thing in those systems that are critical to data access speed, but minor flaws and limitations spoil the overall impression. If you have the opportunity to choose and you do not care about the speed of access, I think it is better to use JDBC



.







The article did not consider working with transactions and locks, and so it turned out quite a lot.







That's it, Happy Coding!







Useful links:



All code with the project lies here

Download Page

Information about ClusterJ

Work with Java and NDB Cluster

Pro MySQL NDB Cluster Book

More about MySQL NDB Cluster here and here







More test examples in MySQL



repository itself








All Articles