مرحبا يا هبر! في هذه المقالة ، أود أن أعتبر مكتبة لـ Java مثل ClusterJ
، مما يجعل من السهل جدًا العمل مع محرك MySQL NDBCLUSTER
من كود Java
، وهو واجهة برمجة تطبيقات عالية المستوى تشبه في مفهومها JPA
و Hibernate
.
في إطار هذه المقالة ، ClusterJ
على ClusterJ
أيضًا بداية مع ClusterJ
على متن الطائرة للاستخدام المريح في التطبيقات التي تستخدم التكوين التلقائي. JUnit5
اختبارات بسيطة باستخدام JUnit5
و TestContainers
، والتي ستظهر الاستخدام الأساسي لواجهة برمجة التطبيقات.
سأتحدث أيضًا عن العديد من أوجه القصور التي تعين علي مواجهتها أثناء العمل معها.
من يهتم ، مرحبا بكم في القط.
مقدمة
يستخدم MySQL NDB Cluster
بنشاط في العمل وفي أحد المشاريع ، من أجل السرعة ، كانت المهمة هي استخدام مكتبة ClusterJ
بدلاً من JDBC
المعتادة ، والتي تشبه واجهة برمجة التطبيقات (API) الخاصة بها إلى JPA
، وفي الواقع ، فهي عبارة عن غلاف على مكتبة libndbclient.so
، والتي تستخدمها من خلال JNI
.
بالنسبة لأولئك الذين ليسوا على دراية ، يعد MySQL NDB Cluster إصدار MySQL يمكن الوصول إليه بشكل كبير ومكيف لبيئة حوسبة موزعة تستخدم NDB
تخزين NDB
( NDBCLUSTER
) للعمل في كتلة. لا أريد أن أتناول هذا بالتفصيل هنا ، يمكنك قراءة المزيد هنا وهنا
هناك طريقتان للعمل من Java code مع قاعدة البيانات هذه:
- قياسي ، من خلال استعلامات
JDBC
و SQL
- عبر
ClusterJ
، للوصول إلى البيانات عالية الأداء في MySQL Cluster
.

