ClusterJ-从Java使用MySQL NDB Cluster

哈Ha! 在本文中,我想考虑一个Java库,例如ClusterJ ,它使使用Java代码的MySQL NDBCLUSTER引擎变得非常容易,该引擎的概念类似于JPAHibernate ,是高级API。


在本文的框架中,我们将在SpringBoot上创建一个简单的应用程序,并使用ClusterJ创建一个入门ClusterJ ,以便在使用自动配置的应用程序中方便使用。 我们将使用JUnit5TestContainers编写简单的测试,这些测试将显示API的基本用法。
我还将讨论在与她合作的过程中我必须面对的几个缺点。


谁在乎,欢迎来猫。


引言


为了提高速度, MySQL NDB Cluster在工作中和一个项目中MySQL NDB Cluster积极使用,其任务是使用ClusterJ库而不是通常的JDBC ,后者的API与JPA非常相似,并且实际上是libndbclient.so库的包装器,它是通过JNI使用的。


对于那些不了解的人,MySQL NDB Cluster是一个高度可访问且冗余的MySQL版本,适用于使用NDB存储NDBNDBCLUSTER )在群集中运行的分布式计算环境。 我不想在这里详细介绍,您可以在这里这里阅读更多内容

使用该数据库的Java代码有两种方法:


  • 标准,通过JDBCSQL查询
  • 通过ClusterJ ,用于MySQL Cluster中的高性能数据访问。

图片


ClusterJ基于4个关键概念构建:


  • SessionFactory连接池的类似物,用于获取会话。 群集的每个实例都必须具有自己的SessionFactory。
  • Session -是与MySQL群集的直接连接。
  • Domain Object -带注释的接口,表示表到Java代码的映射,类似于JPA
  • Transaction -是工作的原子单元。 在任何给定时间,在一个会话中,将执行一个事务。 任何操作(接收,插入,更新,删除)都在新事务中执行。

ClusterJ的局限性:


  • 缺乏加盟
  • 无法创建表和索引。 为此,请使用JDBC
  • 没有延迟加载( Lazy )。 整个记录一次下载。
  • 在域对象中,无法定义表之间的关系。 完全没有OneToManyManyToOneManyToMany的相似性。

练习 谈话很便宜。 给我看代码。


好了,理论足够多,让我们继续练习。


面临的第一个问题是中央Maven存储库中缺少ClusterJ 。 用笔将库安装在本地存储库中。 很明显,它应该位于Nexus或某些Artifactory ,但对于我们的示例而言,这是不必要的。


因此,请转到此处并选择您的操作系统。 如果您使用的是类似Linux操作系统,请下载名为mysql-cluster-community-java软件包并安装此rpm / deb软件包。 如果您使用Windows ,请下载完整的mysql-cluster-gp存档。


一种或另一种方式,我们将具有以下形式的jar文件: clusterj-{version}.jar 。 我们把它通过了maven


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

我们还需要libndbclient库,它是一组C++函数,用于处理ClusterJ通过JNI调用的NDB API 。 对于Windows此库(.dll)位于mysql-cluster-gp归档文件中,对于Linux您需要下载ndbclient_{version}软件包。


接下来,创建一个项目。 我们将使用SpringBootJUnit5 + TestContainers进行测试。


项目的最终结构


该项目包含两个模块:


  • clusterj-spring-boot-starter是一个包含ClusterJ以及atoconfiguration的启动器。 感谢这个入门者,我们可以在appliation.yml文件中描述与MySQL NDB的连接,如下所示:

 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB 

之后, SpringBoot将为我们创建连接所需的SessionFactory工厂。


  • clusterj-app是应用程序本身,我们的入门人员将使用它。 让我们更详细地讨论它。

首先,我们需要创建一个域模型,例如JPA 。 仅在这种情况下,我们需要以接口的形式执行此操作,在clusterj将由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); } 

马上有问题。 PersistenceCapable批注可以指定表所在的模式或数据库的名称,但这不起作用。 绝对是 在ClusterJ这没有实现。 因此,所有通过ClusterJ工作的表都应在同一模式中,这将导致表转储,从逻辑ClusterJ ,表应在不同的模式中。


现在,让我们尝试使用此界面。 为此,我们编写了一个简单的测试。


为了不麻烦安装MySQL Cluster ,我们将使用出色的库对TestContainersDocker进行集成测试。 由于我们使用的是JUnit5,我们将编写一个简单的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); } } 

在此扩展中,我们提出了群集的控制节点,该节点的一个日期以及MySQL节点。 之后,我们设置适当的连接设置以供SpringBoot使用,就像我们在启动程序自动配置中描述的那样:


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

接下来,我们编写一个注释,使我们可以在测试中声明性地引发容器。 这里的一切都很简单,我们使用扩展程序:


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

最后,我们编写测试:


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

此测试显示了如何通过主键获取记录。 此查询等效于SQL查询:


 SELECT * FROM user WHERE id = 1; 

让我们用更复杂的逻辑做另一个测试:


 @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用于以inwhereequal QueryBuilderlike QueryBuilder构建复杂的查询。 在此测试中,我们提取了姓氏= Jonson的所有用户。 此查询等效于以下SQL


 SELECT * FROM user WHERE lastName = 'Jonson'; 

这里也遇到了问题。 无法组成以下形式的查询:


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

当前未实现此功能。 您可以看到测试: andOrNotImplemented


完整的测试示例
 @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(); } } 

感谢我们的注释@EnableMySQLClusterContainer ,我们隐藏了准备测试环境的详细信息。 同样,由于我们的入门者,我们可以简单地将SessionFactory注入我们的测试中,并将其用于我们的需求,而不必担心需要手动创建它的事实。
所有这些使我们专注于编写测试的业务逻辑,而不是服务基础结构。


我还想注意一个事实,您需要运行一个将ClusterJ与参数一起使用的应用程序:


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

其中显示了libndbclient.so的路径。 没有它,什么都不会起作用。


结论


对于我来说,在那些对数据访问速度至关重要的系统中, ClusterJ一件好事,但细微的缺陷和限制破坏了整体印象。 如果您有机会选择并且不关心访问速度,那么我认为使用JDBC更好。


本文没有考虑使用事务和锁,因此结果很多。


就是这样,快乐编码!


有用的链接:


该项目的所有代码都位于此处
下载页面
有关ClusterJ的信息
使用Java和NDB集群
Pro MySQL NDB丛书
在此处此处了解有关MySQL NDB群集的更多信息


MySQL 存储库本身中还有更多测试用例。

Source: https://habr.com/ru/post/zh-CN472468/


All Articles