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
.

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