ClusterJ - bekerja dengan MySQL NDB Cluster dari Java

Halo, Habr! Pada artikel ini saya ingin mempertimbangkan perpustakaan untuk Java seperti ClusterJ , yang membuatnya sangat mudah untuk bekerja dengan mesin MySQL NDBCLUSTER dari kode Java , yang merupakan API tingkat tinggi yang serupa dalam konsep JPA dan Hibernate .


Dalam kerangka kerja artikel ini, kami akan membuat aplikasi sederhana di SpringBoot , dan juga membuat starter dengan ClusterJ on board untuk penggunaan yang mudah dalam aplikasi menggunakan konfigurasi otomatis. Kami akan menulis tes sederhana menggunakan JUnit5 dan TestContainers , yang akan menunjukkan penggunaan dasar API.
Saya juga akan berbicara tentang beberapa kekurangan yang harus saya hadapi dalam proses bekerja dengannya.


Siapa peduli, selamat datang ke kucing.


Pendahuluan


MySQL NDB Cluster aktif digunakan di tempat kerja dan di salah satu proyek, demi kecepatan, tugasnya adalah menggunakan pustaka ClusterJ alih-alih JDBC biasa, yang oleh API-nya sangat mirip dengan JPA , dan pada kenyataannya, adalah pembungkus perpustakaan libndbclient.so , yang digunakan melalui JNI .


Bagi mereka yang tidak tahu, MySQL NDB Cluster adalah versi MySQL yang sangat mudah diakses dan redundan yang diadaptasi untuk lingkungan komputasi terdistribusi yang menggunakan NDB penyimpanan NDB ( NDBCLUSTER ) untuk bekerja dalam sebuah cluster. Saya tidak ingin membahas hal ini secara terperinci, Anda dapat membaca lebih lanjut di sini dan di sini

Ada dua cara untuk bekerja dari kode Java dengan database ini:


  • Standar, melalui permintaan JDBC dan SQL
  • Via ClusterJ , untuk akses data berkinerja tinggi di MySQL Cluster .

gambar


ClusterJ dibangun di sekitar 4 konsep utama:


  • SessionFactory - analog dari kumpulan koneksi, yang digunakan untuk mendapatkan sesi Setiap instance dari cluster harus memiliki SessionFactory sendiri.
  • Session - adalah koneksi langsung ke cluster MySQL .
  • Domain Object - Antarmuka beranotasi yang mewakili pemetaan tabel ke dalam kode Java , mirip dengan JPA .
  • Transaction - adalah unit kerja atom. Pada waktu tertentu, dalam satu sesi, satu transaksi dijalankan. Operasi apa pun (menerima, menyisipkan, memperbarui, menghapus) dilakukan dalam transaksi baru.

Batasan ClusterJ:


  • Kurangnya GABUNG
  • Tidak ada cara untuk membuat tabel dan indeks. Untuk melakukan ini, gunakan JDBC .
  • Tidak ada pemuatan yang tertunda ( Lazy ). Seluruh catatan diunduh sekaligus.
  • Dalam objek domain, tidak mungkin untuk menentukan hubungan antar tabel. Kesamaan OneToMany , ManyToOne , ManyToMany sekali tidak ada.

Berlatih. Bicara itu murah. Tunjukkan kodenya.


Cukup teori, mari kita lanjutkan ke latihan.


Masalah pertama yang harus dihadapi adalah kurangnya ClusterJ dalam repositori Maven pusat. Instal perpustakaan dengan pena di repositori lokal. Jelas bahwa untuk selamanya, itu harus terletak pada Nexus atau Artifactory , tetapi untuk contoh kita ini tidak perlu.


Jadi, buka di sini dan pilih sistem operasi Anda. Jika Anda menggunakan OS mirip Linux , unduh paket yang disebut mysql-cluster-community-java dan instal paket rpm / deb ini. Jika Anda memiliki Windows , unduh arsip mysql-cluster-gp .


Dengan satu atau lain cara, kita akan memiliki file jar dalam bentuk: clusterj-{version}.jar . Kami menempatkannya melalui maven :


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

Kita juga membutuhkan libndbclient library, yang merupakan sekumpulan fungsi C++ untuk bekerja dengan NDB API yang ClusterJ melalui JNI . Untuk Windows perpustakaan ini (.dll) ada di arsip mysql-cluster-gp , untuk Linux Anda harus mengunduh paket ndbclient_{version} .


Selanjutnya, buat proyek. Kami akan menggunakan SpringBoot , JUnit5 + TestContainers untuk pengujian.


Struktur akhir proyek


