Skip to content

6. Spring Data JPA Hibernate

6.1. مقدمة

سنستخدم قاعدة البيانات [dbproduitscategories] التي يديرها مشروع [spring-jdbc-04] وننفذ الواجهتين [IDao<CategoryIDao<Product>] المحددتين في ذلك المشروع. سيسمح لنا ذلك بالقيام بعدة أمور:

  • مقارنة كود التنفيذ؛
  • استخدام نفس طبقة الاختبار؛
  • مقارنة أداء التنفيذين؛
  • يتم تنفيذ طبقة [JDBC] بواسطة مشروع [mysql-config-jdbc] الذي تمت مناقشته في القسم 3.3؛

سننتقل الآن إلى الطبقات الأخرى.

6.2. إعداد بيئة العمل

باستخدام STS، قم باستيراد مشروع [mysql-config-jpa-hibernate] [1] الموجود في المجلد [<examples>/spring-database-config/mysql/eclipse] [2]:

يقوم هذا المشروع بتكوين طبقة [Spring JPA Hibernate] للمشروع. لكل تطبيق JPA مشروع تكوين خاص به.

بعد ذلك، قم باستيراد مشروع [spring-jpa-generic] [1] الموجود في المجلد [<examples>/spring-database-generic/spring-jpa] [2]:

بمجرد الانتهاء من ذلك، قم بتحديث بيئة Maven (Alt-F5) لجميع المشاريع في [Package Explorer]:

 

ثم، للتحقق من بيئة العمل، قم بتشغيل تكوين البناء المسمى [spring-jpa-generic-JUnitTestDao-hibernate]:

تقوم هذه التهيئة بتشغيل اختبار [JUnitTestDao]. يجب أن ينجح هذا الاختبار:

  

6.3. مشروع تكوين طبقة JPA

  

الغرض من هذا المشروع هو تكوين طبقة JPA للبنية الموضحة أدناه:

6.3.1. تكوين Maven

المشروع هو مشروع Maven تم تكوينه بواسطة ملف [pom.xml] التالي:


<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
    xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
    <modelVersion>4.0.0</modelVersion>
    <groupId>dvp.spring.database</groupId>
    <artifactId>generic-config-jpa</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>configuration mysql openjpa</name>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- dépendances variables ********************************************** -->
        <!-- JPA provider -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- dépendances constantes ********************************************** -->
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Spring Context -->
        <!-- configuration JDBC inherited -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-jdbc</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 5-7: عنصر Maven الذي تم إنشاؤه بواسطة هذا المشروع. ستستخدم مشاريع التكوين الخاصة بتنفيذات JPA الأخرى (Eclipselink و OpenJpa) هذا العنصر نفسه. وهذا يعني أنه لا يمكن أن يكون سوى مشروع واحد من هذه المشاريع نشطًا في أي وقت معين. لذا يجب تجنب وجودها جميعًا في [Package Explorer]. فليس هناك حاجة سوى لمشروع واحد؛
  • الأسطر 10–14: مشروع Maven الأصلي الذي يحدد إصدارات معظم التبعيات المطلوبة للمشروع؛
  • الأسطر 19–22: مكتبة Hibernate؛
  • الأسطر 25-28: مكتبة Spring Data؛
  • الأسطر 32–34: يعتمد مشروع تكوين طبقة JPA على مشروع تكوين طبقة JDBC، الذي يحدد، من بين أمور أخرى، برنامج تشغيل JDBC لنظام إدارة قواعد البيانات المستخدم وتفاصيل اتصال قاعدة البيانات؛
  • الأسطر 35–39: يتضمن مشروع تكوين طبقة JDBC مكتبة [Spring JDBC]، والتي تم استبدالها هنا بمكتبة [Spring Data JPA]. لذلك، نحدد عدم تضمينها في تبعيات المشروع. ومع ذلك، إذا بقيت، فلن يتسبب ذلك في أي أخطاء؛

في النهاية، تكون تبعيات المشروع كما يلي:

  

6.3.2. تكوين الربيع

 

تقوم فئة [ConfigJpa] بتكوين مشروع Spring:


package generic.jpa.config;
 
import javax.persistence.EntityManagerFactory;
 
import generic.jdbc.config.ConfigJdbc;
 
import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.PlatformTransactionManager;
 
@Configuration
@Import({ ConfigJdbc.class })
public class ConfigJpa {
 
