asentinel-orm هي أداة ORM خفيفة الوزن مبنية على رأس Spring JDBC، وخاصة JdbcTemplate
. وبالتالي، فهي تمتلك معظم الميزات التي يتوقعها المرء من ORM بسيط، مثل توليد SQL، التحميل الكسول، إلخ.
من خلال الاستفادة من JdbcTemplate
، فهذا يعني أنها تسمح بالمشاركة في المعاملات المدارة من قبل Spring، ويمكن دمجها بسهولة في أي مشروع يستخدم بالفعل JdbcTemplate
كوسيلة للتفاعل مع قاعدة البيانات.
منذ عام 2015، تم استخدام asentinel-orm بنجاح في عدة تطبيقات وتم تحسينها باستمرار حسب احتياجات الأعمال. في صيف عام 2024، أصبحت رسميًا مشروع مفتوح المصدر، وهو ما نعتبره سيعجل من تطورها ويزيد من عدد المساهمين.
في هذه المقالة، يتم بناء تطبيق نموذجي لتوضيح عدة ميزات رئيسية لـ ORM:
- تكوين بسيط
- نموذج كيان نطاق مباشر عبر تعليقات مخصصة
- كتابة سهلة وتنفيذ آمن لعبارات SQL العادية
- توليد تلقائي لعبارات SQL
- نموذج ديناميكي (تُغنى الكيانات بخصائص إضافية في وقت التشغيل، وتُحفظ، وتُقرأ دون تغييرات في الكود)
تطبيق
إعداد
- Java 21
- Spring Boot 3.4.0
- asentinel-orm 1.70.0
- قاعدة بيانات H2
تكوين
لتفاعل مع asentinel-orm واستغلال وظائفه، يلزم وجود مثيل من OrmOperations
.
كما هو مذكور في JavaDoc، فإن هذا هو الواجهة المركزية لأداء عمليات ORM، وليس من المقصود أو المطلوب أن يتم تنفيذه بشكل محدد في كود العميل.
يتضمن التطبيق النموذجي كود التكوين لإنشاء كائن من هذا النوع.
public OrmOperations orm(SqlBuilderFactory sqlBuilderFactory,
JdbcFlavor jdbcFlavor, SqlQuery sqlQuery) {
return new OrmTemplate(sqlBuilderFactory, new SimpleUpdater(jdbcFlavor, sqlQuery));
}
OrmOperations
لديه واجهتان رئيسيتان:
- SqlBuilderFactory – تقوم بإنشاء مثيلات
SqlBuilder
التي يمكن استخدامها لاحقًا لإنشاء استعلامات SQL. يتمكنSqlBuilder
من توليد أجزاء من الاستعلام تلقائيًا، على سبيل المثال، الجزء الذي يحدد الأعمدة. يمكن إضافة عبارة where، وعبارة order by، وشروط أخرى، والأعمدة الفعلية باستخدام طرق من فئةSqlBuilder
أيضًا. في الجزء التالي من هذه القسم، يتم عرض مثال على استعلام تم إنشاؤه بواسطةSqlBuilder
. - Updater – تُستخدم لحفظ الكيانات في جداول قاعدة البيانات الخاصة بها. يمكن أن تقوم بإدراج أو تحديث اعتمادًا على ما إذا كانت الكيان تم إنشاؤه حديثًا أو كان موجودًا بالفعل. توجد واجهة استراتيجية تسمى
NewEntityDetector
تستخدم لتحديد ما إذا كانت الكيان جديدة. بشكل افتراضي، يتم استخدامSimpleNewEntityDetector
.
تُنفذ جميع الاستعلامات التي يتم إنشاؤها بواسطة ORM باستخدام SqlQueryTemplate
مثيل، الذي يحتاج أيضًا إلى JdbcOperations
/JdbcTemplate
لتعمل. في النهاية، تصل جميع الاستعلامات إلى JdbcTemplate
القديم الجيد من خلاله يتم تنفيذها أثناء المشاركة في معاملات Spring، تمامًا كما يحدث في أي JdbcTemplate
تنفيذ مباشر.
توفر التراكيب المنطقية وSQL المحددة لقاعدة البيانات من خلال تطبيقات واجهة JdbcFlavor
، التي تُحقن بعد ذلك في معظم الحبوب المذكورة أعلاه. في هذه المقالة، وبما أن قاعدة بيانات H2 مستخدمة، يتم تكوين تطبيق H2JdbcFlavor
.
التكوين الكامل لـ ORM كجزء من تطبيق العينة هو OrmConfig
.
التنفيذ
النموذج التجريبي للنطاق الذي تعرضه تطبيق العينة هو بسيط ويتكون من كيانين – مصنعي السيارات و نماذج السيارات. يمثل ما تدل عليه أسماؤهم بالضبط، العلاقة بينهما واضحة: قد يمتلك مصنع سيارات واحد عدة نماذج سيارات.
بالإضافة إلى اسمه، يتم إثراء مصنع السيارات بسمات (أعمدة) يتم إدخالها من قبل مستخدم التطبيق ديناميكيًا في وقت التشغيل. الحالة التي تم توضيحها هي مباشرة:
- يُطلب من المستخدم تقديم الأسماء والأنواع المستهدفة للسمات الديناميكية
- تم إنشاء زوج من مصنعي السيارات بقيم ملموسة للسمات الديناميكية المضافة مسبقًا، ثم
- يتم تحميل الكيانات الموصوفة بواسطة السمات الأولية والمحددة في وقت التشغيل
يتم رسم الكيانات الأولية باستخدام جداول قاعدة البيانات أدناه:
CREATE TABLE CarManufacturers (
ID INT auto_increment PRIMARY KEY,
NAME VARCHAR(255)
);
CREATE TABLE CarModels(
ID INT auto_increment PRIMARY KEY,
CarManufacturer int,
NAME VARCHAR(255),
TYPE VARCHAR(15),
foreign key (CarManufacturer) references CarManufacturers(id)
);
تتم تزيين الفئات المجالية المقابلة بتعليقات خاصة بـ ORM لتكوين التعيينات إلى الجداول قاعدة البيانات أعلاه.
"CarManufacturers") (
public class CarManufacturer {
"id") (
private int id;
"name") (
private String name;
parentRelationType = RelationType.MANY_TO_ONE, (
fkName = CarModel.COL_CAR_MANUFACTURER,
fetchType = FetchType.LAZY)
private List<CarModel> models = Collections.emptyList();
...
}
"CarModels") (
public class CarModel {
public static final String COL_CAR_MANUFACTURER = "CarManufacturer";
"id") (
private int id;
"name") (
private String name;
"type") (
private CarType type;
fkName = COL_CAR_MANUFACTURER, fetchType = FetchType.LAZY) (
private CarManufacturer carManufacturer;
...
}
public enum CarType {
CAR, SUV, TRUCK
}
بعض الاعتبارات:
@Table
– يعين (يقارن) الفئة بجدول قاعدة بيانات@PkColumn
– يعين الـid
(المعرف الفريد) إلى مفتاح الجدول الأساسي@Column
– يعين عضو فئة إلى عمود جدول@Child
– يعرف العلاقة مع كيان آخر- أعضاء محددة بـ
@Child
– مُكونة ليتم تحميلها بكسل - عمود جدول
type
– مُعين إلى حقلenum
–CarType
لكي تدعم فئة CarManufacturer
السمات المحددة في وقت التشغيل (معينة إلى أعمدة الجدول المحددة في وقت التشغيل)، يتم تحديد فئة فرعية كما هو مبين أدناه:
public class CustomFieldsCarManufacturer extends CarManufacturer
implements DynamicColumnsEntity<DynamicColumn> {
private final Map<DynamicColumn, Object> customFields = new HashMap<>();
...
public void setValue(DynamicColumn column, Object value) {
customFields.put(column, value);
}
public Object getValue(DynamicColumn column) {
return customFields.get(column);
}
...
}
تُخزن هذه الفئة السمات (الحقول) المحددة في وقت التشغيل في Map
. يتم تحقيق التفاعل بين قيم الحقول في وقت التشغيل و ORM من خلال تنفيذ واجهة DynamicColumnEntity
.
public interface DynamicColumnsEntity<T extends DynamicColumn> {
void setValue(T column, Object value);
Object getValue(T column);
}
setValue()
– يُستخدم لتعيين قيمة العمود المحدد في وقت التشغيل عند قراءته من الجدولgetValue()
– يُستخدم لاسترجاع قيمة العمود المحدد في وقت التشغيل عند حفظه في الجدول
يُعالج DynamicColumn
السمات المحددة في وقت التشغيل إلى أعمدتها المقابلة بطريقة مشابهة لتعيين @Column
الذي يعالج الأعضاء المعروفة في وقت الترجمة.
عند تشغيل التطبيق، يتم تنفيذ CfRunner
. يُطلب من المستخدم إدخال أسماء وأنواع للسمات الديناميكية المخصصة المرغوبة التي تُثري كيان CarManufacturer
(لتبسيط الأمر، يتم دعم أنواع int
و varchar
).
لكل زوج من الاسم والنوع، يتم تنفيذ أمر DML حتى يمكن إضافة الأعمدة الجديدة إلى جدول قاعدة بيانات CarManufacturer
. الطريقة التالية (المعرفة في CarService
) تنفذ العملية.
public void addManufacturerField(String name, String type) {
orm.getSqlQuery()
.update("alter table CarManufacturers add column " + name + " " + type);
}
يتم تسجيل كل سمة مدخلة كـ DefaultDynamicColumn
، وهو تنفيذ مرجعي لـ DynamicColumn
.
بمجرد تعريف جميع السمات، تتم إضافة مصنعين للسيارات إلى قاعدة البيانات، حيث يوفر المستخدم قيمًا لكل سمة من هذه السمات.
Map<DynamicColumn, Object> dynamicColumnsValues = new HashMap<>();
for (DynamicColumn dynamicColumn : dynamicColumns) {
// read values for each dynamic attribute
...
}
CustomFieldsCarManufacturer mazda = new CustomFieldsCarManufacturer("Mazda", dynamicColumnsValues);
carService.createManufacturer(mazda, dynamicColumns);
الطريقة أدناه (المعرفة في CarService
) تقوم فعليًا بإنشاء الكيان عبر ORM.
public void createManufacturer(CustomFieldsCarManufacturer manufacturer, List<DynamicColumn> attributes) {
orm.update(manufacturer, new UpdateSettings<>(attributes, null));
}
يتم استدعاء النسخة ذات المعاملين من OrmOperations
update()
، والتي تسمح بتمرير مثيل UpdateSettings
والتواصل مع ORM عند التنفيذ بأن هناك قيم محددة في وقت التشغيل يجب الاحتفاظ بها.
أخيرًا، يتم إنشاء نموذجين للسيارات، يتوافقان مع أحد الشركات المصنعة للسيارات التي تمت إضافتها مسبقًا.
CarModel mx5 = new CarModel("MX5", CarType.CAR, mazda);
CarModel cx60 = new CarModel("CX60", CarType.SUV, mazda);
carService.createModels(mx5, cx60);
تقوم الطريقة أدناه (المعرفة في CarService
) بإنشاء الكيانات عبر ORM، هذه المرة باستخدام طريقة OrmOperations
update() للاحتفاظ بالكيانات بدون سمات ديناميكية. لسهولة الاستخدام، يتم إنشاء عدة كيانات في مكالمة واحدة.
public void createModels(CarModel... models) {
orm.update(models);
}
كخطوة أخيرة، يتم تحميل أحد الشركات المصنعة التي تم إنشاؤها مرة أخرى بواسطة اسمها باستخدام استعلام تم إنشاؤه بواسطة ORM.
CarManufacturer mazda1 = carService.findManufacturerByName("Mazda", dynamicColumns);
readOnly = true) (
public CarManufacturer findManufacturerByName(String name, List<DynamicColumn> attributes) {
return orm.newSqlBuilder(CustomFieldsCarManufacturer.class)
.select(
AutoEagerLoader.forPath(CarManufacturer.class, CarModel.class),
new DynamicColumnsEntityNodeCallback<>(
new DefaultObjectFactory<>(CustomFieldsCarManufacturer.class),
attributes
)
)
.where().column("name").eq(name)
.execForEntity();
}
تستحق بعض الشروحات المتعلقة بالطريقة المعرفة أعلاه القيام بها.
تقوم طريقة OrmOperations
newSqlBuilder()
بإنشاء مثيل SqlBuilder
، وكما يشير الاسم، يمكن استخدام هذا لإنشاء استعلامات SQL. تقوم طريقة SqlBuilder
select()
بإنشاء جزء select from table من الاستعلام، في حين يجب إضافة الباقي (where, order by). يمكن تخصيص جزء الاستعلام select عن طريق تمرير مثيلات EntityDescriptorNodeCallback
(قد تكون التفاصيل المتعلقة بـ EntityDescriptorNodeCallback
موضوع مقال مستقبلي).
لكي نعلم الـ ORM أن الخطة هي اختيار وتعيين الأعمدة المعرفة أثناء وقت التشغيل، يجب تمرير DynamicColumnsEntityNodeCallback
. مع ذلك، يتم توفير AutoEagerLoader
حتى يفهم الـ ORM أنه يجب تحميل قائمة CarModel
s المتعلقة بالشركة المصنعة بشكل متعجل. ومع ذلك، لا علاقة لهذا بالسمات المعرفة أثناء وقت التشغيل، ولكنه يظهر كيف يمكن تحميل عضو فرعي بشكل متعجل.
الخاتمة
بينما قد تكون هناك طرق أخرى للعمل مع الأعمدة المعرفة أثناء وقت التشغيل عندما يتم تخزين البيانات في قواعد البيانات العلائقية، فإن النهج المقدم في هذه المقالة له ميزة استخدام أعمدة قاعدة البيانات القياسية التي تتم قراءتها/كتابتها باستخدام استعلامات SQL القياسية التي يتم إنتاجها مباشرة بواسطة الـ ORM.
لم يكن من النادر عندما أتيحت لنا الفرصة لمناقشة في “المجتمع” asentinel-orm، الأسباب التي دفعتنا لتطوير مثل هذه الأداة. عادة، من النظرة الأولى، أثبت المطورون أنهم مترددون ومحتاطون عندما يتعلق الأمر بـ ORM مصنوعة حسب الطلب، متسائلين لماذا لا نستخدم Hibernate أو غيرها من تطبيقات JPA.
في حالتنا، كان الدافع الرئيسي هو الحاجة إلى طريقة سريعة ومرنة وسهلة للعمل مع عدد كبير أحياناً من السمات المعرفة أثناء وقت التشغيل (الأعمدة) للكيانات التي هي جزء من مجال العمل. بالنسبة لنا، أثبتت أنها الطريقة الصحيحة. التطبيقات تعمل بسلاسة في الإنتاج، العملاء راضون عن السرعة والأداء المحقق، والمطورون مرتاحون ومبدعون مع واجهة برمجة التطبيقات البديهية.
مع فتح مشروع المصدر الحالي، من السهل جدًا على أي شخص مهتم أن يلقي نظرة عليه، ويشكل رأيًا موضوعيًا حوله، و، لما لا، ينشئ نسخة منه، ويُقدم طلب سحب، ويساهم.
الموارد
Source:
https://dzone.com/articles/runtime-defined-columns-with-asentinel-orm