تم بناء ClusterJ حول 4 مفاهيم أساسية:
SessionFactory
- تناظرية تجمع الاتصال ، وتستخدم للحصول على جلسة. يجب أن يكون لكل مثيل من الكتلة SessionFactory الخاص به.Session
- هي اتصال مباشر مع مجموعة MySQL
.Domain Object
- واجهة مشروحة تمثل تعيين جدول في كود Java
، على غرار JPA
.Transaction
- هي وحدة ذرية للعمل. في أي وقت ، في جلسة واحدة ، يتم تنفيذ معاملة واحدة. يتم تنفيذ أي عملية (استقبال ، إدراج ، تحديث ، حذف) في معاملة جديدة.
قيود ClusterJ:
- قلة الانضمام
- لا توجد طريقة لإنشاء جدول وفهارس. للقيام بذلك ، استخدم
JDBC
. - لا تأخير التحميل (
Lazy
). يتم تحميل السجل بأكمله في وقت واحد. - في كائنات المجال ، لا يمكن تعريف العلاقات بين الجداول. تشابه
OneToMany
، ManyToOne
، ManyToMany
غائب تمامًا.
الممارسة. الحديث رخيص. أرني الرمز.
حسنًا ، نظرية كافية ، دعنا ننتقل إلى الممارسة.
المشكلة الأولى التي يجب مواجهتها هي عدم وجود ClusterJ
في مستودع Maven المركزي. تثبيت المكتبة مع الأقلام في المستودع المحلي. من الواضح أنه من أجل الخير ، يجب أن يكون في Nexus
أو بعض Artifactory
، ولكن على سبيل المثال لدينا هذا غير ضروري.
لذا ، اذهب هنا واختر نظام التشغيل الخاص بك. إذا كنت تستخدم نظام تشغيل مثل Linux
، Linux
بتنزيل الحزمة المسماة mysql-cluster-community-java
وتثبيت حزمة rpm / deb هذه. إذا كان لديك Windows
، mysql-cluster-gp
بتنزيل أرشيف mysql-cluster-gp
الكامل.
بطريقة أو بأخرى ، سيكون لدينا ملف برطمان من النموذج: 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++
للعمل مع NDB API
التي ClusterJ
خلال JNI
. بالنسبة Windows
هذه المكتبة (.dll) في أرشيف mysql-cluster-gp
، لأن نظام Linux
يجب عليك تنزيل ndbclient_{version}
.
بعد ذلك ، قم بإنشاء مشروع. سوف نستخدم JUnit5
، JUnit5
+ TestContainers
للاختبارات.
يتكون المشروع من وحدتين:
clusterj-spring-boot-starter
هو بداية تحتوي على ClusterJ
، وكذلك تكوين atoconfiguration. بفضل هذا المنشور ، يمكننا وصف الاتصال بـ MySQL NDB
في ملف appliation.yml
النحو التالي:
clusterj: connectString: localhost:1186 dataBaseName: NDB_DB
بعد ذلك ، سوف يخلق SpringBoot
لنا مصنع SessionFactory
اللازم للاتصال.
clusterj-app
هو التطبيق نفسه ، والذي سيستخدمه المبدئ الخاص بنا. دعونا نتناولها بمزيد من التفاصيل.
للبدء ، نحتاج إلى إنشاء نموذج مجال ، مثل JPA
. في هذه الحالة فقط ، نحتاج إلى القيام بذلك في شكل واجهة ، سيتم تنفيذها في 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
في نفس المخطط ، مما يؤدي إلى تفريغ الجداول ، والتي يجب أن تكون منطقية في مخططات مختلفة.
الآن دعونا نحاول استخدام هذه الواجهة. للقيام بذلك ، نكتب اختبار بسيط.
حتى لا تهتم بتثبيت MySQL Cluster
، سنستخدم المكتبة الرائعة لاختبار TestContainers و Docker . بما أننا نستخدم JUnit5 ، فسنكتب ملحقًا بسيطًا:
تمديد شفرة المصدر 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);
QueryBuilder
استخدام QueryBuilder
لإنشاء استعلامات معقدة QueryBuilder
، where
، على equal
، like
. في هذا الاختبار ، نقوم بسحب جميع المستخدمين الذين اسمهم الأخير = 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);
بفضل الشرح @EnableMySQLClusterContainer
بنا @EnableMySQLClusterContainer
، قمنا بإخفاء تفاصيل إعداد بيئة الاختبار. وأيضًا ، بفضل كاتبنا ، يمكننا ببساطة حقن SessionFactory في اختبارنا ، واستخدامه لتلبية احتياجاتنا ، دون الحاجة إلى القلق بشأن الحاجة إلى إنشاؤه يدويًا.
كل هذا يركز علينا على كتابة منطق الأعمال للاختبارات ، بدلاً من البنية التحتية للخدمة.
أريد أيضًا الانتباه إلى حقيقة أنك تحتاج إلى تشغيل تطبيق يستخدم ClusterJ
مع المعلمة:
-Djava.library.path=/usr/lib/x86_64-linux-gnu/
مما يدل على الطريق إلى libndbclient.so
. بدونها ، لن ينجح شيء.
استنتاج
بالنسبة لي ، ClusterJ
جيدًا في تلك الأنظمة التي تعد مهمة للغاية لسرعة الوصول إلى البيانات ، ولكن العيوب والقيود الطفيفة تفسد الانطباع العام. إذا كانت لديك الفرصة للاختيار ولا تهتم بسرعة الوصول ، أعتقد أنه من الأفضل استخدام JDBC
.
لم تنظر المقالة في التعامل مع المعاملات والأقفال ، وبالتالي فقد تحولت إلى حد كبير.
هذا كل شيء ، سعيد الترميز!
روابط مفيدة:
كل رمز مع المشروع يكمن هنا
تحميل الصفحة
معلومات حول ClusterJ
العمل مع جافا و NDB الكتلة
المؤيد MySQL NDB كتاب الكتلة
المزيد عن MySQL NDB Cluster هنا و هنا
المزيد من حالات الاختبار في مستودع MySQL
نفسه.