    // the provider JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        return hibernateJpaVendorAdapter;
    }
 
    // JPA entity packages
    public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };
 
    // data source
    @Bean
    public DataSource dataSource() {
        // data source TomcatJdbc
        DataSource dataSource = new DataSource();
        // configuration access JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // initially open connections
        dataSource.setInitialSize(5);
        // result
        return dataSource;
    }
 
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(ENTITIES_PACKAGES);
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }
 
    // Transaction manager
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }
 
}
  • السطر 18: الفئة هي فئة تكوين Spring؛
  • السطر 19: تستورد الفاصوليا المحددة بواسطة فئة التكوين [ConfigJdbc] المستخدمة لتكوين مشروع Spring [mysql-config-jdbc]. هذه هي مرشحات JSON؛
  • الأسطر 23–30: تحدد تطبيق JPA المستخدم، وفي هذه الحالة تطبيق Hibernate (السطر 25)؛
  • السطر 26: يمكنك اختيار ما إذا كنت تريد عرض عمليات SQL التي ينفذها تطبيق Hibernate أم لا؛
  • السطر 27: يحدد نظام إدارة قواعد البيانات (DBMS) المتصل بـ Hibernate. هذا التكوين مهم. فهو يسمح لـ Hibernate باستخدام لهجة SQL الخاصة بنظام إدارة قواعد البيانات MySQL، بما في ذلك ميزاته الخاصة. علاوة على ذلك، فإنه يبلغه بأنواع SQL وكائنات نظام إدارة قواعد البيانات التي سيتمكن من استخدامها. إن هذه القدرة التي يتمتع بها تطبيق JPA على التكيف مع نظام إدارة قواعد بيانات معين هي التي تمنحه قابلية نقل عالية عبر أنظمة إدارة قواعد البيانات؛
  • السطر 28: قد يقوم Hibernate أو لا يقوم بإنشاء الجداول لقاعدة البيانات المستهدفة من كيانات JPA التي يعثر عليها. لا يحدث هذا الإنشاء إلا في حالة عدم وجود الجداول. إذا كانت موجودة بالفعل، فلن يتم القيام بأي شيء. سنستخدم هذه القدرة على إنشاء الجداول عندما نوضح كيفية إنشاء نصوص إنشاء SQL لمختلف قواعد البيانات المستخدمة في هذا المستند؛
  • السطر 33: الحزمة التي تحتوي على كيانات JPA لقاعدة البيانات [dbproduitscategories
  • الأسطر 36-49: مصدر البيانات [tomcat-jdbc] المرتبط بقاعدة البيانات [dbproduitscategories
  • الأسطر 52-60: الحبة المسماة [entityManagerFactory] (يجب تسميتها بهذه الطريقة) هي الحبة التي ستنشئ كائن [EntityManager]، الذي يدير سياق ثبات JPA. تمر جميع عمليات JPA من خلاله. نظرًا لأننا نستخدم [Spring Data JPA]، فلن نستخدم هذا الكائن أبدًا بأنفسنا. ومع ذلك، نحتاج إلى تكوينه. يجب أن يعرف ما يلي:
    • تنفيذ JPA المستخدم (السطر 55)؛
    • مصدر البيانات المستخدم (السطر 57)؛
    • كيانات JPA لهذا المصدر (السطر 56)؛
  • السطر 58: تهيئة EntityManager بهذه المعلومات؛
  • السطر 59: يُرجع الكائن الفردي [entityManagerFactory
  • الأسطر 63-68: تعريف مدير المعاملات. يجب تسميته [transactionManager
  • السطر 65: يتم إنشاء مدير معاملات JPA؛
  • السطر 66: يتم توصيله بمصدر البيانات من السطر 37 عبر حبة [entityManagerFactory] (السطران 53 و 57)؛

يعتمد فقط bean الموجود في الأسطر 23–30 على تطبيق JPA المستخدم. ثم تعتمد عليه beans الأخرى.

6.3.3. الكيانات في طبقة [JPA]

  

قاعدة البيانات المستهدفة هي قاعدة البيانات [dbproduitscategories] مع الجدولين [CATEGORIES] و[PRODUITS]. وقد رأينا أنها تحتوي أيضًا على ثلاثة جداول أخرى [USERS، ROLES، USERS_ROLES] ستُستخدم لتأمين خدمة الويب التي سيتم نشرها على الويب. سنتجاهل هذه الجداول في الوقت الحالي. للتذكير، إليك هيكل الجدولين [CATEGORIES] و[PRODUCTS]:

جدول [PRODUCTS] كما يلي:

  • [ID]: المفتاح الأساسي المتزايد تلقائيًا للجدول [2]؛
  • [NAME]: الاسم الفريد للمنتج [4]؛
  • [PRICE]: سعر المنتج؛
  • [DESCRIPTION]: وصف المنتج؛
  • [VERSIONING] هو رقم إصدار المنتج. الإصدار الأولي هو 1 [3]. في كل مرة يتم تعديل المنتج، يتم زيادة رقم إصداره بواسطة الكود الذي يشغل الجدول؛
  • [CATEGORY_ID]: المفتاح الخارجي في جدول [CATEGORIES] لتحديد الفئة التي ينتمي إليها المنتج؛
  • في [1-3]، المفتاح الخارجي [CATEGORIE_ID] لجدول [PRODUITS]. وهو يشير إلى عمود [ID] في جدول [CATEGORIES] [4-5]؛
  • عند حذف فئة، يتم حذف جميع المنتجات المرتبطة بها أيضًا [6]. من المهم ملاحظة هذه النقطة لأنها تُستخدم في بناء طبقة [DAO] التي تستخدم قاعدة البيانات [dbproduitscategories

جدول [CATEGORIES] كما يلي:

  • [ID]: مفتاح أساسي متزايد تلقائيًا؛
  • [VERSIONING]: رقم إصدار الفئة؛
  • [NAME]: الاسم الفريد للفئة؛

سنقوم الآن بوصف كيانات JPA [Product] و [Category]، والتي تتوافق مع الجدولين [PRODUCTS] و [CATEGORIES].

  

6.3.3.1. واجهة [AbstractCoreEntity]

يتم تنفيذ واجهة [AbstractCoreEntity] بواسطة كيانات JPA [Category] و [Product]:


package generic.jpa.entities.dbproduitscategories;
 
public interface AbstractCoreEntity {
 
    // getters and setters for [id], [version], [entityType] fields
    public Long getId();
 
    public void setId(Long id);
 
    public Long getVersion();
 
    public void setVersion(Long version);
 
    public enum EntityType {
        PROXY, POJO
    }
 
    public EntityType getEntityType();
 
    public void setEntityType(EntityType entityType);
 
}

هذه الواجهة، التي تنفذها كيانات JPA، تسرد ببساطة الطرق الخاصة بقراءة وكتابة حقول [id] و[version] و[entityType] لهذه الكيانات. سيتم شرح دور حقل [entityType] لاحقًا؛

6.3.3.2. كيان JPA [Product]

فئة [Product] هي كيان JPA المرتبط بصف في جدول [PRODUCTS]:

 

package generic.jpa.entities.dbproduitscategories;
 
import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
 
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_PRODUITS)
@JsonFilter("jsonFilterProduit")
public class Produit implements AbstractCoreEntity {
    // properties
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType = EntityType.POJO;
 
    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();

    // properties
    @Column(name = ConfigJdbc.TAB_PRODUITS_NOM, unique = true, length = 30, nullable = false)
    private String nom;
 
    @Column(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, insertable = false, updatable = false, nullable = false)
    private Long idCategorie;
 
    @Column(name = ConfigJdbc.TAB_PRODUITS_PRIX, nullable = false)
    private double prix;
 
    @Column(name = ConfigJdbc.TAB_PRODUITS_DESCRIPTION, length = 100)
    private String description;
 
    // the category
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
    private Categorie categorie;
 
    // manufacturers
    public Produit() {
 
    }
 
    public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
            Categorie categorie) {
        this.id = id;
        this.version = version;
        this.nom = nom;
        this.idCategorie = idCategorie;
        this.prix = prix;
        this.description = description;
        this.categorie = categorie;
    }
 
    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
                description, idCategorie);
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }
 
    // getters and setters
...
    public void setCategorie(Categorie categorie) {
        // entity type
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1005, new RuntimeException(
                    "On ne peut changer la catégorie d'un produit de type [PROXY]"), simpleClassName);
        }
        this.categorie = categorie;
    }
 
}
  • السطر 21: تجعل العلامة [@Entity] فئة [Product] كيانًا تديره طبقة [JPA]. يمكنك أيضًا كتابة [@Entity(name="MyProduct")]، مما يمنح الكيان الاسم [MyProduct]. في حالة عدم وجود هذه المعلومات، يكون اسم الكيان هو اسم الفئة، وهو في هذه الحالة [Product]. تصبح قاعدة التسمية هذه ضرورية عندما توجد فئتان من حزمتين مختلفتين بين الكيانات التي تشترك في نفس الاسم؛
  • السطر 22: تشير العلامة [@Table(name = "PRODUCTS")] إلى أن فئة [Product] هي تمثيل الكائن لصف في جدول [PRODUCTS] في قاعدة البيانات؛
  • السطر 23: اسم مرشح JSON المراد تطبيقه على الكيان. سنرى أن الخاصية [categorie] في السطر 58 ليست متاحة دائمًا. لذلك يجب استبعادها من تمثيل الكائن في JSON. للقيام بذلك، نحتاج إلى مرشح. لذلك سنستخدم مرشحًا باسم [jsonFilterCategorie] لتحديد ما إذا كنا نريد الخاصية [categorie] أم لا؛
  • السطر 26: تجعل العلامة [@Id] الحقل المُعلَّم هو الحقل المرتبط بالمفتاح الأساسي للجدول في السطر 19؛
  • السطر 27: تحدد علامة [@GeneratedValue(strategy = GenerationType.IDENTITY)] وضع الإنشاء التلقائي للمفتاح الأساسي في جدول [PRODUITS]. وتحدد السمة [strategy] ذلك. هناك أوضاع مختلفة:

Image

استراتيجية [IDENTITY] غير متاحة لجميع أنظمة إدارة قواعد البيانات (DBMS). من بين أنظمة إدارة قواعد البيانات الستة التي تم اختبارها، كانت متاحة لـ [MySQLPostgreSQL 9.4، SQL Server 2014، DB2 Express-C10.5]. بالنسبة لنظامي إدارة قواعد البيانات الآخرين [Oracle Express 11g Release 2 و Firebird 2.5.4]، كان لا بد من استخدام استراتيجية [SEQUENCE]. من أجل قابلية النقل بين تطبيقات JPA، لا ينبغي استخدام استراتيجية [AUTO]، لأنها تترك اختيار استراتيجية إنشاء المفتاح الأساسي لتطبيق JPA. وبالتالي، مع MySQL 5 واستراتيجية [AUTO]:

  • يختار Hibernate استراتيجية [IDENTITY] مع وضع [AUTO_INCREMENT] للمفتاح الأساسي؛
  • يختار EclipseLink استراتيجية [TABLE]، التي تنشئ جدولًا باسم [SEQUENCE] افتراضيًا يجب الاستعلام عنه لاسترداد المفاتيح الأساسية.

