Olá Habr! Neste artigo, quero considerar uma biblioteca para Java como o ClusterJ
, que facilita muito o trabalho com o mecanismo MySQL NDBCLUSTER
a partir do código Java
, que é uma API de alto nível semelhante em conceito ao JPA
e Hibernate
.
Na estrutura deste artigo, criaremos um aplicativo simples no SpringBoot
e também ClusterJ
a bordo para uso conveniente em aplicativos usando a configuração automática. Escreveremos testes simples usando JUnit5
e TestContainers
, que mostrarão o uso básico da API.
Também falarei sobre várias deficiências que tive que enfrentar no processo de trabalhar com ela.
Quem se importa, bem-vindo ao gato.
1. Introdução
MySQL NDB Cluster
usado ativamente no trabalho e, em um dos projetos, por uma questão de velocidade, a tarefa era usar a biblioteca ClusterJ
vez do JDBC
usual, que em sua API é muito semelhante ao JPA
e, de fato, é um invólucro da biblioteca libndbclient.so
que ele usa na JNI
.
Para quem não conhece, o MySQL NDB Cluster é uma versão MySQL altamente acessível e redundante, adaptada para um ambiente de computação distribuído que usa o NDB
storage NDB
( NDBCLUSTER
) para operar em um cluster. Não quero me debruçar sobre isso aqui em detalhes, você pode ler mais aqui e aqui
Há duas maneiras de trabalhar com código Java com este banco de dados:
- Padrão, por meio de consultas
JDBC
e SQL
- Via
ClusterJ
, para acesso a dados de alto desempenho no MySQL Cluster
.

