
مقدمة
يتفاعل أي نظام معلومات تقريبًا بطريقة أو بأخرى مع مخازن البيانات الخارجية. في معظم الحالات ، تكون هذه قاعدة بيانات علائقية ، وغالبًا ما يتم استخدام نوع من إطار عمل ORM للعمل مع البيانات. ORM يلغي معظم العمليات الروتينية ، وبدلاً من ذلك يقدم مجموعة صغيرة من التجريدات الإضافية للعمل مع البيانات.
نشر مارتن فاولر مقالًا مثيرًا للاهتمام ، أحد الأفكار الرئيسية هناك: "يساعدنا ORM في حل عدد كبير من المشكلات في تطبيقات المؤسسات ... لا يمكن تسمية هذه الأداة بشكل جميل ، لكن المشاكل التي تتعامل معها ليست لطيفة أيضًا. أعتقد أن ORM تستحق مزيدًا من الاحترام والتفاهم ".
نحن نستخدم ORM بشكل مكثف في إطار CUBA ، لذلك نحن نعرف عن كثب المشاكل والقيود المفروضة على هذه التكنولوجيا ، حيث يتم استخدام CUBA في مشاريع مختلفة في جميع أنحاء العالم. هناك العديد من الموضوعات التي يمكن مناقشتها فيما يتعلق بـ ORM ، لكننا سنركز على واحد منها: الاختيار بين طرق "الكسل" (الكسل) و "الجشع" (المتحمس) لأخذ البيانات. سنتحدث عن طرق مختلفة لحل هذه المشكلة من خلال الرسوم التوضيحية من JPA API و Spring ، كما سنشرح كيفية استخدام ORM (ولماذا بالضبط) في CUBA وما العمل الذي نقوم به لتحسين العمل باستخدام البيانات في إطار عملنا.
أخذ عينات البيانات: كسول أم لا؟
إذا كان نموذج البيانات الخاص بك يحتوي على كيان واحد فقط ، فمن المحتمل ألا تلاحظ أي مشاكل عند العمل مع ORM. دعونا نلقي نظرة على مثال صغير. افترض أن لدينا كيان User ()
له سمتين: ID
Name ()
:
public class User { @Id @GeneratedValue private int id; private String name;
للحصول على مثيل لهذا الكيان من قاعدة البيانات ، نحتاج فقط إلى استدعاء طريقة واحدة لكائن EntityManager
:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, id);
تصبح الأمور أكثر إثارة للاهتمام عندما تظهر علاقة رأس بأطراف:
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany private List<Address> addresses;
إذا احتجنا إلى استخراج مثيل مستخدم من قاعدة البيانات ، فسيظهر السؤال: "هل نختار أيضًا عناوين؟". والإجابة "الصحيحة" هنا هي: "يعتمد على ..." في بعض الحالات ، سنحتاج إلى عناوين ، في بعض الحالات - لا. عادةً ما يوفر ORM طريقتين لجلب السجلات التابعة: كسول وجشع. بشكل افتراضي ، تستخدم معظم ORM طريقة الكسول. لكن إذا كتبنا هذا الكود:
EntityManager em = entityManagerFactory.createEntityManager(); User user = em.find(User.class, 1); em.close(); System.out.println(user.getAddresses().get(0));
... ثم نحصل على استثناء “LazyInitException”
، والذي يربك بشكل رهيب القادمين الجدد الذين بدأوا للتو العمل مع ORM. وهنا تأتي اللحظة التي تحتاج فيها لبدء قصة حول ماهية المثيلات "المرفقة" و "المنفصلة" للكيان ، وما هي الجلسات والمعاملات.
نعم ، هذا يعني أنه يجب "إرفاق" الكيان بالجلسة حتى تتمكن من تحديد البيانات التابعة. حسنًا ، دعونا لا نغلق المعاملات على الفور ، وستصبح الحياة أسهل على الفور. وهنا تبرز مشكلة أخرى - تصبح المعاملات أطول ، مما يزيد من خطر الجمود. جعل المعاملات أقصر؟ إنه أمر ممكن ، لكن إذا أنشأت العديد من المعاملات الصغيرة ، فسنحصل على "حكاية كومار كوماروفيتش - أنف طويل وحول ميشا فروي - ذيل قصير" حول كيفية فوز حشد صغير من البعوض الدب - سيحدث ذلك مع قاعدة البيانات. إذا زاد عدد المعاملات الصغيرة بشكل كبير ، فستظهر مشاكل في الأداء.
كما قيل ، عند إحضار بيانات حول مستخدم ، قد تكون العناوين مطلوبة أم لا ، وبالتالي ، بناءً على منطق العمل ، يجب عليك تحديد المجموعة أم لا. من الضروري إضافة شروط جديدة إلى الكود ... Hmmm ... هناك شيء ما يزداد تعقيدًا.
لذا ، ماذا لو جربت نوعًا مختلفًا من العينة؟
public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.EAGER) private List<Address> addresses;
حسنًا ... لا يمكنك القول إنها تساعد كثيرًا. نعم ، سوف نتخلص من LazyInit
المكروه LazyInit
هناك حاجة للتحقق مما إذا كان الكيان مرتبطًا بالجلسة أم لا. ولكن الآن قد نواجه مشكلات في الأداء ، لأننا لا نحتاج دائمًا إلى عناوين ، لكننا لا نزال نختار هذه الكائنات في ذاكرة الخادم.
أي المزيد من الأفكار؟
الربيع jdbc
يتعب بعض المطورين من ORM بحيث يتحولون إلى أطر عمل بديلة. على سبيل المثال ، في Spring JDBC ، الذي يوفر القدرة على تحويل البيانات العلائقية إلى بيانات الكائنات في الوضع "شبه التلقائي". يكتب المطور استعلامات لكل حالة تحتاج إلى مجموعة معينة من السمات (أو يتم إعادة استخدام نفس الرمز للحالات التي تكون فيها هياكل البيانات نفسها مطلوبة).
هذا يعطينا مرونة كبيرة. على سبيل المثال ، يمكنك تحديد سمة واحدة فقط دون إنشاء كائن الكيان المقابل:
String name = this.jdbcTemplate.queryForObject( "select name from t_user where id = ?", new Object[]{1L}, String.class);
أو حدد كائنًا بالشكل المعتاد:
User user = this.jdbcTemplate.queryForObject( "select id, name from t_user where id = ?", new Object[]{1L}, new RowMapper<User>() { public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user = new User(); user.setName(rs.getString("name")); user.setId(rs.getInt("id")); return user; } });
يمكنك أيضًا تحديد قائمة عناوين للمستخدم ، ما عليك سوى كتابة رمز أكثر قليلاً وإنشاء استعلام SQL بشكل صحيح لتجنب مشكلة استعلامات n + 1 .
سووو ، معقدة مرة أخرى. نعم ، نحن نتحكم في جميع الاستعلامات وكيفية تعيين البيانات على الكائنات ، لكننا نحتاج إلى كتابة المزيد من التعليمات البرمجية ، وتعلم SQL ومعرفة كيفية تنفيذ الاستعلامات في قاعدة البيانات. أنا شخصياً أعتقد أن معرفة SQL هي مهارة مطلوبة لمبرمج التطبيق ، لكن ليس الجميع يفكر بهذه الطريقة ، ولن أشارك في الجدال. بعد كل شيء ، معرفة تعليمات التجميع إلى x86 في هذه الأيام هي أيضا اختيارية. دعونا نفكر بشكل أفضل في كيفية جعل الحياة أسهل للمبرمجين.
JPA EntityGraph
ودعنا نعود خطوة ونفكر ، ماذا نحتاج؟ يبدو أننا نحتاج فقط إلى الإشارة إلى السمات التي نحتاجها بالضبط في كل حالة. حسنا ، دعنا نفعل ذلك! قدم JPA 2.1 واجهة برمجة تطبيقات جديدة - EntityGraph (رسم بياني للكيان). الفكرة بسيطة للغاية: نستخدم التعليقات التوضيحية لوصف ما سنختاره من قاعدة البيانات. هنا مثال:
@Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "user-only-entity-graph"), @NamedEntityGraph(name = "user-addresses-entity-graph", attributeNodes = {@NamedAttributeNode("addresses")}) }) public class User { @Id @GeneratedValue private int id; private String name; @OneToMany(fetch = FetchType.LAZY) private Set<Address> addresses;
يوصف رسمان بيانيان لهذا الكيان: لا يحدد user-only-entity-graph
السمة Addresses
(تم وضع علامة كسول) ، بينما يخبر الرسم البياني الثاني ORM بتحديد هذه السمة. إذا حددنا Addresses
أنها حريصة ، فسيتم تجاهل الرسم البياني وسيتم تحديد العناوين على أي حال.
لذلك ، في JPA 2.1 ، يمكنك اختبار بيانات مثل هذا:
EntityManager em = entityManagerFactory.createEntityManager(); EntityGraph graph = em.getEntityGraph("user-addresses-entity-graph"); Map<String, Object> properties = Map.of("javax.persistence.fetchgraph", graph); User user = em.find(User.class, 1, properties); em.close();
هذا النهج يبسط العمل إلى حد كبير ، لا حاجة للتفكير بشكل منفصل حول السمات البطيئة ، وطول المعاملة. ميزة إضافية هي أن الرسم البياني يتم تطبيقه على مستوى استعلام SQL ، لذلك لم يتم تحديد البيانات "الإضافية" في تطبيق Java. ولكن هناك مشكلة صغيرة واحدة: لا يمكنك تحديد السمات التي تم اختيارها والتي لم يتم تحديدها. هناك واجهة برمجة التطبيقات للتحقق ، ويتم ذلك باستخدام فئة PersistenceUtil
:
PersistenceUtil pu = entityManagerFactory.getPersistenceUnitUtil(); System.out.println("User.addresses loaded: " + pu.isLoaded(user, "addresses"));
لكن هذا ممل للغاية وليس الجميع على استعداد للقيام بمثل هذه الشيكات. هل هناك أي شيء آخر يمكنك تبسيطه وعدم إظهار السمات التي لم يتم اختيارها؟
توقعات الربيع
يحتوي Spring Spring على شيء رائع يسمى الإسقاطات (وهذا ليس هو نفسه الإسقاطات في Hibernate ). إذا كنت بحاجة إلى تحديد بعض سمات الكيان فقط ، فسيتم إنشاء واجهة مع السمات الضرورية ، ويقوم Spring بتحديد "مثيلات" هذه الواجهة من قاعدة البيانات. كمثال ، خذ بعين الاعتبار الواجهة التالية:
interface NamesOnly { String getName(); }
الآن يمكنك تحديد مستودع Spring JPA لجلب كيانات المستخدم على النحو التالي:
interface UserRepository extends CrudRepository<User, Integer> { Collection<NamesOnly> findByName(String lastname); }
في هذه الحالة ، بعد استدعاء طريقة findByName ، في القائمة الناتجة ، نحصل على الكيانات التي لها حق الوصول فقط إلى السمات المحددة في الواجهة! وفقًا لنفس المبدأ ، يمكن للمرء اختيار كيانات تابعة ، على سبيل المثال حدد على الفور "سيد التفاصيل" العلاقة. علاوة على ذلك ، فإن Spring ينشئ SQL "صحيحًا" في معظم الحالات ، أي يتم تحديد السمات الموضحة في الإسقاط فقط من قاعدة البيانات ، وهذا يشبه إلى حد كبير كيفية عمل الرسوم البيانية للكيان.
هذا واجهة برمجة تطبيقات قوية جدًا ، عند تعريف الواجهات ، يمكنك استخدام تعبيرات SpEL ، واستخدام الفئات مع نوع من المنطق المدمج بدلاً من الواجهات ، وأكثر من ذلك بكثير ، يتم وصف كل شيء بالتفصيل في الوثائق .
المشكلة الوحيدة في الإسقاطات هي أنه يتم تنفيذها داخلها كأزواج قيمة ، أي للقراءة فقط. هذا يعني أنه حتى لو حددنا طريقة واضعة للإسقاط ، فلن نتمكن من حفظ التغييرات إما من خلال مستودعات CRUD أو من خلال EntityManager. لذا فإن الإسقاطات هي DTOs التي يمكن تحويلها مرة أخرى إلى الكيان وحفظها فقط إذا كتبت الرمز الخاص بك لهذا الغرض.
كيفية اختيار البيانات في كوبا
منذ بداية تطوير إطار عمل CUBA ، حاولنا تحسين جزء الكود الذي يعمل مع قاعدة البيانات. في CUBA ، نستخدم EclipseLink كأساس لواجهة برمجة تطبيقات الوصول إلى البيانات. ما هو جيد في EclipseLink هو أنه يدعم التحميل الجزئي للكيان من البداية ، وكان هذا عاملاً حاسماً في الاختيار بينه وبين السبات. في EclipseLink ، يمكنك تحديد سمات للتحميل قبل ظهور معيار JPA 2.1 بفترة طويلة. CUBA لها طريقتها لوصف رسم بياني للكيان ، يسمى CUBA Views . التمثيلات CUBA هي واجهة برمجة تطبيقات تم تطويرها إلى حد ما ، يمكنك أن ترث بعض العروض من الآخرين ، وتجمعها ، وتطبق على كلاً من الكيانات الرئيسية والتفصيلية. الدافع الآخر لإنشاء طرق عرض CUBA هو أننا أردنا استخدام معاملات قصيرة حتى نتمكن من العمل مع كيانات منفصلة في واجهة مستخدم الويب.
في CUBA ، يتم عرض المشاهدات في ملف XML ، كما في المثال أدناه:
<view class="com.sample.User" extends="_minimal" name="user-minimal-view"> <property name="name"/> <property name="addresses" view="address-street-only-view"/> </property> </view>
تقوم طريقة العرض هذه بتحديد كيان User
name
السمة المحلي الخاص به ، وكذلك تحديد العناوين عن طريق تطبيق address-street-only-view
. كل هذا يحدث (الاهتمام!) على مستوى استعلام SQL. عند إنشاء العرض ، يمكنك استخدامه في تحديد البيانات باستخدام فئة DataManager:
List<User> users = dataManager.load(User.class).view("user-edit-view").list();
تعمل هذه الطريقة بشكل جيد ، بينما تستهلك حركة مرور الشبكة اقتصاديًا ، نظرًا لأن السمات غير المستخدمة لا يتم نقلها ببساطة من قاعدة البيانات إلى التطبيق ، ولكن كما هو الحال في JPA ، هناك مشكلة: لا يمكن القول أن سمات الكيان قد تم تحميلها. وفي CUBA يوجد استثناء “IllegalStateException: Cannot get unfetched attribute [...] from detached object”
، والذي ، مثل LazyInit
، يجب أن يكون قد واجهه كل من يكتب باستخدام إطار عملنا. كما هو الحال في JPA ، هناك طرق للتحقق من السمات التي تم تحميلها وأيها ليست كذلك ، ولكن مرة أخرى ، تعد كتابة هذه الاختبارات مهمة شاقة ومضنية تزعج المطورين كثيرًا. يجب اختراع شيء آخر حتى لا يثقل كاهل الناس عن العمل ، من الناحية النظرية ، يمكن للآلات القيام به.
مفهوم - CUBA عرض واجهات
ولكن ماذا لو حاولت دمج الرسوم البيانية للكيان وتوقعاته؟ قررنا تجربة هذا وقمنا بتطوير واجهات عرض واجهات عرض الكيانات التي تتبع نهج الإسقاط Spring. تتم ترجمة هذه الواجهات إلى طرق عرض CUBA عند بدء تشغيل التطبيق ويمكن استخدامها في DataManager. الفكرة بسيطة: نحن نصف واجهة (أو مجموعة من الواجهات) ، وهو رسم بياني للكيان.
interface UserMinimalView extends BaseEntityView<User, Integer> { String getName(); void setName(String val); List<AddressStreetOnly> getAddresses(); interface AddressStreetOnly extends BaseEntityView<Address, Integer> { String getStreet(); void setStreet(String street); } }
تجدر الإشارة إلى أنه في بعض الحالات المحددة ، يمكنك إنشاء واجهات محلية ، كما في حالة AddressStreetOnly
من المثال أعلاه ، حتى لا "تلوث" واجهة برمجة التطبيقات العامة AddressStreetOnly
.
في عملية بدء تطبيق CUBA (معظمه هو تهيئة سياق Spring) ، نقوم بإنشاء طرق عرض CUBA برمجيًا ونضعها في مستودع الحبة الداخلي في السياق.
أنت الآن بحاجة إلى تعديل تطبيق فئة DataManager قليلاً حتى يقبل طرق عرض الواجهة ، ويمكنك تحديد الكيانات بهذه الطريقة:
List<UserMinimalView> users = dataManager.load(UserMinimalView.class).list();
تحت الغطاء ، يتم إنشاء كائن وكيل يقوم بتطبيق الواجهة ويلتف مثيل الكيان المحدد من قاعدة البيانات (بالطريقة نفسها كما في حالة السبات). وعندما يستدعي المطور قيمة السمة ، يقوم الوكيل بتفويض استدعاء الأسلوب إلى المثيل "الحقيقي" للكيان.
في تطوير هذا المفهوم ، نحاول قتل عصفورين بحجر واحد:
- لا يتم تحميل البيانات غير الموضحة في الواجهة في التطبيق ، وبالتالي توفير موارد الخادم.
- يمكن للمطور استخدام تلك السمات التي يمكن الوصول إليها فقط من خلال الواجهة (وبالتالي ، يتم
UnfetchedAttribute
من قاعدة البيانات) ، وبالتالي التخلص من استثناءات UnfetchedAttribute
التي كتبنا عنها أعلاه.
على عكس الإسقاطات Spring ، نلف الكيانات في كائنات الوكيل ، بالإضافة إلى ذلك ، كل واجهة ترث واجهة CUBA القياسية - Entity
. هذا يعني أنه يمكن تغيير سمات عرض الكيان ، ثم قم بحفظ هذه التغييرات في قاعدة البيانات باستخدام واجهة برمجة تطبيقات CUBA القياسية للتعامل مع البيانات.
وبالمناسبة ، "الأرنب الثالث" - يمكنك جعل السمات للقراءة فقط إذا قمت بتحديد واجهة باستخدام طرق getter فقط. وبالتالي ، قمنا بالفعل بتعيين قواعد التعديل على مستوى واجهة برمجة تطبيقات الكيان.
بالإضافة إلى ذلك ، يمكنك القيام ببعض العمليات المحلية للكيانات المنفصلة باستخدام السمات المتاحة ، على سبيل المثال ، تحويل سلسلة الاسم ، كما في المثال أدناه:
@MetaProperty default String getNameLowercase() { return getName().toLowerCase(); }
لاحظ أنه يمكن إخراج السمات المحسوبة من نموذج فئة الكيان ونقلها إلى واجهات قابلة للتطبيق على منطق عمل معين.
ميزة أخرى مثيرة للاهتمام هي واجهة الميراث. يمكنك إنشاء عدة طرق عرض مع مجموعات مختلفة من السمات ، ثم دمجها. على سبيل المثال ، يمكنك إنشاء واجهة لكيان المستخدم مع سمات الاسم والبريد الإلكتروني ، وواجهة أخرى بها سمات الاسم والعناوين. الآن ، إذا كنت بحاجة إلى تحديد الاسم والبريد الإلكتروني والعناوين ، فأنت لا تحتاج إلى نسخ هذه السمات إلى الواجهة الثالثة ، فأنت بحاجة فقط إلى أن ترث من أول عرضين. ونعم ، يمكن تمرير مثيلات الواجهة الثالثة إلى الأساليب التي تقبل المعلمات بنوع الواجهات الأصل ، وقواعد OOP هي نفسها للجميع.
تم أيضًا تنفيذ تحويل بين طرق العرض - كل واجهة لها طريقة إعادة تحميل () ، والتي يمكنك من خلالها تمرير فئة العرض كمعلمة:
UserFullView userFull = userMinimal.reload(UserFullView.class);
قد يحتوي UserFullView على سمات إضافية ، لذلك سيتم إعادة تحميل الكيان من قاعدة البيانات ، إذا لزم الأمر. وهذه العملية متأخرة. لن يتم الوصول إلى قاعدة البيانات إلا عند حدوث أول وصول إلى سمات الكيان. سيؤدي هذا إلى إبطاء المكالمة الأولى قليلاً ، ولكن تم اختيار هذا النهج عن قصد - إذا تم استخدام مثيل الكيان في الوحدة النمطية "web" ، التي تحتوي على واجهة المستخدم ووحدات التحكم في REST الخاصة بها ، يمكن نشر هذه الوحدة على خادم منفصل. وهذا يعني أن الحمل الزائد القسري للكيان سيخلق حركة مرور إضافية على الشبكة - الوصول إلى الوحدة الأساسية ثم إلى قاعدة البيانات. وبالتالي ، فإيقاف التحميل الزائد حتى اللحظة التي يكون فيها ذلك ضروريًا ، فإننا نحفظ حركة المرور ونخفض عدد استعلامات قاعدة البيانات.
تم تصميم المفهوم كوحدة نمطية لـ CUBA ، ويمكن تنزيل مثال للاستخدام من GitHub .
استنتاج
يبدو أننا في المستقبل القريب سنظل نستخدم ORM على نطاق واسع في تطبيقات المؤسسات ببساطة لأننا نحتاج إلى شيء يحول البيانات العلائقية إلى كائنات. بالطبع ، سيتم تطوير حلول محددة لتطبيقات معقدة وفريدة وعالية التحميل ، ولكن يبدو أن أطر عمل ORM ستعيش ما دامت قواعد البيانات العلائقية.
في CUBA ، نحاول تبسيط العمل مع ORM إلى أقصى حد ، وفي الإصدارات المستقبلية سوف نقدم ميزات جديدة للعمل مع البيانات. سيكون من الصعب تحديد ما إذا كانت هذه ستكون واجهات عرض تقديمي أو أي شيء آخر ، لكنني متأكد من شيء واحد: سنواصل تبسيط العمل بالبيانات في الإصدارات المستقبلية من الإطار.