في النهاية، بنية قاعدة البيانات التي تديرها هذان التنفيذان لـ JPA ليست هي نفسها. إذا تم إنشاؤها بواسطة Hibernate، فلن تكون قابلة للاستخدام بواسطة EclipseLink، والعكس صحيح.

  • السطر 28: تحدد التعليقة التوضيحية [@Column(name="ID"] اسم العمود في جدول [PRODUCTS] ليتم ربطه بحقل [id]؛
  • السطر 29: يتم استخدام النوع [Long] بدلاً من [long] للمفتاح الأساسي. وذلك لأن المفاتيح الأساسية [null] لها معنى محدد بالنسبة لـ JPA. لذلك، يُفضل استخدام نوع كائن هنا بدلاً من نوع بسيط؛
  • السطر 31: تشير العلامة [@Version] إلى أن حقل [version] مرتبط بعمود الإصدار. سيقوم تطبيق JPA بزيادة رقم الإصدار هذا في كل مرة يتم فيها تعديل الكيان. يُستخدم هذا الرقم لمنع التحديثات المتزامنة للكيان من قبل مستخدمين مختلفين: يقرأ مستخدمان، U1 و U2، الكيان E برقم إصدار يساوي V1. يقوم U1 بتعديل E وحفظ هذا التغيير في قاعدة البيانات: ثم يتغير رقم الإصدار إلى V1+1. يقوم U2 بدوره بتعديل E ومحاولة حفظ هذا التغيير في قاعدة البيانات: سيتلقى استثناءً لأن إصداره (V1) يختلف عن الإصدار الموجود في قاعدة البيانات (V1+1
  • السطر 36: نوع الكيان. سيكون هناك نوعان: POJO و PROXY. بشكل افتراضي، ستكون مثيل Product من نوع POJO (كائن جافا عادي قديم). في بعض الحالات، ستكون مثيلات [Product] المستردة من قاعدة البيانات من النوع [PROXY]. سيكون هذا هو الحال عندما لا يتم تهيئة الخاصية [Category] في السطر 58 بفئة ما بسبب السمة [fetch = FetchType.LAZY] في السطر 56. في هذه الحالة، تختلف تطبيقات JPA المراد اختبارها:
    • [Hibernate، OpenJPA]: يؤدي الوصول إلى فئة منتج من النوع [PROXY] إلى إلقاء استثناء. يستخدم Hibernate مصطلح "proxy" للإشارة إلى مثيل JPA تم الحصول عليه في الوضع [LAZY]. ولهذا السبب استخدمت هذا المصطلح للإشارة إلى هذا النوع من الكيانات؛
    • [EclipseLink]: يؤدي الوصول إلى فئة منتج من النوع [PROXY] إلى إجراء بحث عن تلك الفئة في قاعدة البيانات، ولا يتم إلقاء أي استثناء؛

نظرًا لأنني أردت طبقة اختبار مستقلة عن تطبيق JPA المستخدم، فقد احتجت إلى معرفة نوع كل كيان: POJO أو PROXY. ولهذا السبب أضفت حقل [entityType] إلى كيانات JPA؛

  • السطر 35: تشير العلامة [@Transient] إلى أن تطبيق JPA يجب أن يتجاهل هذا الحقل. وبالفعل، فهو غير موجود في جداول DBMS؛
  • السطر 40: ترمي فئة [Product] استثناء [ProxyException] الذي يتطلب اسم الفئة؛
  • السطر 38: كما في السابق، نشير إلى أن تطبيق JPA يجب أن يتجاهل هذا الحقل؛
  • السطر 39: تشير العلامة [@JsonIgnore] إلى أن أداة التسلسل/إلغاء التسلسل JSON لمثيل [Product] يجب أن تتجاهل هذا الحقل؛
  • السطر 43: تربط علامة [@Column] الحقل [name] بالعمود [NAME] في جدول [PRODUCTS]. عندما يكون للحقل نفس اسم العمود المرتبط به (بغض النظر عن حالة الأحرف)، يمكن حذف علامة [@Column]. وهذا هو الحال هنا. لا تُستخدم السمات [unique = true, length = 30, nullable = false] إلا عندما يقوم تطبيق JPA بإنشاء جدول [CATEGORIES] من كيان [Product]. سيتم ترجمتها إلى سمات SQL [UNIQUE, VARCHAR(30), NOT NULL]، والتي تضمن أن العمود [NAME] سيحتوي على 30 حرفًا كحد أقصى، وسيكون فريدًا في الجدول، ولا يمكن أن يكون له القيمة NULL؛
  • السطران 46-47: الحقل [idCategorie] مرتبط بالعمود [CATEGORIE_ID]. سنعود إلى هذه السمات لاحقًا؛
  • السطران 49-50: الحقل [price] مرتبط بالعمود [PRICE
  • السطران 52-53: الحقل [description] مرتبط بالعمود [DESCRIPTION
  • الأسطر 56-58: فئة المنتج؛
  • السطر 56: تشير التعليقة التوضيحية [@ManyToOne] إلى أن العمود المشار إليه في التعليقة التوضيحية في السطر 57 [@JoinColumn(name = "CATEGORIE_ID")] هو مفتاح خارجي من جدول [PRODUITS] الخاص بالكيان [Product] إلى جدول [CATEGORIES] المرتبط بالكيان في السطر 58. يجب تطبيق هذه التعليقة التوضيحية على كيان JPA. لذلك، يجب أن تكون الفئة في السطر 58 كيان JPA؛
  • السطر 56: تحدد التعليقة [fetch = FetchType.LAZY] أنه عند استرداد منتج من جدول [PRODUCTS]، لا يتم استرداد فئته (السطر 58) على الفور (التحميل المتأخر). ثم يتم استردادها أثناء أول استدعاء لطريقة [getCategory]. ولتحقيق ذلك، تقوم طبقة JPA، في وقت التشغيل، بتحسين الأسلوب الأولي [getCategorie] (الذي يعرض حقل الفئة ببساطة) عن طريق إجراء استدعاء إلى نظام إدارة قواعد البيانات (DBMS) لجلب الفئة — وهي تقنية تُعرف باسم "الوكالة". تختلف تطبيقات JPA في كيفية تعاملها مع هذه الميزة، كما ذكرنا سابقًا. هذه السمة ليست إلزامية. ويُسمح لتطبيق JPA المستخدم بتجاهلها. ونظرًا لأن الخاصية [categorie] قد تكون موجودة أو غير موجودة، فقد أدخلنا مرشح JSON في السطر 23. يتم تحديث عمود الانضمام [CATEGORIE_ID] في جدول [PRODUITS] تلقائيًا عند إدراج منتج أو تحديثه. ويتلقى قيمة [categorie.getId()]، حيث [categorie] هو الحقل الموجود في السطر 58. تتطلب مواصفات JPA ألا يتم تحديث عمود الربط هذا بأي وسيلة أخرى. ولذلك، فإنها تفرض السمتين [insertable = false, updatable = false] في السطر 46، مما يضمن عدم إمكانية تعديل عمود [CATEGORIE_ID] (عمود الربط) المرتبط بحقل [idCategorie] بواسطة حقل [idCategorie]. لن يكون ممكنًا سوى نقل عمود [CATEGORIE_ID] إلى حقل [idCategorie
  • الأسطر 91–104: يتم تعريف المساواة بين كيانات [Product] على أنها المساواة بين مفاتيحها الأساسية [id]؛
  • الأسطر 108-115: لجعل طبقة الاختبار قابلة للنقل، سنقوم بإدارة كيانات [PROXY] بشكل موحد عبر تطبيقات JPA الثلاثة [Hibernate، EclipseLink، OpenJpa]. بالنسبة لكيان [Product] من النوع [PROXY]، سنمنع تغيير قيمة حقل [category]. فئة [ProxyException] هي كما يلي:
  

package generic.jpa.infrastructure;
 
import generic.jdbc.infrastructure.UncheckedException;
 
public class ProxyException extends UncheckedException {
 
    private static final long serialVersionUID = 7278276670314994574L;
 
    public ProxyException() {
    }
 
    public ProxyException(int code, Throwable e, String simpleClassName) {
        super(code, e, simpleClassName);
    }
 
}

في ختام مناقشة هذا الكيان، تجدر الإشارة إلى أن التعليقات التوضيحية وسماتها تُستخدم في حالتين متميزتين:

  • لإنشاء جداول قاعدة البيانات؛
  • للاستعلام عنها. في هذه الحالة، يتوقع تطبيق JPA العثور على الجداول تمامًا كما كان سيقوم بإنشائها بنفسه. لذلك، لا يمكننا ربط أي جدول [PRODUCTS] بالكيان [Product] السابق. يجب أن يحتوي على الأقل (قد يحتوي على خصائص أخرى) على خصائص جدول [PRODUCTS] الذي كان سيقوم JPA بإنشائه. عند العمل مع JPA، فإن النهج المثالي هو البدء بقاعدة بيانات فارغة يقوم JPA بإنشاء الجداول فيها. سنناقش هذا الإنشاء لاحقًا. تم إنشاء البرنامج النصي SQL المقدم لنظام إدارة قواعد البيانات MySQL من الجداول التي أنشأها JPA.

تُستخدم جميع سمات كيان [Product] لإنشاء جدول [PRODUCTS]. وبمجرد الانتهاء من ذلك، لا تُستخدم سمات الإنشاء مثل [unique = true, length = 30, nullable = false] عند الاستعلام عن الجداول.

6.3.3.3. كيان JPA [Category]

فئة [Category] هي كيان JPA مرتبط بصف في جدول [CATEGORIES]:

وإليك كودها:


package generic.jpa.entities.dbproduitscategories;

import generic.jdbc.config.ConfigJdbc;
import generic.jpa.infrastructure.ProxyException;
 
import java.util.ArrayList;
import java.util.List;
 
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
 
import com.fasterxml.jackson.annotation.JsonFilter;
import com.fasterxml.jackson.annotation.JsonIgnore;
 
@Entity
@Table(name = ConfigJdbc.TAB_CATEGORIES)
@JsonFilter("jsonFilterCategorie")
public class Categorie implements AbstractCoreEntity {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = ConfigJdbc.TAB_JPA_ID)
    protected Long id;
 
    @Version
    @Column(name = ConfigJdbc.TAB_JPA_VERSIONING)
    protected Long version;
 
    @Transient
    protected EntityType entityType = EntityType.POJO;
 
    @Transient
    @JsonIgnore
    protected String simpleClassName = getClass().getSimpleName();
 
    // properties
    @Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
    private String nom;
 
    // related products
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    private List<Produit> produits;
 
    // manufacturers
    public Categorie() {
 
    }
 
    public Categorie(Long id, Long version, String nom, List<Produit> produits) {
        this.id = id;
        this.version = version;
        this.nom = nom;
        this.produits = produits;
    }
 
    // signature
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }
 
    // methods
    public void addProduit(Produit produit) {
        // entity type
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1004, new RuntimeException(
                    "On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
        }
        // add a product
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // we add the product
            produits.add(produit);
            // set your category
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }
 
    // ------------------------------------------------------------
    // redefine [equals] and [hashcode]
    @Override
    public int hashCode() {
        Long id = getId();
        return (id != null ? id.hashCode() : 0);
    }
 
    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractCoreEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractCoreEntity other = (AbstractCoreEntity) entity;
        Long id = getId();
        Long otherId = other.getId();
        return id != null && otherId != null && id.equals(otherId);
    }
 
    // getters and setters
...
}
  • السطر 24: الفئة هي كيان JPA؛
  • السطر 25: مرتبطة بجدول [CATEGORIES
  • السطر 26: يتم التحكم في تمثيل JSON لكيان [Category] بواسطة المرشح المسمى [jsonFilterCategory]. يجب تكوين هذا قبل أي طلب لتمثيل JSON للكيان. سيتم استخدام مرشح [jsonFilterCategorie] لتحديد ما إذا كان سيتم تضمين حقل [products] من السطر 40 في تمثيل JSON لكيان [Categorie] أم لا؛
  • الأسطر 29-32: الحقل [id] مرتبط بالمفتاح الأساسي [ID] لجدول [CATEGORIES]. وضع التوليد المحدد هو [IDENTITY]، والذي يتوافق مع [AUTO_INCREMENT] في MySQL؛
  • الأسطر 34-36: الحقل [version] مرتبط بعمود [VERSIONING] في جدول [CATEGORIES
  • السطور 38-39: نوع كيان [Categorie
  • الأسطر 41–43: الاسم المختصر لفئة [Categorie
  • السطران 46-47: حقل [name] مرتبط بعمود [NAME] في جدول [CATEGORIES]. نقوم بتعيين سمات JPA [unique = true, length = 30, nullable = false] له بحيث عندما يتم إنشاء جدول [CATEGORIES]، يكون للعمود [NAME] سمات SQL [UNIQUE, VARCHAR(30), NOT NULL
  • السطران 50-51: المنتجات التي تنتمي إلى الفئة؛
  • السطر 50: التعليق التوضيحي [@OneToMany] هو العلاقة العكسية للعلاقة [@ManyToOne] التي صادفناها في الكيان [Product]. تحدد السمة [mappedBy = "category"] الحقل في الكيان [Product] المُعلَّم بالعلاقة العكسية [@ManyToOne]. تحدد السمة [cascade = { CascadeType.ALL }] أن العمليات (persist، merge، remove) التي يتم إجراؤها على @Entity [Category] يجب أن تتسلسل إلى [products] في السطر 51. يمكن تحديد التسلسلات الجزئية باستخدام الثوابت [CascadeType.PERSIST، CascadeType.MERGE، CascadeType.REMOVE
  • السطر 50: تحدد السمة [fetch = FetchType.LAZY] أنه عند استرداد فئة من جدول [CATEGORIES]، لا يتم استرداد منتجاتها على الفور. يتم استردادها أثناء أول استدعاء لطريقة [getProduits]. لتحقيق ذلك، في وقت التشغيل، تعزز طبقة JPA الأسلوب [getProduits] الأولي (الذي يعرض ببساطة حقل المنتجات) عن طريق إجراء استدعاء إلى نظام إدارة قواعد البيانات (DBMS) لجلب المنتجات الخاصة بالفئة. هذه السمة إلزامية. لا يمكن لتطبيق JPA تجاهلها. نظرًا لأن الخاصية [products] قد يتم تهيئتها أو لا يتم تهيئتها، فقد أدخلنا مرشح JSON في السطر 26، والذي يسمح لنا بتحديد ما إذا كنا نريد هذه الخاصية ونوع الكيان في السطر 39 أم لا؛
  • الأسطر 71-88: تسمح طريقة [addProduct] بإضافة منتج إلى الفئة؛
  • الأسطر 73-76: لتوحيد معالجة الوكيل عبر تطبيقات JPA المختلفة، قررنا أنه لا يمكن إضافة منتجات إلى كيان [Category] من النوع PROXY؛
  • الأسطر 92–112: تعتبر كيانات [Category] متساوية إذا كان لها نفس المفتاح الأساسي [id]؛

6.3.4. ملف [persistence.xml]

  

يجب أن تحدد تطبيقات JPA خصائص معينة لمزود JPA المستخدم، بالإضافة إلى كيانات JPA التي سيتم استخدامها، في ملف [META-INF/persistence.xml] الموجود في مسار فئات التطبيق. أعلاه، تم وضعه في المجلد [src/main/resources]، والذي يعد فعليًا جزءًا من مسار فئات مشروع Eclipse. عند استخدام JPA بالاقتران مع Spring، يتم وضع بعض المعلومات التي يجب أن تكون في ملف [persistence.xml] في مكان آخر في فئات تكوين Spring. في تطبيق Spring JPA، يقوم Spring بتشغيل JPA. مع Spring JPA Hibernate، يمكن اختزال ملف [persistence.xml] إلى أبسط صوره:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="dummy-persistence-unit" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • الأسطر 1-5: يجب أن يحتوي ملف [persistence.xml] على علامة <persistence> جذرية. لن يتم استخدام سمات العلامة الموجودة في السطر 2 في هذا التطبيق؛
  • يمكن لملف الاستمرارية تعريف وحدة استمرارية واحدة أو أكثر باستخدام علامة <persistence-unit> (السطر 4). تدير وحدة الاستمرارية الوصول إلى قاعدة بيانات محددة. إذا كان التطبيق يدير قاعدتي بيانات في وقت واحد، فسيكون له وحدتا استمرارية؛
  • السطر 4: تحتوي وحدة الاستمرارية على اسم [سمة name]، وتدعم نوع المعاملة [سمة transaction-type]، ولها خصائص، وتحدد الكيانات المرتبطة بجداول قاعدة البيانات التي تديرها وحدة الاستمرارية. هنا، نظرًا لأن الوصول إلى قاعدة البيانات سيتم إدارته بواسطة [Spring JPA Hibernate]، يمكن وضع هاتين المعلومتين الأخيرتين في مكان آخر. هناك نوعان من المعاملات:
    • [RESOURCE_LOCAL]: تتم إدارة المعاملات بواسطة التطبيق نفسه. وهذا هو الحال هنا، حيث ستدير Spring المعاملات؛
    • [JTA] (واجهة برمجة تطبيقات المعاملات في جافا): يقوم حاوية EJB (Enterprise Java Bean) التي تشغل التطبيق بإدارة المعاملات تلقائيًا استنادًا إلى تعليقات جافا الموجودة في الكود. ونحن لا نستخدم هذا الإعداد هنا؛

سنرى لاحقًا أن محتويات ملف [persistence.xml] هذا تعتمد على تطبيق JPA المستخدم.

6.4. مشروع [spring-jpa-generic]

دعونا نلخص ما نريد القيام به. نريد تنفيذ البنية التالية:

حيث ستقوم طبقة [DAO] بتنفيذ واجهة [IDao<ProductIDao<Category>] التي درسناها في الفصل 4. الهدف هو مقارنة تنفيذين لهذه الواجهة:

  • أحدهما تم بناؤه باستخدام Spring JDBC؛
  • والآخر تم إنشاؤه باستخدام Spring JPA؛

في البنية أعلاه:

  • يتم تنفيذ طبقة [JDBC] بواسطة مشروع [mysql-config-jdbc] الذي تمت مناقشته في القسم 3.3؛
  • يتم تنفيذ طبقة [JPA] بواسطة مشروع [mysql-config-jpa-hibernate] الذي تمت مناقشته في القسم 6.3؛

يتولى مشروع [spring-jpa-generic] تنفيذ طبقات [DAO] و[Spring Data].

  

6.4.1. تكوين Maven

مشروع [spring-jpa-generic] هو مشروع Maven تم تكوينه بواسطة ملف [pom.xml] التالي:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
 
    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jpa-generic</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jpa-generic</name>
    <description>démo spring data avec tables de catégories et de produits</description>
 
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>
 
    <dependencies>
        <!-- configuration JPA of SGBD -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jpa</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
 
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.7</java.version>
    </properties>
 
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>
 
</project>
  • الأسطر 22–26: يحتوي المشروع على تبعيات واحدة فقط، وهي المشروع الذي يقوم بتكوين طبقة [JPA] للتطبيق، والتي قمنا بفحصها للتو. هذا تطبيق عام:
    • نقوم بتغيير نظام إدارة قواعد البيانات (DBMS) عن طريق تغيير مشروع تكوين طبقة [JDBC
    • نقوم بتغيير تنفيذ JPA عن طريق تغيير مشروع تكوين طبقة [JPA

في النهاية، تكون التبعيات كما يلي:

  

6.4.2. تكوين Spring

  

تقوم فئة [AppConfig] بتكوين مشروع Spring:


package spring.data.config;
 
import generic.jpa.config.ConfigJpa;
 
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
 
@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
@Import({ ConfigJpa.class })
public class AppConfig {
 
}
  • السطر 11: الفئة هي فئة تكوين Spring؛
  • السطر 10: تُستخدم العلامة [@EnableJpaRepositories] لتعيين الحزم التي تحتوي على واجهات [CrudRepository] الخاصة بـ Spring Data. وهذا يجعلها مكونات Spring يمكن حقنها في مكونات Spring أخرى؛
  • السطر 12: تشير العلامة [@ComponentScan] إلى أنه يجب فحص حزمة [spring.data.dao] بحثًا عن مكونات Spring. سيتم العثور على مكونات [DaoCategory] و [DaoProduct
  • السطر 13: يتم استيراد الحبوب من فئة التكوين [ConfigJpa]. وتشمل هذه الحبوب الحبة الخاصة بتنفيذ JPA المستخدم (Hibernate، Eclipselink، OpenJpa)، ومصدر البيانات الذي سيتم استخدامه، و EntityManager الذي سيتولى عمليات JPA، ومدير المعاملات؛

6.4.3. طبقة [Spring Data]

  

6.4.3.1. واجهة [CategoriesRepository]

تدير واجهة [CategoriesRepository] الوصول إلى جدول [CATEGORIES]:


package spring.data.repositories;
 
import generic.jpa.entities.dbproduitscategories.Categorie;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
 
public interface CategoriesRepository extends CrudRepository<Categorie, Long> {
 
    // categorie avec ses produits
    @Query("select c from Categorie c left join fetch c.produits where c.id=?1")
    public Categorie getLongCategorieById(Long id);
 
    @Query("select c from Categorie c left join fetch c.produits where c.nom=?1")
    public Categorie getLongCategorieByName(String nom);
 
    @Query("select c from Categorie c where c.nom in ?1")
    public List<Categorie> getShortCategoriesByName(Iterable<String> names);
 
    @Query("select c from Categorie c where c.id in ?1")
    public List<Categorie> getShortCategoriesById(Iterable<Long> ids);
 
    @Query("select distinct c from Categorie c left join fetch c.produits where c.id in ?1")
    public List<Categorie> getLongCategoriesById(List<Long> names);

    @Query("select distinct c from Categorie c left join fetch c.produits where c.nom in ?1")
    public List<Categorie> getLongCategoriesByName(List<String> names);
 
    @Query("select c from Categorie c")
    public List<Categorie> getAllShortCategories();
 
    @Query("select distinct c from Categorie c left join fetch c.produits")
    public List<Categorie> getAllLongCategories();
 
}
  • السطر 10: تم استخدام واجهة [CrudRepository] وشرحها في القسم 5.1.3. للتذكير:
    • نوع المعلمة الأولى للواجهة هو كيان JPA الذي تتم إدارته لعمليات CRUD (findOne، findAll، save، delete، deleteAll
    • نوع المعلمة الثانية للواجهة هو المفتاح الأساسي لكيان JPA، وهو هنا عدد صحيح [Long

يتم تنفيذ أساليب الواجهة باستخدام استعلامات JPQL (لغة استعلامات الاستمرارية في Java). وتستهدف هذه الاستعلامات كيانات JPA. وفي مثل هذا الاستعلام:

  • يتم استبدال الجداول بكيانات JPA المرتبطة بها؛
  • يتم استبدال الأعمدة بحقول كيانات JPA المستخدمة في الاستعلام؛

لنأخذ مثال السطرين 31-32: تسترد الطريقة في السطر 32 جميع الفئات من قاعدة البيانات في صيغتها المختصرة. ويتم تنفيذها بواسطة استعلام JPQL (لغة استعلامات استمرارية Java) في السطر 31، والذي يشبه إلى حد كبير نظيره في SQL. لفهم أعمق لـ JPQL، انظر [ref2] (انظر القسم 1.2).

طرق واجهة [CategoriesRepository] هي كما يلي:

  • السطران 13-14: تُرجع طريقة [getLongCategoryById] النسخة الطويلة لفئة يُشار إليها بواسطة مفتاحها الأساسي [id]، أي الفئة مع منتجاتها. تذكر أنه في كيان [Category]، كان الحقل [products] يحتوي على السمة [fetch = FetchType.LAZY] (التحميل المتأخر). في استعلام JPQL، نفرض تحميل المنتجات باستخدام الكلمة الرئيسية [fetch]. سيتم استبدال المعلمة ?1 في الاستعلام في وقت التشغيل بقيمة المعلمة الأولى للطريقة في السطر 12، أي المعلمة [Long id
  • السطران 16-17: تُرجع الطريقة [getLongCategoryByName] النسخة الطويلة من الفئة المشار إليها باسمها [name
  • السطران 19-20: تُرجع الطريقة [getShortCategoriesByName] النسخ المختصرة للفئات المشار إليها بأسمائها. حقل [products] لهذه الفئات ليس فارغًا. فهو يحتوي على مرجع إلى وكيل (فئة تم إنشاؤها بواسطة تطبيق JPA) يتمثل دوره في استرداد المنتجات الموجودة في الفئة عند استدعائه. يؤدي استدعاؤه خارج سياق ثبات JPA إلى إلقاء استثناء (Hibernate و OpenJPA، ولكن ليس EclipseLink). ولهذا السبب، لن نستخدم حقل [products] الخاص بالنسخة المختصرة للفئة؛
  • السطران 22-23: تُرجع الطريقة [getShortCategoriesById] النسخ المختصرة للفئات المشار إليها بواسطة مفاتيحها الأساسية [id]؛
  • السطران 25-26: تُرجع الطريقة [getLongCategoriesById] النسخ الطويلة للفئات المشار إليها بواسطة مفاتيحها الأساسية [id]؛
  • السطران [28-29]: تُرجع الطريقة [getLongCategoriesByName] النسخ الطويلة للفئات المشار إليها بأسمائها؛
  • السطران 31-32: تعرض طريقة [getAllShortCategories] النسخ المختصرة لجميع الفئات؛
  • السطران 34-35: تُرجع الطريقة [getAllLongCategories] النسخ الطويلة لجميع الفئات؛

ملاحظة: لا تدعم جميع تطبيقات JPA نفس صيغة JPQL. وبالتالي، فإن الصيغة التالية مقبولة من قبل Hibernate و EclipseLink ولكنها غير مقبولة من قبل OpenJpa:


@Query("select c from Categorie c left join fetch c.produits p where c.nom=?1")

لا يقبل OpenJpa الاسم المستعار [p] أعلاه.

6.4.3.2. واجهة [ProductsRepository]

تدير واجهة [ProductsRepository] الوصول إلى جدول [PRODUCTS]:


package spring.data.repositories;
 
import generic.jpa.entities.dbproduitscategories.Produit;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.transaction.annotation.Transactional;
 
@Transactional()
public interface ProduitsRepository extends CrudRepository<Produit, Long> {
 
    // un produit avec sa catégorie
    @Query("select p from Produit p left join fetch p.categorie where p.id=?1")
    public Produit getLongProduitById(Long id);
 
    @Query("select p from Produit p left join fetch p.categorie where p.nom=?1")
    public Produit getLongProduitByName(String nom);
 
    @Query("select p from Produit p where p.id in ?1")
    public List<Produit> getShortProduitsById(List<Long> ids);
 
    @Query("select p from Produit p where p.nom in ?1")
    public List<Produit> getShortProduitsByName(List<String> names);
 
    @Query("select distinct p from Produit p left join fetch p.categorie where p.id in ?1")
    public List<Produit> getLongProduitsById(List<Long> ids);
 
    @Query("select distinct p from Produit p left join fetch p.categorie where p.nom in ?1")
    public List<Produit> getLongProduitsByName(List<String> names);
 
    @Query("select distinct p from Produit p left join fetch p.categorie")
    public List<Produit> getAllLongProduits();
 
    @Query("select p from Produit p")
    public List<Produit> getAllShortProduits();
}
  • السطران [15-16]: تُرجع طريقة [getLongProductById] النسخة الطويلة من المنتج المحدد بواسطة مفتاحه الأساسي [id]، بما في ذلك فئته. تذكر أنه في كيان [Product]، كان الحقل [category] يحتوي على السمة [fetch = FetchType.LAZY] (التحميل المتأخر). في استعلام JPQL، نجبر التحميل الفوري للفئة باستخدام الكلمة الرئيسية [fetch
  • السطران 18-19: تُرجع الطريقة [getLongProductByName] النسخة الطويلة من المنتج المحدد باسمه؛
  • السطران 21-22: تُرجع الطريقة [getShortProduitsById] النسخة المختصرة من المنتجات التي يتم تحديدها بواسطة مفتاحها الأساسي [id]. في هذه النسخة المختصرة، لا يكون حقل [category] فارغًا. فهو يحتوي على مرجع إلى وكيل تم إنشاؤه بواسطة تطبيق JPA، والذي، إذا تم استدعاؤه، سيقوم بجلب فئة المنتج. لا يمكن إجراء هذا الاستدعاء إلا ضمن سياق ثبات JPA. ويؤدي إجراؤه في مكان آخر إلى حدوث استثناء (Hibernate و OpenJPA، ولكن ليس EclipseLink). لذلك، في طبقة [DAO] أو في أي مكان آخر، لن نستخدم حقل [category] للمنتج في نسخته المختصرة. في النسخة المختصرة للمنتج، يتم تهيئة حقل [idCategorie]. قيمته هي المفتاح الأساسي للفئة التي ينتمي إليها المنتج. وهذا يسمح لنا لاحقًا باسترداد هذه الفئة من طبقة [DAO] عبر الطريقة [DaoCategorie.getShortCategoriesById(idCategorie)]؛
  • السطران 24-25: تُرجع الطريقة [getShortProduitsByName] النسخة المختصرة للمنتجات المحددة بأسمائها؛
  • السطران 27-28: تُرجع الطريقة [getLongProduitsById] النسخة الطويلة من المنتجات المحددة بمفاتيحها الأساسية؛
  • السطران 30-31: تعرض طريقة [getLongProductsByName] النسخة الطويلة من المنتجات المحددة بأسمائها؛
  • السطران 33-34: تعرض طريقة [getAllLongProducts] النسخة الطويلة لجميع المنتجات؛
  • السطران 36-37: تعرض طريقة [getAllShortProducts] النسخة المختصرة لجميع المنتجات؛

سيتم تنفيذ هذه الواجهات بواسطة فئات تم إنشاؤها بواسطة تنفيذ JPA في وقت التشغيل. تسمى هذه الفئات فئات [proxy]. بشكل افتراضي، يتم تنفيذ طرق واجهة [CrudRepository] ضمن معاملة. حقيقة أن واجهتي [ProductsRepository] و [CategoriesRepository] تمتدان من فئة [CrudRepository] تجعلهما مكونات Spring. وبالتالي، يمكن حقنهما في مكونات Spring أخرى.

6.4.4. طبقة [DAO]

  

6.4.4.1. واجهة [IDao<T>]

واجهة [IDao<T>] هي تلك التي تمت مناقشتها سابقًا في سياق تنفيذ طبقة [DAO] باستخدام Spring JDBC (انظر القسم 4.7


package spring.data.dao;
 
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity;
 
import java.util.List;
 
public interface IDao<T extends AbstractCoreEntity> {
 
    // list of all T entities
    public List<T> getAllShortEntities();
 
    public List<T> getAllLongEntities();
 
    // special entities - short version
    public List<T> getShortEntitiesById(Iterable<Long> ids);
 
    public List<T> getShortEntitiesById(Long... ids);
 
    public List<T> getShortEntitiesByName(Iterable<String> names);
 
    public List<T> getShortEntitiesByName(String... names);
 
    // special entities - long version
    public List<T> getLongEntitiesById(Iterable<Long> ids);
 
    public List<T> getLongEntitiesById(Long... ids);
 
    public List<T> getLongEntitiesByName(Iterable<String> names);
 
    public List<T> getLongEntitiesByName(String... names);
 
    // update of several entities
    public List<T> saveEntities(Iterable<T> entities);
 
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);
 
    // delete all entities
    public void deleteAllEntities();
 
    // deletion of multiple entities
    public void deleteEntitiesById(Iterable<Long> ids);
 
    public void deleteEntitiesById(Long... ids);
 
    public void deleteEntitiesByName(Iterable<String> names);
 
    public void deleteEntitiesByName(String... names);
 
    public void deleteEntitiesByEntity(Iterable<T> entities);
 
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}

6.4.4.2. الفئة المجردة [AbstractDao]

  

الفئة المجردة [AbstractDao] هي الفئة الأم للفئات التي تنفذ طبقة [DAO]:

  • فئة [DaoProduit]، التي تنفذ واجهة [IDao<Produit>] وتدير الوصول إلى جدول [PRODUITS
  • فئة [DaoCategorie]، التي تنفذ واجهة [IDao<Categorie>] وتدير الوصول إلى جدول [CATEGORIES

يكون كودها كما هو موصوف في القسم 4.8، مع الاختلاف الطفيف التالي: لا تحتوي أي طريقة على السمة [@Transactional]، التي تجعل الطريقة تُنفذ ضمن معاملة. هنا، نستفيد من حقيقة أن واجهات [CrudRepository] في Spring Data تُنفذ ضمن معاملة بشكل افتراضي.

6.4.4.3. فئة [DaoCategorie]

  

تنفذ فئة [DaoCategorie] واجهة [IDao<Categorie>] على النحو التالي:


package spring.data.dao;
 
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
 
import java.util.ArrayList;
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
 
@Component
public class DaoCategorie extends AbstractDao<Categorie> {
 
    @Autowired
    private ProduitsRepository produitsRepository;
 
    @Autowired
    private CategoriesRepository categoriesRepository;
 
    @Override
    public List<Categorie> getAllShortEntities() {
        try {
            return setShortCategoriesType(categoriesRepository.getAllShortCategories());
        } catch (Exception e) {
            throw new DaoException(211, e, simpleClassName);
        }
    }
 
    private List<Categorie> setShortCategoriesType(List<Categorie> categories) {
        for (Categorie categorie : categories) {
            categorie.setEntityType(EntityType.PROXY);
        }
        return categories;
    }
 
    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return categoriesRepository.getAllLongCategories();
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
    }
 
    @Override
    public void deleteAllEntities() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(208, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesById(ids));
        } catch (Exception e) {
            throw new DaoException(203, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Categorie> getShortEntitiesByName(List<String> names) {
        try {
            return setShortCategoriesType(categoriesRepository.getShortCategoriesByName(names));
        } catch (Exception e) {
            throw new DaoException(204, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Categorie> getLongEntitiesById(List<Long> ids) {
        try {
            return categoriesRepository.getLongCategoriesById(ids);
        } catch (Exception e) {
            throw new DaoException(205, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Categorie> getLongEntitiesByName(List<String> names) {
        try {
            return categoriesRepository.getLongCategoriesByName(names);
        } catch (Exception e) {
            throw new DaoException(206, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
    ...
    }
 
    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            categoriesRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(209, e, simpleClassName);
        }
    }
 
    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            categoriesRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(212, e, simpleClassName);
        }
    }
 
}
  • السطر 17: تجعل العلامة [@Component] فئة [DaoCategorie] مكونًا في Spring؛
  • السطر 18: تمتد فئة [DaoCategorie] إلى فئة [AbstractDao<Categorie>]، مما يعني أنها تنفذ واجهة [IDao<Categorie>]؛
  • الأسطر 20-24: حقن المراجع إلى واجهتي [CrudRepository] من [Spring Data]. يحدث هذا الحقن عند إنشاء مثيلات كائنات Spring، عادةً في بداية تنفيذ مشروع Spring؛
  • تفوض جميع طرق الفئة العمل إلى الطرق التي تحمل الأسماء نفسها في واجهات [CrudRepository
  • تشير جميع الطرق التي تُرجع كيانات في صيغتها المختصرة إلى ذلك عن طريق تعيين نوع الكيان إلى [EntityType.PROXY] (الأسطر 29 و63 و72)؛

تستدعي طريقة [saveEntities] توضيحًا:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // on note les produits qui vont être insérés
        List<Produit> insertedProduits = new ArrayList<Produit>();
        for (Categorie categorie : categories) {
            EntityType categorieType = categorie.getEntityType();
            List<Produit> produits = null;
            if ((categorieType == EntityType.POJO) && (produits = categorie.getProduits()) != null) {
                for (Produit produit : produits) {
                    if (produit.getId() == null) {
                        insertedProduits.add(produit);
                    }
                    // on en profite pour rétablir (si besoin est) la relation produit --> categorie
                    produit.setCategorie(categorie);
                }
            }
        }
        // on persiste les catégories / produits
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // on met à jour le champ [idCategorie] des produits insérés
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        // résultat
        return categories;
    }
  • السطر 2: الفئات التي تم تمريرها كمعلمات هي فئات سيتم إدراجها [id==null] وفئات سيتم تحديثها [id!=null
  • السطر 20: نقوم بحفظ الفئات باستخدام الطريقة [categoriesRepository.save(entities)]. أثناء الاختبار، نلاحظ أن حقل [idCategorie] للمنتجات المحفوظة (id==null) غير مملوء. لحل هذه المشكلة، نسجل في الأسطر 4–17 المنتجات المراد إدراجها، وبمجرد حفظها، نملأ حقل [idCategorie] الخاص بها (الأسطر 25–27)؛
  • الأسطر 5-17: نكرر عبر قائمة الفئات؛
  • الأسطر 8–16: لكل فئة، نكرر عبر قائمة منتجاتها. وهنا تكمن الصعوبة. تُستخدم طريقة [saveEntities] لكل من حفظ الفئة وتعديلها. في الحالة الأخيرة، قد تكون الفئة قد تم استردادها في نسختها المختصرة، وبالتالي تحتوي على مرجع إلى طريقة وكيل في حقل [products]. يؤدي استخدامها مع Hibernate إلى حدوث استثناء، لأن الفئة المستخدمة لم تعد موجودة في سياق استمرارية JPA، الذي تم إغلاقه في نهاية المعاملة الخاصة بالطريقة التي استردت النسخ المختصرة للفئات. ثم نستخدم حقل [EntityType] للكيان [Category] في السطر 8 لتحديد ما إذا كان بإمكاننا الوصول إلى قائمة منتجات الفئة أم لا؛
  • السطر 14: نربط المنتج بفئته. عادةً، يجب أن يكون هذا هو الحال بالفعل. لكننا لا نعرف كيف تم إنشاء هذا المنتج أو ما إذا كان قد تم ربطه بفئته. لذلك، لتجنب أي مشكلات في "التسلسل الخاطئ" ( ) (لإدارة كيان [Product]، يتطلب JPA أن يشير إلى كيان [Category] المرتبط به)، نقوم بإنشاء هذا الارتباط بأنفسنا.

بمقارنة هذا الكود بكود فئة [DaoProduit] في تطبيق Spring JDBC (انظر القسم 4.9)، يمكننا أن نرى أن مكتبة Spring Data JPA تبسط بشكل كبير كتابة طبقة [DAO].

6.4.4.4. فئة [ProductDao]

  

تنفذ فئة [DaoProduct] واجهة [IDao<Product>] على النحو التالي:


package spring.data.dao;
 
import generic.jpa.entities.dbproduitscategories.AbstractCoreEntity.EntityType;
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
 
import java.util.List;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
 
import spring.data.infrastructure.DaoException;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;
 
import com.google.common.collect.Lists;
 
@Component
public class DaoProduit extends AbstractDao<Produit> {
    @Autowired
    private ProduitsRepository produitsRepository;
 
    @Autowired
    private CategoriesRepository categoriesRepository;
 
    @Override
    public List<Produit> getAllShortEntities() {
        try {
            return setShortProduitsType(produitsRepository.getAllShortProduits());
        } catch (Exception e) {
            throw new DaoException(102, e, simpleClassName);
        }
    }

    private List<Produit> setShortProduitsType(List<Produit> produits) {
        for (Produit produit : produits) {
            produit.setEntityType(EntityType.PROXY);
        }
        return produits;
    }
 
    @Override
    public List<Produit> getAllLongEntities() {
        try {
            return produitsRepository.getAllLongProduits();
        } catch (Exception e) {
            throw new DaoException(117, e, simpleClassName);
        }
    }
 
    @Override
    public void deleteAllEntities() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(112, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Produit> getShortEntitiesById(List<Long> ids) {
        try {
            return setShortProduitsType(produitsRepository.getShortProduitsById(ids));
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Produit> getShortEntitiesByName(List<String> names) {
        try {
            return setShortProduitsType(produitsRepository.getShortProduitsByName(names));
        } catch (Exception e) {
            throw new DaoException(104, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Produit> getLongEntitiesById(List<Long> ids) {
        try {
            return linkLongProduitsToCategories(produitsRepository.getLongProduitsById(ids));
        } catch (Exception e) {
            throw new DaoException(105, e, simpleClassName);
        }
    }
 
    @Override
    protected List<Produit> getLongEntitiesByName(List<String> names) {
        try {
            return linkLongProduitsToCategories(produitsRepository.getLongProduitsByName(names));
        } catch (Exception e) {
            throw new DaoException(106, e, simpleClassName);
        }
    }
 
    private List<Produit> linkLongProduitsToCategories(List<Produit> produits) {
        for (Produit produit : produits) {
            Categorie categorie = produit.getCategorie();
            if (categorie != null) {
                produit.setCategorie(categorie);
                produit.setIdCategorie(categorie.getId());
            }
        }
        return produits;
    }
 
    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        // re-establish (if necessary) the link between a product and its category
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        // we persist products
        try {
            return Lists.newArrayList(produitsRepository.save(entities));
        } catch (Exception e) {
            throw new DaoException(111, e, simpleClassName);
        }
    }
 
    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            produitsRepository.delete(getShortEntitiesById(ids));
        } catch (Exception e) {
            throw new DaoException(113, e, simpleClassName);
        }
    }
 
    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            produitsRepository.delete(getShortEntitiesByName(names));
        } catch (Exception e) {
            throw new DaoException(118, e, simpleClassName);
        }
    }
 
}

يشبه هذا الكود كود فئة [DaoCategorie]:

  • بالنسبة للإصدارات الطويلة من الفئات، تظهر الاختبارات أن حقل [idCategorie] للمنتجات غير مملوء. تعمل الطريقة [linkLongProduitsToCategories] في الأسطر 96–105 على حل هذه المشكلة؛
  • تقوم طريقة [saveEntities] في الأسطر 108–121 بإدراج منتجات جديدة أو تعديل المنتجات الموجودة. تتطلب طبقة JPA أن يتم ربط كل كيان [Product] بكيان [Category]. وبما أننا لا نعرف ما إذا كان المستخدم قد قام بذلك، فإننا نقوم بذلك بأنفسنا في الأسطر 110–113. كل ما نحتاج إلى فعله هو ربط [Product] بكيان [Category] الذي يتطابق مفتاحه الأساسي مع حقل [idCategory] في [Product]. أثناء الاختبار، نجد أن خطأً يحدث إذا قمنا بتعيين إصدار الفئة على null. لذا، نقوم هنا بتعيينه على 0، ولكن يمكننا تعيينه على أي قيمة نريدها. وبصرف النظر عن المفتاح الأساسي، لا تتطلب طبقة JPA أي حقول من كيان [Category] لإدراج كيان [Product] أو تحديثه؛

6.4.5. طبقة الاختبار

  

الاختبارات المذكورة أعلاه مطابقة لتلك الموجودة في تطبيق Spring JDBC. راجع الصفحات التالية إذا لزم الأمر:

نستخدم تكوينات الاختبار التالية:

فيما يلي النتائج التي تم الحصول عليها من الاختبارات المختلفة:

في [1]، اختبار [JUnitTestPushTheLimits] مع تطبيق Spring Data JPA Hibernate، وفي [2]، مع تطبيق Spring JDBC. يمكننا ملاحظة أن الأخير يقدم أداءً أفضل. وبالتالي، نصل إلى استنتاج أولي: من الأسهل بكثير تطوير طبقة [DAO] باستخدام Spring Data JPA، لكنها أقل أداءً من تطبيق Spring JDBC.

اختبار [JUnitTestProxies] هو اختبار JUnit وهمي. وهو موجود لتوضيح كيفية تصرف كل تطبيق JPA عند التعامل مع الوكلاء، أي النسخ المختصرة للكيانات:


package spring.data.tests;
 
import generic.jpa.entities.dbproduitscategories.Categorie;
import generic.jpa.entities.dbproduitscategories.Produit;
 
import java.util.ArrayList;
import java.util.List;
 
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
import spring.data.config.AppConfig;
import spring.data.dao.IDao;
 
import com.google.common.collect.Lists;
 
@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class JUnitTestProxies {
 
    // layer [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;
 
    @Before
    public void clean() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // we empty table [CATEGORIES] and cascade table [PRODUITS]
        daoCategorie.deleteAllEntities();
    }
 
    @Test
    public void doNothing() {
        System.out.println("doNothing");
    }
 
    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // fill the tables
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
            categorie.setProduits(new ArrayList<Produit>());
            for (int j = 0; j < nbProduits; j++) {
                Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
                        100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
                categorie.addProduit(produit);
            }
            categories.add(categorie);
        }
        // adding the category - by cascading the products will also be
        // inserted
        daoCategorie.saveEntities(categories);
        // result
        return categories;
    }
 
    @Test
    public void getShortCategoriesByName1() {
        // filling
        fill(1, 1);
        // test
        log("getShortCategoriesByName1", 1);
        Categorie categorie = daoCategorie.getShortEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
        System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
        System.out.println("Catégorie :");
        try {
            System.out.println(categorie.getProduits().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }
 
    @Test
    public void getShortProduitsByName1() {
        // filling
        fill(1, 1);
        // test
        log("getShortProduitsByName1", 1);
        Produit produit = daoProduit.getShortEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
        System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
        System.out.println("Nom de la catégorie du produit :");
        try {
            System.out.println(produit.getCategorie().getNom());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }
 
    @Test
    public void getLongCategoriesByName1() {
        // filling
        fill(1, 1);
        // test
        log("getLongCategoriesByName1", 1);
        Categorie categorie = daoCategorie.getLongEntitiesByName(Lists.newArrayList("categorie[0]")).get(0);
        System.out.println(String.format("Catégorie de type : %s", categorie.getEntityType()));
        System.out.println("Catégorie :");
        try {
            System.out.println(categorie.getProduits().size());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }
 
    @Test
    public void getLongProduitsByName1() {
        // filling
        fill(1, 1);
        // test
        log("getLongProduitsByName1", 1);
        Produit produit = daoProduit.getLongEntitiesByName(Lists.newArrayList("produit[0,0]")).get(0);
        System.out.println(String.format("Produit de type : %s", produit.getEntityType()));
        System.out.println("Nom de la catégorie du produit :");
        try {
            System.out.println(produit.getCategorie().getNom());
        } catch (Exception e) {
            System.err.println(String.format("Exception : %s, Message : %s", e.getClass().getName(), e.getMessage()));
        }
    }
 
    private void log(String message, int mode) {
        // poster message
        String toPrint = null;
        switch (mode) {
        case 1:
            toPrint = String.format("%s --------------------------------", message);
            break;
        case 2:
            toPrint = String.format("-- %s", message);
            break;
        }
        System.out.println(toPrint);
    }
 
}

والنتائج هي كما يلي:


Vidage de la base de données --------------------------------
doNothing
Vidage de la base de données --------------------------------
getShortCategoriesByName1 --------------------------------
Catégorie de type : PROXY
Catégorie :
Exception : org.hibernate.LazyInitializationException, Message : failed to lazily initialize a collection of role: generic.jpa.entities.dbproduitscategories.Categorie.produits, could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongCategoriesByName1 --------------------------------
Catégorie de type : POJO
Catégorie :
1
Vidage de la base de données --------------------------------
getShortProduitsByName1 --------------------------------
Produit de type : PROXY
Nom de la catégorie du produit :
Exception : org.hibernate.LazyInitializationException, Message : could not initialize proxy - no Session
Vidage de la base de données --------------------------------
getLongProduitsByName1 --------------------------------
Produit de type : POJO
Nom de la catégorie du produit :
categorie[0]

هنا يمكننا أن نرى أنه عند الوصول إلى حقل [Categorie.produits] لفئة من نوع PROXY وحقل [Produit.categorie] لمنتج من نوع PROXY، يتم إلقاء استثناء [org.hibernate.LazyInitializationException] في كلتا الحالتين (السطران 7 و 17).