O ClusterJ é construído em torno de 4 conceitos principais:
SessionFactory
- um análogo do pool de conexões, usado para obter uma sessão. Cada instância do cluster deve ter seu próprio SessionFactory.Session
- é uma conexão direta com um cluster MySQL
.Domain Object
- uma interface anotada representando um mapeamento de uma tabela para o código Java
, semelhante ao JPA
.Transaction
- é uma unidade atômica de trabalho. A qualquer momento, em uma sessão, uma transação é executada. Qualquer operação (receber, inserir, atualizar, excluir) é executada em uma nova transação.
Limitações do ClusterJ:
- Falta de JOINs
- Não há como criar uma tabela e índices. Para fazer isso, use
JDBC
. - Sem carregamento atrasado (
Lazy
). O registro inteiro é baixado de uma vez. - Em objetos de domínio, não é possível definir relacionamentos entre tabelas. A semelhança de
OneToMany
, ManyToOne
, ManyToMany
completamente ausente.
Prática. Falar é barato. Mostre-me o código.
Bem, teoria suficiente, vamos seguir praticando.
O primeiro problema a ser enfrentado é a falta de ClusterJ
no repositório central do Maven. Instale a biblioteca com canetas no repositório local. É claro que, para sempre, ele deve estar no Nexus
ou em algum Artifactory
, mas, para o nosso exemplo, isso é desnecessário.
Então, vá aqui e escolha seu sistema operacional. Se você estiver em um sistema operacional Linux
, faça o download do pacote chamado mysql-cluster-community-java
e instale este pacote rpm / deb. Se você possui o Windows
, baixe o arquivo mysql-cluster-gp
completo.
De uma forma ou de outra, teremos um arquivo jar do formato: clusterj-{version}.jar
. Colocamos no maven
:
mvn install:install-file -DgroupId=com.mysql.ndb -DartifactId=clusterj -Dversion={version} -Dpackaging=jar -Dfile=clusterj-{version}.jar -DgeneratePom=true
Também precisamos da biblioteca libndbclient
, que é um conjunto de funções C++
para trabalhar com a NDB API
que ClusterJ
chama por meio da JNI
. Para Windows
esta biblioteca (.dll) está no arquivo mysql-cluster-gp
, para Linux
você precisa fazer o download do ndbclient_{version}
.
Em seguida, crie um projeto. Usaremos SpringBoot
, JUnit5
+ TestContainers
para testes.
A estrutura final do projeto O projeto consiste em dois módulos:
clusterj-spring-boot-starter
é um iniciador que contém o ClusterJ
, bem como a configuração. Graças a esta iniciação, podemos descrever a conexão com o MySQL NDB
em nosso arquivo appliation.yml
seguinte maneira:
clusterj: connectString: localhost:1186 dataBaseName: NDB_DB
Depois disso, o SpringBoot
criará para nós a fábrica SessionFactory
necessária para a conexão.
clusterj-app
é o próprio aplicativo, que nosso iniciador usará. Vamos insistir nisso com mais detalhes.
Para começar, precisamos criar um modelo de domínio, como o JPA
. Somente neste caso, precisamos fazer isso na forma de uma interface, cuja implementação em 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); }
Há um problema imediatamente. A anotação PersistenceCapable
tem a capacidade de especificar o nome do esquema ou banco de dados no qual a tabela está, mas isso não funciona. Absolutamente. No ClusterJ
isso não é implementado. Portanto, todas as tabelas que estão trabalhando no ClusterJ
devem estar no mesmo esquema, o que resulta em um despejo de tabelas, que logicamente deve estar em esquemas diferentes.
Agora vamos tentar usar essa interface. Para fazer isso, escrevemos um teste simples.
Para não se preocupar com a instalação do MySQL Cluster
, usaremos a maravilhosa biblioteca para testar os testes de integração TestContainers e Docker . Como estamos usando o JUnit5 , escreveremos uma Extension
simples:
Código fonte da extensão 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); } }
Nesta extensão, aumentamos o nó de controle do cluster, uma data para o nó e o nó MySQL
. Depois disso, definimos as configurações de conexão apropriadas para uso pelo SpringBoot, apenas aquelas que descrevemos na configuração automática inicial:
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);
Em seguida, escrevemos uma anotação que nos permitirá aumentar declarativamente os contêineres nos testes. Tudo é muito simples aqui, usamos nossa Extensão:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @ExtendWith(MySQLClusterTcExtension.class) public @interface EnableMySQLClusterContainer { }
Por fim, escrevemos o teste:
@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")); }
Este teste mostra como podemos obter o registro pela chave primária. Esta consulta é equivalente à consulta SQL
:
SELECT * FROM user WHERE id = 1;
Vamos fazer outro teste, com lógica mais complexa:
@Test void queryBuilderTest() { QueryBuilder builder = session.getQueryBuilder(); QueryDomainType<User> userQueryDomainType = builder.createQueryDefinition(User.class);
QueryBuilder
usado para criar consultas complexas com condições in
, where
, equal
e like
. Neste teste, selecionamos todos os usuários cujo sobrenome = Jonson. Esta consulta é equivalente ao seguinte SQL
:
SELECT * FROM user WHERE lastName = 'Jonson';
Aqui também houve um problema. Não foi possível compor uma consulta no formulário:
SELECT * FROM user WHERE (lastName = 'Jonson' and firstName = 'John') or id = 2;
Este recurso não está implementado no momento. Você pode ver o teste: andOrNotImplemented
.
Exemplo de teste completo @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);
Graças à nossa anotação @EnableMySQLClusterContainer
, @EnableMySQLClusterContainer
os detalhes da preparação do ambiente para testes. Além disso, graças ao nosso iniciante, podemos simplesmente injetar o SessionFactory em nosso teste e usá-lo para nossas necessidades, sem nos preocupar com o fato de que ele precisa ser criado manualmente.
Tudo isso nos concentra em escrever a lógica de negócios dos testes, em vez da infraestrutura de atendimento.
Também quero prestar atenção ao fato de que você precisa executar um aplicativo que use ClusterJ
com o parâmetro:
-Djava.library.path=/usr/lib/x86_64-linux-gnu/
que mostra o caminho para libndbclient.so
. Sem ele, nada vai funcionar.
Conclusão
Quanto a mim, o ClusterJ
bom nos sistemas críticos para a velocidade de acesso aos dados, mas pequenas falhas e limitações estragam a impressão geral. Se você tem a oportunidade de escolher e não se importa com a velocidade do acesso, acho melhor usar o JDBC
.
O artigo não considerou trabalhar com transações e bloqueios e, por isso, resultou bastante.
É isso aí, Happy Coding!
Links úteis:
Todo o código do projeto está aqui
Página de Download
Informações sobre o ClusterJ
Trabalhar com Java e NDB Cluster
Livro de Cluster Pro MySQL NDB
Mais sobre o MySQL NDB Cluster aqui e aqui
Ainda mais casos de teste no próprio repositório MySQL
.