Proyek ini terdiri dari dua modul:


  • clusterj-spring-boot-starter adalah starter yang berisi ClusterJ , serta konfigurasi-atok. Berkat starter ini, kami dapat menggambarkan koneksi ke MySQL NDB di file appliation.yml kami sebagai berikut:

 clusterj: connectString: localhost:1186 dataBaseName: NDB_DB 

Setelah itu, SpringBoot akan menciptakan bagi kita pabrik SessionFactory diperlukan untuk koneksi.


  • clusterj-app adalah aplikasi itu sendiri, yang akan digunakan pemula kami. Mari kita bahas lebih detail.

Untuk memulai, kita perlu membuat model domain, seperti JPA . Hanya dalam hal ini kita perlu melakukan ini dalam bentuk antarmuka, implementasi yang dalam clusterj akan 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); } 

Ada masalah saat itu juga. Anotasi PersistenceCapable memiliki kemampuan untuk menentukan nama skema atau database di mana tabel berada, namun ini tidak berfungsi. Tentu saja Di ClusterJ ini tidak diterapkan. Oleh karena itu, semua tabel yang bekerja melalui ClusterJ harus dalam skema yang sama, yang menghasilkan ClusterJ tabel yang secara logis harus di skema yang berbeda.


Sekarang mari kita coba gunakan antarmuka ini. Untuk melakukan ini, kami menulis tes sederhana.


Agar tidak repot-repot memasang MySQL Cluster , kami akan menggunakan pustaka yang luar biasa untuk pengujian integrasi TestContainers dan Docker . Karena kita menggunakan JUnit5 kita akan menulis Extension sederhana:


Kode sumber ekstensi
 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); } } 

Dalam ekstensi ini, kami menaikkan node kontrol dari cluster, satu tanggal untuk node, dan node MySQL . Setelah itu, kami menetapkan pengaturan koneksi yang sesuai untuk digunakan oleh SpringBoot, hanya yang kami jelaskan di konfigurasi otomatis starter:


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

Selanjutnya, kami menulis anotasi yang akan memungkinkan kami untuk secara deklaratif meningkatkan wadah dalam pengujian. Semuanya sangat sederhana di sini, kami menggunakan Ekstensi kami:


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

Akhirnya, kami menulis tes:


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

Tes ini menunjukkan bagaimana kita bisa mendapatkan catatan dengan kunci utama. Kueri ini setara dengan kueri SQL :


 SELECT * FROM user WHERE id = 1; 

Mari kita lakukan tes lain, dengan logika yang lebih kompleks:


 @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 digunakan untuk membangun kueri kompleks dengan kondisi, in where , equal , like . Dalam tes ini, kami menarik semua pengguna yang nama belakangnya = Jonson. Kueri ini setara dengan SQL berikut:


 SELECT * FROM user WHERE lastName = 'Jonson'; 

Di sini, juga mengalami masalah. Tidak dapat membuat kueri formulir:


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

Fitur ini saat ini tidak diterapkan. Anda dapat melihat tes: andOrNotImplemented .


Contoh tes lengkap
 @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(); } } 

Berkat penjelasan kami @EnableMySQLClusterContainer , kami menyembunyikan detail persiapan lingkungan untuk pengujian. Juga, terima kasih kepada pemula kami, kami dapat dengan mudah menyuntikkan SessionFactory ke dalam pengujian kami, dan menggunakannya untuk kebutuhan kami, tanpa khawatir tentang fakta bahwa itu perlu dibuat secara manual.
Semua ini memusatkan kami pada penulisan logika bisnis dari pengujian, bukan infrastruktur penyajian.


Saya juga ingin memperhatikan fakta bahwa Anda perlu menjalankan aplikasi yang menggunakan ClusterJ dengan parameter:


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

yang menunjukkan jalur ke libndbclient.so . Tanpanya, tidak akan ada yang berhasil.


Kesimpulan


Bagi saya, ClusterJ hal yang baik dalam sistem yang sangat penting untuk kecepatan akses data, tetapi kelemahan dan keterbatasan kecil merusak kesan keseluruhan. Jika Anda memiliki kesempatan untuk memilih dan Anda tidak peduli dengan kecepatan akses, saya pikir lebih baik menggunakan JDBC .


Artikel itu tidak mempertimbangkan bekerja dengan transaksi dan kunci, dan ternyata cukup banyak.


Itu dia, Selamat Coding!


Tautan yang bermanfaat:


Semua kode dengan proyek ada di sini
Halaman Unduh
Informasi tentang ClusterJ
Bekerja dengan Cluster Java dan NDB
Buku MySQL NDB Cluster Pro
Lebih lanjut tentang MySQL NDB Cluster di sini dan di sini


Lebih banyak contoh uji dalam repositori MySQL itu sendiri

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


All Articles