ClusterJ - العمل مع MySQL NDB Cluster من جافا

مرحبا يا هبر! في هذه المقالة ، أود أن أعتبر مكتبة لـ 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); // 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 استخدام 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); // 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 بنا @EnableMySQLClusterContainer ، قمنا بإخفاء تفاصيل إعداد بيئة الاختبار. وأيضًا ، بفضل كاتبنا ، يمكننا ببساطة حقن SessionFactory في اختبارنا ، واستخدامه لتلبية احتياجاتنا ، دون الحاجة إلى القلق بشأن الحاجة إلى إنشاؤه يدويًا.
كل هذا يركز علينا على كتابة منطق الأعمال للاختبارات ، بدلاً من البنية التحتية للخدمة.


أريد أيضًا الانتباه إلى حقيقة أنك تحتاج إلى تشغيل تطبيق يستخدم ClusterJ مع المعلمة:


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

مما يدل على الطريق إلى libndbclient.so . بدونها ، لن ينجح شيء.


استنتاج


بالنسبة لي ، ClusterJ جيدًا في تلك الأنظمة التي تعد مهمة للغاية لسرعة الوصول إلى البيانات ، ولكن العيوب والقيود الطفيفة تفسد الانطباع العام. إذا كانت لديك الفرصة للاختيار ولا تهتم بسرعة الوصول ، أعتقد أنه من الأفضل استخدام JDBC .


لم تنظر المقالة في التعامل مع المعاملات والأقفال ، وبالتالي فقد تحولت إلى حد كبير.


هذا كل شيء ، سعيد الترميز!


روابط مفيدة:


كل رمز مع المشروع يكمن هنا
تحميل الصفحة
معلومات حول ClusterJ
العمل مع جافا و NDB الكتلة
المؤيد MySQL NDB كتاب الكتلة
المزيد عن MySQL NDB Cluster هنا و هنا


المزيد من حالات الاختبار في مستودع MySQL نفسه.

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


All Articles