Skip to content

6. Spring Data JPA Hibernate

6.1. Introduction

ما از پایگاه داده [dbproduitscategories] که توسط پروژه [spring-jdbc-04] مدیریت می‌شود استفاده خواهیم کرد و دو رابط [IDao<Categorie>, IDao<Produit>] تعریف‌شده در آن پروژه را پیاده‌سازی خواهیم کرد. این کار به ما امکان می‌دهد چندین کار را انجام دهیم:

  • مقایسه کد پیاده‌سازی؛
  • استفاده از همان مجموعه تست؛
  • مقایسه عملکرد دو پیاده‌سازی؛
  • لایه [JDBC] توسط پروژه [mysql-config-jdbc] که در بخش ۳.۳ به آن پرداخته شده است، پیاده‌سازی شده است؛

اکنون به سایر لایه‌ها می‌پردازیم.

6.2. راه‌اندازی محیط کاری

با استفاده از STS، پروژه [mysl-config-jpa-hibernate] [1] را که در پوشه [<exemples>/spring-database-config/mysql/eclipse] [2] قرار دارد، وارد کنید:

این پروژه لایه [Spring JPA Hibernate] پروژه را پیکربندی می‌کند. هر پیاده‌سازی JPA پروژه پیکربندی خاص خود را دارد.

سپس، پروژه [spring-jpa-generic] [1] را که در پوشه [<exemples>/spring-database-generic/spring-jpa] [2] قرار دارد، وارد کنید:

پس از انجام این کار، محیط Maven را برای تمام پروژه‌ها در [Package Explorer] ریست کنید (Alt-F5):

 

سپس، برای بررسی محیط کاری، پیکربندی ساخت با نام [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>
        <!--وابستگی‌های متغیر ********************************************** -->
        <!-- JPA ارائه‌دهنده -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!--وابستگی‌های ثابت ********************************************** -->
        <!-- داده‌های اسپرینگ -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Spring Context -->
        <!-- ميراثی JDBC پیکربندی -->
        <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>
  • خطوط ۵–۷: آرتفکت Maven تولید شده توسط این پروژه. پروژه‌های پیکربندی برای سایر پیاده‌سازی‌ها، JPA (Eclipselink) و OpenJpa، از همین آرتفکت استفاده خواهند کرد. این بدان معناست که در هر زمان تنها یکی از این پروژه‌ها می‌تواند فعال باشد. بنابراین باید از قرار دادن همهٔ آن‌ها در [Package Explorer] خودداری کنید. تنها به یکی از آن‌ها نیاز است؛
  • خطوط ۱۰–۱۴: پروژهٔ والد Maven، که نسخه‌های اکثر وابستگی‌های مورد نیاز پروژه را مشخص می‌کند؛
  • خطوط ۱۹–۲۲: کتابخانه Hibernate؛
  • خطوط ۲۵–۲۸: کتابخانه Spring Data؛
  • خطوط ۳۲–۳۴: پروژه پیکربندی برای لایه JPA بر اساس پروژه پیکربندی برای لایه JDBC است که، در میان موارد دیگر، تعریف می‌کند، درایور JDBC برای SGBD در حال استفاده و جزئیات پایگاه داده مورد استفاده؛
  • خطوط ۳۵–۳۹: پروژه پیکربندی برای لایه JDBC شامل کتابخانه [Spring JDBC] است که در اینجا با کتابخانه [Spring Data JPA] جایگزین می‌شود. بنابراین توصیه می‌شود که در وابستگی‌های پروژه گنجانده نشود. با این حال، اگر باقی بماند، این امر باعث بروز هیچ خطایی نخواهد شد؛

در نهایت، وابستگی‌های پروژه به شرح زیر است:

  

6.3.2. پیکربندی Spring

 

کلاس [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 {

    // ارائه‌دهنده JPA
    @Bean
    public JpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        hibernateJpaVendorAdapter.setShowSql(false);
        hibernateJpaVendorAdapter.setDatabase(Database.MYSQL);
        hibernateJpaVendorAdapter.setGenerateDdl(true);
        return hibernateJpaVendorAdapter;
    }

    // JPA بسته‌های انتیت
    public final static String[] ENTITIES_PACKAGES = { "generic.jpa.entities.dbproduitscategories" };

    //منبع داده
    @Bean
    public DataSource dataSource() {
        //منبع داده TomcatJdbc
        DataSource dataSource = new DataSource();
        //پیکربندی دسترسی JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // اتصالات اولیه باز
        dataSource.setInitialSize(5);
        // نتیجه
        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();
    }

    // مدیر تراکنش
    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager txManager = new JpaTransactionManager();
        txManager.setEntityManagerFactory(entityManagerFactory);
        return txManager;
    }

}
  • خط ۱۸: این کلاس یک کلاس پیکربندی Spring است؛
  • خط ۱۹: این کلاس، بین‌هایی را که توسط کلاس پیکربندی [ConfigJdbc] تعریف شده‌اند، وارد می‌کند؛ این کلاس برای پیکربندی پروژه اسپرینگ [mysql-config-jdbc] استفاده شده است. این‌ها فیلترهای jSON هستند؛
  • خطوط ۲۳–۳۰: پیاده‌سازی مورد استفاده JPA را تعریف می‌کنند، در این مورد پیاده‌سازی Hibernate (خط ۲۵);
  • خط ۲۶: می‌توانید انتخاب کنید که آیا عملیات‌های SQL اجرا شده توسط پیاده‌سازی Hibernate نمایش داده شوند یا خیر؛
  • خط ۲۷: SGBD متصل به Hibernate را مشخص می‌کند. این پیکربندی مهم است. این امکان را برای Hibernate فراهم می‌کند تا از گویش SQL از SGBD MySQL، از جمله جنبه‌های اختصاصی آن، استفاده کند. علاوه بر این، این به آن اطلاع می‌دهد که چه انواع SQL و چه اشیاء SGBD را قادر به استفاده خواهد بود. این قابلیت پیاده‌سازی JPA برای تطبیق با یک SGBD خاص است که به آن قابلیت حمل‌پذیری بالایی بین SGBD می‌بخشد؛
  • خط ۲۸: Hibernate ممکن است جداول پایگاه داده مقصد را از اشیاء JPA که پیدا می‌کند، ایجاد کند یا نکند. این ایجاد تنها در صورتی انجام می‌شود که جداول موجود نباشند. اگر از قبل موجود باشند، هیچ اقدامی انجام نمی‌شود. ما از این قابلیت برای ایجاد جدول‌ها استفاده خواهیم کرد، زمانی که توضیح می‌دهیم اسکریپت‌های SQL برای ایجاد پایگاه‌داده‌های مختلفی که در این سند استفاده شده‌اند، چگونه ایجاد شده‌اند؛
  • خط ۳۳: پکیج حاوی انتیت‌های JPA از پایگاه داده [dbproduitscategories
  • خطوط ۳۶–۴۹: منبع داده [tomcat-jdbc] که به پایگاه داده [dbproduitscategories] متصل است؛
  • خطوط ۵۲–۶۰: بین با نام [entityManagerFactory] (این نام باید دقیقاً همان‌طور که نشان داده شده باشد) بینِ ایجادکننده‌ی شیء [EntityManager] است که زمینهٔ پایداری JPA را مدیریت می‌کند. تمام عملیات JPA از طریق آن انجام می‌شود. از آنجایی که ما از [Spring Data JPA] استفاده می‌کنیم، هرگز خودمان از این شیء استفاده نخواهیم کرد. با این حال، باید آن را پیکربندی کنیم. این شیء باید موارد زیر را بداند:
    • پیاده‌سازی JPA مورد استفاده (خط ۵۵)؛
    • منبع داده‌ی مورد استفاده (خط ۵۷)؛
    • اشیاء JPA از این منبع (خط ۵۶)؛
  • خط ۵۸: EntityManager را با این اطلاعات اولیه می‌کند؛
  • خط ۵۹: نمونهٔ واحد [entityManagerFactory] را بازمی‌گرداند؛
  • خطوط ۶۳–۶۸: مدیر تراکنش را تعریف کنید. نام آن باید [transactionManager] باشد؛
  • خط ۶۵: یک مدیر تراکنش با نام JPA ایجاد می‌شود؛
  • خط ۶۶: این از طریق بین [entityManagerFactory] (خطوط ۵۳ و ۵۷) به منبع داده در خط ۳۷ متصل می‌شود؛

تنها بین موجود در خطوط 23–30 به پیاده‌سازی JPA وابسته است. سایر بین‌ها سپس به آن متکی هستند.

6.3.3. اشیاء در لایه [JPA]

  

پایگاه داده هدف، پایگاه داده [dbproduitscategories] است که شامل دو جدول [CATEGORIES] و [PRODUITS] می‌باشد. ما دیدیم که آن همچنین دارای سه جدول دیگر است، [USERS, ROLES, USERS_ROLES]، که برای ایمن‌سازی سرویس وبی که قرار است روی وب مستقر شود، استفاده خواهد شد. در حال حاضر این جدول‌ها را نادیده می‌گیریم. به عنوان یادآوری، ساختار جدول‌های [CATEGORIES] و [PRODUITS] به شرح زیر است:

جدول [PRODUITS] به شرح زیر است:

  • [ID]: کلید اصلی با افزایش خودکار جدول [2];
  • [NOM]: نام منحصربه‌فرد محصول برای [4];
  • [PRIX]: قیمت محصول؛
  • [DESCRIPTION]: توضیحات محصول؛
  • [VERSIONING] شماره نسخه محصول است. نسخه اولیه آن 1 [3] است. هر بار که محصول اصلاح شود، شماره نسخه آن توسط کدی که جدول را اجرا می‌کند، افزایش می‌یابد؛
  • [CATEGORIE_ID]: کلید خارجی در جدول [CATEGORIES]، که برای شناسایی دسته‌ای که محصول به آن تعلق دارد، استفاده می‌شود؛
  • در [1-3]، کلید خارجی [CATEGORIE_ID] از جدول [PRODUITS]. این کلید خارجی ستون [ID] در جدول [CATEGORIES] [4-5] را هدف قرار می‌دهد؛
  • هنگامی که یک دسته‌بندی حذف می‌شود، تمام محصولات مرتبط با آن نیز حذف می‌شوند ([6]). این نکته مهم است زیرا در ساخت لایه [DAO] که از پایگاه داده [dbproduitscategories] استفاده می‌کند، به کار می‌رود؛

جدول دسته‌بندی [CATEGORIES] به شرح زیر است:

  • [ID]: کلید اصلی با افزایش خودکار؛
  • [VERSIONING]: شماره نسخه دسته‌بندی;
  • [NOM]: نام منحصربه‌فرد دسته‌بندی؛

اکنون موجودیت‌های JPA، [Produit] و [Categorie] را که تصاویر جداول [PRODUITS] و [CATEGORIES] هستند، توصیف خواهیم کرد.

  

6.3.3.1. رابط [AbstractCoreEntity]

رابط [AbstractCoreEntity] توسط اجزای JPA، [Categorie] و [Produit] پیاده‌سازی شده است:


package generic.jpa.entities.dbproduitscategories;

public interface AbstractCoreEntity {

    // گیرنده و تنظیم‌کننده برای فیلدها [id], [version], [entityType]
    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 [Produit]

کلاس [Produit]، موجودیت JPA است که با یک سطر در جدول [PRODUITS] مرتبط می‌باشد:

 

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 {
    // ویژگی‌ها
    @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();

    // ویژگی‌ها
    @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;

    // دسته‌بندی
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID)
    private Categorie categorie;

    // تولیدکنندگان
    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;
    }

    // امضا
    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);
    }

    // ------------------------------------------------------------
    // تعریف مجدد [equals] و [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);
    }

    // گیرنده و تنظیم‌کننده
...
    public void setCategorie(Categorie categorie) {
        // نوع موجودیت
        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;
    }

}
  • ردیف ۲۱: نشانه‌گذاری [@Entity] کلاس [Produit] را به‌عنوان یک موجودیت که توسط لایه [JPA] مدیریت می‌شود، مشخص می‌کند. همچنین می‌توان [@Entity(name="MonProduit")] را نوشت که به این موجودیت نام [MonProduit] را می‌دهد. در غیاب این اطلاعات، نام این موجودیت، نام کلاس است، که در این مورد [Produit] می‌باشد. این قرارداد نام‌گذاری زمانی ضروری می‌شود که دو کلاس از پکیج‌های مختلف در میان موجودیت‌ها وجود داشته باشند که نام یکسانی داشته باشند؛
  • خط ۲۲: انوتیشن [@Table(name = "PRODUITS")] نشان می‌دهد که کلاس [Produit] نمایانگر شیء یک سطر در جدول [PRODUITS] در پایگاه داده است؛
  • خط ۲۳: نام فیلتر jSON که باید بر روی انتیت اعمال شود. خواهیم دید که ویژگی [categorie] در خط ۵۸ همیشه در دسترس نیست. بنابراین باید از نمایش jSON شیء حذف شود. برای این کار به یک فیلتر نیاز داریم. بنابراین در فیلتری با نام [jsonFilterCategorie] مشخص خواهیم کرد که آیا می‌خواهیم ویژگی [categorie] باشد یا خیر؛
  • خط ۲۶: نشانه‌گذاری [@Id]، فیلد نشانه‌گذاری‌شده را به‌عنوان فیلد مرتبط با کلید اصلی جدول در خط ۱۹ تعیین می‌کند؛
  • خط ۲۷: حاشیه‌نویسی [@GeneratedValue(strategy = GenerationType.IDENTITY)] حالت تولید خودکار کلید اصلی در جدول [PRODUITS] را تعیین می‌کند. این حالت توسط ویژگی [strategy] مشخص می‌شود. حالت‌های مختلفی وجود دارد:

Image

استراتژی [IDENTITY] برای همه نمونه‌های SGBD در دسترس نیست. از میان شش نمونه SGBD که آزمایش شدند، این قابلیت برای نمونه‌های SGBD و [MySQL 5, PostgreSQL 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 قابل استفاده نخواهد بود و بالعکس.

  • خط ۲۸: آنوتیشن [@Column(name="ID"] نام ستون در جدول [PRODUITS] را که باید با فیلد [id] مرتبط شود، مشخص می‌کند؛
  • خط ۲۹: نوع [Long] به جای [long] برای کلید اصلی استفاده می‌شود. این به این دلیل است که کلیدهای اصلی از نوع [null] معنای خاصی برای JPA دارند. بنابراین ترجیح دارد در اینجا از یک نوع شیء به جای یک نوع ساده استفاده شود؛
  • خط ۳۱: انوتیشن [@Version] نشان می‌دهد که فیلد [version] با یک ستون نسخه‌بندی مرتبط است. پیاده‌سازی JPA هر بار که این انتیتي تغییر می‌کند، این شماره نسخه را افزایش خواهد داد. این عدد برای جلوگیری از به‌روزرسانی همزمان یک موجودیت توسط دو کاربر مختلف استفاده می‌شود: دو کاربر، U1 و U2، موجودیت E را با شماره نسخه برابر با V1 می‌خوانند. U1، E را اصلاح می‌کند و این تغییر را در پایگاه داده ثبت می‌کند: شماره نسخه سپس به V1+1 تغییر می‌یابد. به نوبه خود E را اصلاح کرده و این تغییر را در پایگاه داده commit می‌کند: یک exception دریافت خواهد کرد زیرا نسخه آن (V1) با نسخه موجود در پایگاه داده (V1+1) متفاوت است؛
  • خط ۳۶: نوع انتیت. دو نوع وجود خواهد داشت: POJO و PROXY. به‌طور پیش‌فرض، نمونهٔ حاصل یک POJO (Plain Old Java Object) خواهد بود. در برخی موارد، نمونه‌های [Produit] بازیابی‌شده از پایگاه داده از نوع [PROXY] خواهند بود. این حالت زمانی است که ویژگی [Categorie categorie] در خط 58 به دلیل ویژگی [fetch = FetchType.LAZY] در خط 56 با یک دسته‌بندی مقداردهی اولیه نشده است. در این حالت، پیاده‌سازی‌های JPA که باید آزمایش شوند متفاوت هستند:
    • [Hibernate, OpenJPA]: دسترسی به دسته‌بندی محصولی با نوع [PROXY] منجر به بروز خطا می‌شود. Hibernate از اصطلاح «proxy» برای اشاره به یک نمونه JPA که در حالت [LAZY] به دست آمده است، استفاده می‌کند. به همین دلیل من از این اصطلاح برای اشاره به این نوع موجودیت استفاده کرده‌ام؛
    • [EclipseLink]: دسترسی به دسته‌بندی یک محصول از نوع [PROXY]، جستجو برای آن دسته‌بندی را در پایگاه داده راه‌اندازی می‌کند و هیچ استثنایی پرتاب نمی‌شود؛

از آنجا که می‌خواستم یک لایه تست مستقل از پیاده‌سازی JPA داشته باشم، لازم بود نوع هر انتیت: POJO یا PROXY را بدانم. به همین دلیل فیلد [entityType] را به موجودیت‌های JPA اضافه کردم؛

  • خط ۳۵: انوتیشن [@Transient] نشان می‌دهد که پیاده‌سازی JPA باید این فیلد را نادیده بگیرد. این به این دلیل است که این فیلد در جداول SGBD وجود ندارد؛
  • خط ۴۰: کلاس [Produit] یک استثنا از نوع [ProxyException] پرتاب می‌کند که نیاز به نام کلاس دارد؛
  • خط ۳۸: همانند قبل، مشخص شده است که پیاده‌سازی JPA باید این فیلد را نادیده بگیرد؛
  • خط ۳۹: انوتیشن [@JsonIgnore] مشخص می‌کند که سریالیزر/دسریالیزر jSON برای یک نمونه از [Produit] باید این فیلد را نادیده بگیرد؛
  • خط ۴۳: حاشیه‌نویسی [@Column] فیلد [nom] را با ستون [NOM] در جدول [PRODUITS] مرتبط می‌سازد. وقتی نام فیلد با نام ستون مربوطه یکسان باشد (بدون توجه به حروف بزرگ و کوچک)، می‌توان از توضیح [@Column] صرف‌نظر کرد. این مورد در اینجا صادق است. ویژگی‌های [unique = true, length = 30, nullable = false] تنها زمانی استفاده می‌شوند که پیاده‌سازی JPA ملزم به تولید جدول [CATEGORIES] از طریق انتیت [Produit] باشد. این موارد توسط ویژگی‌های SQL و [UNIQUE, VARCHAR(30), NOT NULL] نمایش داده می‌شوند که تضمین می‌کنند ستون [NOM] بیش از ۳۰ کاراکتر نداشته باشد، در جدول یکتا باشد و نمی‌تواند مقدار NULL را داشته باشد؛
  • خطوط ۴۶–۴۷: فیلد [idCategorie] به ستون [CATEGORIE_ID] متصل است. کمی بعد به ویژگی‌های آن باز خواهیم گشت؛
  • خطوط ۴۹–۵۰: فیلد [prix] با ستون [PRIX] مرتبط است؛
  • خطوط ۵۲–۵۳: فیلد [description] با ستون [DESCRIPTION] مرتبط است؛
  • خطوط ۵۶–۵۸: دسته‌بندی محصول؛
  • خط ۵۶: انوتیشن [@ManyToOne] نشان می‌دهد که ستون انوتیشن در خط ۵۷، [@JoinColumn(name = "CATEGORIE_ID")]، یک کلید خارجی در جدول [PRODUITS] از نوعوجوده [Produit] در جدول [CATEGORIES] که با وجوده در خط 58 مرتبط است. این حاشیه‌نویسی باید یک وجوده JPA را حاشیه‌نویسی کند. بنابراین، کلاس در سطر 58 باید یک موجودیت JPA باشد؛
  • خط ۵۶: انوتیشن [fetch = FetchType.LAZY] مشخص می‌کند که وقتی یک محصول از جدول [PRODUITS] بازیابی می‌شود، دسته‌بندی آن (خط ۵۸) نباید بلافاصله بازیابی شود (بارگذاری تنبل). سپس در اولین فراخوانی متد [getCategorie] بازیابی می‌شود. برای دستیابی به این هدف، در زمان اجرا، لایه JPA، متد اولیه [getCategorie] (که به سادگی فیلد categorie را برمی‌گرداند) را با فراخوانی SGBD برای بازیابی دسته‌بندی، گسترش می‌دهد – تکنیکی که به آن «پراکسی‌سازی» (proxying) گفته می‌شود.. پیاده‌سازی‌های JPA در پیاده‌سازی این ویژگی، همان‌طور که در بالا ذکر شد، متفاوت هستند. این ویژگی اجباری نیست. پیاده‌سازی JPA مجاز است که آن را نادیده بگیرد. دلیل آن این است که ویژگی [categorie] ممکن است موجود باشد یا نباشد، به همین دلیل ما فیلتر jSON را در خط ۲۳ معرفی کرده‌ایم. ستون الحاق [CATEGORIE_ID] در جدول [PRODUITS] به طور خودکار هنگام درج یا به‌روزرسانی محصول به‌روز می‌شود. این مقدار از [categorie.getId()] اختصاص می‌یابد، که در آن [categorie] فیلد در سطر 58 است. مشخصه JPA مقرر می‌دارد که این ستون الحاقی نمی‌تواند از هیچ طریق دیگری به‌روزرسانی شود. بنابراین ویژگی‌های [insertable = false, updatable = false] از خط ۴۶ را اعمال می‌کند که تضمین می‌کنند ستون [CATEGORIE_ID] (یعنی ستون الحاق) مرتبط با فیلد [idCategorie] نمی‌تواند توسط فیلد [idCategorie] اصلاح شود. تنها انتقال ستون [CATEGORIE_ID] به فیلد [idCategorie] امکان‌پذیر خواهد بود؛
  • خطوط ۹۱–۱۰۴: برابری بین entities [Produit] به عنوان برابری بین کلیدهای اصلی آنها [id] تعریف می‌شود؛
  • خطوط ۱۰۸–۱۱۵: برای قابل حمل کردن لایه تست خود، ما اشیاء [PROXY] را از سه پیاده‌سازی (JPA و [Hibernate, EclipseLink, OpenJpa]) به صورت یکنواخت مدیریت خواهیم کرد. برای یک عنصر [Produit] از نوع [PROXY]، از تغییر مقدار فیلد [categorie] جلوگیری خواهیم کرد. کلاس [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 انتظار دارد که جدول‌ها را دقیقاً همان‌طور که خود ایجاد کرده بود، بیابد. بنابراین، نمی‌توان هر جدول [PRODUITS] را با موجودیت قبلی [Produit] مرتبط دانست. جدول مورد نظر باید حداقل (و ممکن است ویژگی‌های دیگری نیز داشته باشد) ویژگی‌های جدولی را که [PRODUITS] تولید می‌کرد، دارا باشد. هنگام کار با JPA، بهتر است با یک پایگاه داده خالی شروع کنید که در آن به JPA اجازه داده شود جداول را ایجاد کند. ما کمی بعد در مورد این فرآیند تولید بحث خواهیم کرد. اسکریپت SQL که برای SGBD و MySQL ارائه شده بود، از جداول تولید شده توسط JPA ایجاد شد.

تمام ویژگی‌های موجودیت [Produit] برای تولید جدول [PRODUITS] استفاده می‌شوند. پس از انجام این کار، ویژگی‌های تولید مانند [unique = true, length = 30, nullable = false] هنگام پردازش جدول‌ها دیگر استفاده نمی‌شوند.

6.3.3.3. وجودیت JPA [Categorie]

کلاس [Categorie] یک موجودیت 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();

    //ویژگی‌ها
    @Column(name = ConfigJdbc.TAB_CATEGORIES_NOM, unique = true, length = 30, nullable = false)
    private String nom;

    // محصولات مرتبط
    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    private List<Produit> produits;

    // سازنده‌ها
    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;
    }

    // امضا
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s]", id, version, nom);
    }

    // روش‌ها
    public void addProduit(Produit produit) {
        // نوع موجودیت
        if (entityType == EntityType.PROXY) {
            throw new ProxyException(1004, new RuntimeException(
                    "On ne peut ajouter de produits à une catégorie de type [PROXY]"), simpleClassName);
        }
        //افزودن محصول
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // افزودن محصول
            produits.add(produit);
            // تنظیم دسته‌بندی آن
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }

    // ------------------------------------------------------------
    // تعریف مجدد [equals] و [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);
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • ردیف ۲۴: کلاس یک موجودیت JPA است؛
  • خط ۲۵: مرتبط با جدول [CATEGORIES];
  • خط ۲۶: نمایش jSON از موجودیت [Categorie] توسط فیلتری به نام [jsonFilterCategorie] کنترل می‌شود. این فیلتر باید قبل از هر درخواستی برای نمایش jSON از این موجودیت پیکربندی شود. فیلتر [jsonFilterCategorie] برای حذف یا درج فیلد [produits] در خط 40 از نمایش jSON موجودیت [Categorie] استفاده خواهد شد؛
  • رده‌های ۲۹–۳۲: فیلد [id] با کلید اصلی [ID] از جدول [CATEGORIES] مرتبط است. حالت تولید انتخاب‌شده [IDENTITY] است، بنابراین حالت برای MySQL، [AUTO_INCREMENT] است؛
  • رده‌های ۳۴–۳۶: فیلد [version] به ستون نسخه‌بندی [VERSIONING] در جدول [CATEGORIES] متصل است؛
  • خطوط ۳۸–۳۹: نوع موجودیت [Categorie];
  • خطوط ۴۱–۴۳: نام کوتاه کلاس [Categorie];
  • خطوط ۴۶–۴۷: فیلد [nom] به ستون [NOM] در جدول [CATEGORIES] متصل شده است. ویژگی‌های JPA و [unique = true, length = 30, nullable=false] به آن اختصاص داده شده است تا زمانی که جدول [CATEGORIES] ایجاد می‌شود، ستون [NOM] دارای ویژگی‌های SQL و [UNIQUE, VARCHAR(30), NOT NULL] است؛
  • خطوط 50–51: محصولاتی که به این دسته تعلق دارند؛
  • خط ۵۰: نشانه‌گذاری [@OneToMany] معکوس رابطه [@ManyToOne] است که در موجودیت [Produit] با آن مواجه شدیم. ویژگی [mappedBy = "categorie"] نشان‌دهنده فیلدی از انتیتای [Produit] است که توسط رابطه معکوس [@ManyToOne] نشانه‌گذاری شده است. ویژگی [cascade = { CascadeType.ALL }] مشخص می‌کند که عملیات (persist، merge، remove) انجام‌شده روی یک @Entity [Categorie] باید به انتیت‌های [produits] در خط 51 سرایت کند. می‌توان آبشاری جزئی را با استفاده از ثابت [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] مشخص کرد؛
  • خط 50: ویژگی [fetch = FetchType.LAZY] مشخص می‌کند که وقتی یک دسته‌بندی از جدول [CATEGORIES] بازیابی می‌شود، محصولات آن بلافاصله بازیابی نمی‌شوند. سپس این محصولات در اولین فراخوانی متد [getProduits] بازیابی می‌شوند. برای دستیابی به این هدف، در زمان اجرا، لایه JPA، متد اولیه [getProduits] (که صرفاً فیلد produits را برمی‌گرداند) را با فراخوانی SGBD برای بازیابی محصولات آن دسته، گسترش می‌دهد. این ویژگی الزامی است. پیاده‌سازی JPA نمی‌تواند آن را نادیده بگیرد. از آنجایی که ویژگی [produits] ممکن است مقداردهی اولیه شود یا نشود، ما فیلتر jSON را در خط 26 معرفی کرده‌ایم که به ما امکان می‌دهد مشخص کنیم که آیا می‌خواهیم این ویژگی را داشته باشیم یا خیر، و نوع موجودیت را در خط 39 مشخص کنیم؛
  • خطوط ۷۱–۸۸: متد [addProduit] امکان افزودن یک محصول به دسته‌بندی را فراهم می‌کند؛
  • خطوط ۷۳–۷۶: برای استانداردسازی نحوه مدیریت پروکسی‌ها در پیاده‌سازی‌های مختلف JPA، تصمیم گرفته شده است که محصولات را نمی‌توان به یک موجودیت [Categorie] از نوع PROXY اضافه کرد؛
  • خطوط ۹۲–۱۱۲: دو موجودیت [Categorie] در صورتی برابر در نظر گرفته می‌شوند که کلید اصلی یکسانی داشته باشند [id];

6.3.4. فایل [persistence.xml]

  

برنامه‌های کاربردی JPA باید برخی از ویژگی‌های ارائه‌دهنده JPA مورد استفاده و همچنین موجودیت‌های JPA مورد استفاده را در یک فایل [META-INF/persistence.xml] که در مسیر کلاس (classpath) برنامه قرار دارد، تعریف کنند. در مثال بالا، این فایل در پوشه [src/main/resources] قرار داده شده است که در واقع بخشی از مسیر کلاس‌پث پروژه اکلیپس است. هنگام استفاده از 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>
  • خطوط ۱–۵: یک فایل [persistence.xml] باید دارای یک تگ ریشه <persistence> باشد. ویژگی‌های تگ در خط ۲ در این برنامه استفاده نخواهند شد؛
  • یک فایل پایداری می‌تواند با استفاده از تگ <persistence-unit> (خط ۴) یک یا چند واحد پایداری را تعریف کند. یک واحد پایداری دسترسی به یک پایگاه دادهٔ خاص را مدیریت می‌کند. اگر برنامه به‌طور همزمان دو پایگاه داده را مدیریت کند، دو واحد پایداری خواهد داشت؛
  • خط ۴: یک واحد پایداری دارای نام [attribut name] است، از نوع تراکنش [attribut transaction-type] پشتیبانی می‌کند، دارای ویژگی‌ها است و موجودیت‌های مرتبط با جدول‌های پایگاه داده که توسط واحد پایداری مدیریت می‌شوند را تعریف می‌کند. در اینجا، از آنجا که دسترسی به پایگاه داده توسط [Spring JPA Hibernate] مدیریت می‌شود، این دو بخش آخر اطلاعات را می‌توان در جای دیگری قرار داد. دو نوع تراکنش وجود دارد:
    • [RESOURCE_LOCAL]: تراکنش‌ها توسط خود برنامه مدیریت می‌شوند. در اینجا نیز همین‌طور است، زیرا Spring تراکنش‌ها را مدیریت خواهد کرد؛
    • [JTA] (معاملات جاوا API): این کانتینر EJB (Enterprise Java Bean) است که برنامه را اجرا می‌کند و بر اساس حاشیه‌نویسی‌های (annotations) جاوا موجود در کد، معاملات را به‌طور خودکار مدیریت خواهد کرد. این پیکربندی مورد استفاده ما در اینجا نیست؛

بعداً خواهیم دید که محتویات این فایل [persistence.xml] به پیاده‌سازی JPA مورد استفاده بستگی دارد.

6.4. پروژه [spring-jpa-generic]

بیایید خلاصه‌ای از آنچه می‌خواهیم انجام دهیم مرور کنیم. ما می‌خواهیم معماری زیر را پیاده‌سازی کنیم:

که در آن لایه [DAO] رابط [IDao<Produit>, IDao<Categorie>] را که در فصل ۴ مورد بحث قرار گرفت، پیاده‌سازی می‌کند. هدف مقایسه دو پیاده‌سازی از این رابط است:

  • یکی که با استفاده از 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>
        <!--پیکربندی JPA برای 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>
  • خطوط ۲۲–۲۶: پروژه تنها یک وابستگی دارد، یعنی به پروژه‌ای که لایه [JPA] برنامه را پیکربندی می‌کند، که همین‌الان بررسی کردیم. این یک برنامه عمومی است:
    • ما با تغییر پروژه پیکربندی برای لایه [JDBC] به SGBD سوئیچ می‌کنیم؛
    • ما پیاده‌سازی 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 {

}
  • خط ۱۱: این کلاس یک کلاس پیکربندی Spring است؛
  • خط ۱۰: آناوتیشن [@EnableJpaRepositories] برای مشخص کردن پکیج‌هایی که شامل رابط‌های Spring Data [CrudRepository] هستند، استفاده می‌شود. این کار آن‌ها را به کامپوننت‌های Spring تبدیل می‌کند که می‌توانند به سایر کامپوننت‌های Spring تزریق شوند؛
  • خط ۱۲: آنوتیشن [@ComponentScan] نشان می‌دهد که باید در پکیج [spring.data.dao] برای کامپوننت‌های Spring جستجو شود. کامپوننت‌های [DaoCategorie] و [DaoProduit] یافت خواهند شد؛
  • خط ۱۳: بین‌های کلاس پیکربندی [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> {

    // دسته‌بندی با محصولاتش
    @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();

}
  • خط ۱۰: رابط [CrudRepository] در بخش ۵.۱.۳ استفاده و توضیح داده شد. باید توجه داشت که:
    • نوع اولین پارامتر این رابط، انتیت JPA است که برای دسترسی به CRUD مدیریت می‌شود (findOne, findAll, ذخیره، حذف، deleteAll
    • نوع دوم پارامتر در این رابط، نوع کلید اصلی انتیتای JPA است، که در این مورد یک عدد صحیح [Long] است؛

متدهای این رابط توسط پرس‌وجوهای JPQL (زبان پرس‌وجوی پایداری جاوا) پیاده‌سازی می‌شوند. این پرس‌وجو، اشیاء JPA را بازیابی می‌کند. در چنین پرس‌وجویی:

  • جدول‌ها با انتیته‌های مرتبط JPA جایگزین می‌شوند؛
  • ستون‌ها با فیلدهای موجود در اشیاء JPA مورد استفاده در پرس‌وجو جایگزین می‌شوند؛

بگذارید خطوط ۳۱–۳۲ را به‌عنوان مثال در نظر بگیریم: متد در خط ۳۲ تمام دسته‌بندی‌ها را در پایگاه داده به‌صورت کوتاه بازمی‌گرداند. این کار توسط پرس‌وجوی JPQL (زبان پرس‌وجوی پایداری جاوا) در خط ۳۱ پیاده‌سازی شده است که شباهت زیادی به معادل خود SQL دارد. برای جزئیات بیشتر در مورد JPQL، به [ref2] مراجعه کنید (به بخش 1.2 مراجعه کنید).

متدهای رابط [CategoriesRepository] به شرح زیر است:

  • سطور ۱۳–۱۴: متد [getLongCategorieById] نسخهٔ کامل یک دسته‌بندی را که با کلید اصلی [id] ارجاع شده است بازمی‌گرداند، یعنی دسته‌بندی همراه با محصولات آن. به یاد داشته باشید که در موجودیت [Categorie]، فیلد [produits] دارای ویژگی [fetch = FetchType.LAZY] (بارگذاری تنبل) بود. در پرس‌وجوی JPQL، با استفاده از کلمه کلیدی [fetch]، بارگذاری محصولات را اجباری می‌کنیم. پارامتر ؟1 در کوئری در زمان اجرا با مقدار اولین پارامتر متد در خط 12، یعنی پارامتر [Long id]، جایگزین خواهد شد؛
  • خطوط 16–17: متد [getLongCategorieByName] نسخه طولانی یک دسته‌بندی را که با نام [nom] ارجاع شده است، بازمی‌گرداند؛
  • خطوط ۱۹–۲۰: متد [getShortCategoriesByName] نسخه‌های کوتاه دسته‌بندی‌هایی را که با نام‌هایشان ارجاع شده‌اند، بازمی‌گرداند. فیلد [produits] برای این دسته‌بندی‌ها، null نیست. این شامل مرجعی به یک پروکسی (کلاسی که توسط پیاده‌سازی JPA ایجاد شده) است که وظیفه‌اش بازگرداندن محصولات در آن دسته هنگام فراخوانی است. فراخوانی آن خارج از زمینه پایداری JPA باعث ایجاد یک استثنا می‌شود (Hibernate و OpenJpa، اما نه EclipseLink). به همین دلیل، ما از فیلد [produits] نسخه کوتاه یک دسته استفاده نخواهیم کرد؛
  • خطوط ۲۲–۲۳: متد [getShortCategoriesById] نسخه‌های کوتاه دسته‌بندی‌ها را که با کلیدهای اصلی‌شان [id] مرجع شده‌اند، بازمی‌گرداند؛
  • خطوط ۲۵–۲۶: متد [getLongCategoriesById] نسخه‌های طولانی دسته‌بندی‌ها را که با کلیدهای اصلی‌شان [id] ارجاع شده‌اند، بازمی‌گرداند؛
  • خطوط [28-29]: متد [getLongCategoriesByName] نسخه‌های طولانی دسته‌بندی‌ها را که با نام‌هایشان ارجاع شده‌اند، بازمی‌گرداند؛
  • خطوط ۳۱–۳۲: متد [getAllShortCategories] نسخه‌های کوتاه همه دسته‌بندی‌ها را بازمی‌گرداند؛
  • خطوط ۳۴–۳۵: متد [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. رابط [ProduitsRepository]

رابط [ProduitsRepository] دسترسی به جدول [PRODUITS] را مدیریت می‌کند:


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> {

    //یک محصول و دسته‌بندی آن
    @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]: متد [getLongProduitById] جزئیات کامل یک محصول را که با کلید اصلی [id] شناسایی شده است، از جمله دسته‌بندی آن، بازمی‌گرداند. به یاد داشته باشید که در موجودیت [Produit]، فیلد [categorie] دارای ویژگی [fetch = FetchType.LAZY] (بارگذاری تنبل) بود. در پرس‌وجوی JPQL، با استفاده از کلمه کلیدی [fetch]، بارگذاری دسته‌ای را اجباری می‌کنیم؛
  • خطوط ۱۸–۱۹: متد [getLongProduitByName] نسخه طولانی محصولی را که با نامش شناسایی شده بازمی‌گرداند؛
  • خطوط ۲۱–۲۲: متد [getShortProduitsById] نسخهٔ کوتاه محصولات شناسایی‌شده با کلید اصلی [id] را بازمی‌گرداند. در این نسخهٔ کوتاه، فیلد [categorie] مقدار null را ندارد. این فیلد حاوی مرجعی به یک پروکسی است که توسط پیاده‌سازی JPA ایجاد شده و در صورت فراخوانی، دستهٔ محصول را بازیابی می‌کند. این فراخوانی فقط در داخل زمینه پایداری JPA قابل انجام است. انجام آن در جای دیگر باعث ایجاد یک استثنا (Hibernate و OpenJpa، اما نه EclipseLink) می‌شود. بنابراین، در لایه [DAO] یا هر جای دیگر، از فیلد [categorie] یک محصول در نسخه کوتاه آن استفاده نخواهیم کرد. در نسخه کوتاه محصول، فیلد [idCategorie] مقداردهی اولیه می‌شود. مقدار آن کلید اصلی دسته‌ای است که محصول به آن تعلق دارد. این کار امکان بازیابی بعدی این دسته را از لایه [DAO] از طریق متد [DaoCategorie. getShortCategoriesById(idCategorie)] فراهم می‌کند؛
  • خطوط ۲۴–۲۵: متد [getShortProduitsByName] نسخه کوتاه محصولات شناسایی‌شده بر اساس نام‌هایشان را بازمی‌گرداند؛
  • خطوط ۲۷–۲۸: متد [getLongProduitsById] نسخه طولانی محصولات شناسایی‌شده بر اساس کلیدهای اصلی آن‌ها را بازمی‌گرداند؛
  • خطوط ۳۰–۳۱: متد [getLongProduitsByName] نسخه طولانی محصولات شناسایی‌شده بر اساس نام‌هایشان را بازمی‌گرداند؛
  • خطوط ۳۳–۳۴: متد [getAllLongProduits] نسخه طولانی همه محصولات را بازمی‌گرداند؛
  • خطوط ۳۶–۳۷: متد [getAllShortProduits] نسخهٔ کوتاه همهٔ محصولات را بازمی‌گرداند؛

این رابط‌ها توسط کلاس‌هایی که هنگام اجرای پروژه توسط پیاده‌سازی JPA تولید می‌شوند، پیاده‌سازی خواهند شد. به چنین کلاس‌هایی، کلاس‌های [proxy] گفته می‌شود. به‌طور پیش‌فرض، متدهای رابط [CrudRepository] در داخل یک تراکنش اجرا می‌شوند. این واقعیت که رابط‌های [ProduitsRepository, 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> {

    // فهرست تمام موجودیت‌های T
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // واحدهای خاص – نسخهٔ کوتاه
    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);

    // واحدهای خاص – نسخه طولانی
    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);

    // به‌روزرسانی چندین موجودیت
    public List<T> saveEntities(Iterable<T> entities);

    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities);

    // حذف تمام موجودیت‌ها
    public void deleteAllEntities();

    // حذف چندین انتیت
    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);
        }
    }

}
  • خط ۱۷: آناوتیشن [@Component] کلاس [DaoCategorie] را به یک کامپوننت Spring تبدیل می‌کند؛
  • خط ۱۸: کلاس [DaoCategorie] از کلاس [AbstractDao<Categorie>] ارث می‌برد، که به این معنی است که این کلاس رابط [IDao<Categorie>] را پیاده‌سازی می‌کند؛
  • خطوط ۲۰–۲۴: تزریق ارجاع به دو رابط [CrudRepository] و [Spring Data]. این تزریق هنگام نمونه‌سازی اشیاء Spring، معمولاً در ابتدای اجرای پروژه Spring، انجام می‌شود؛
  • تمام متدهای کلاس کار را به متدهای با همان نام در رابط‌های [CrudRepository] واگذار می‌کنند؛
  • تمام متدهایی که اِنتیت‌ها را به صورت کوتاه بازمی‌گردانند، این موضوع را با تنظیم نوع اِنتیتی روی [EntityType.PROXY] (خطوط 29، 63، 72) نشان می‌دهند؛

متد [saveEntities] نیاز به توضیح دارد:


@Override
    protected List<Categorie> saveEntities(List<Categorie> categories) {
        // توجه به محصولات قابل درج
        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);
                    }
                    // از این فرصت استفاده می‌کنیم تا (در صورت لزوم) رابطه محصول ← دسته‌بندی را مجدداً برقرار کنیم
                    produit.setCategorie(categorie);
                }
            }
        }
        //ذخیرهٔ دسته‌بندی‌ها و محصولات
        try {
            categoriesRepository.save(categories);
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        //به‌روزرسانی فیلد [idCategorie] برای محصولات درج‌شده
        for (Produit produit : insertedProduits) {
            produit.setIdCategorie(produit.getCategorie().getId());
        }
        //نتیجه
        return categories;
    }
  • خط ۲: دسته‌های ارسالی به عنوان پارامتر، هم دسته‌های ورودی برای درج ([id==null]) و هم دسته‌های ورودی برای اصلاح ([id!=null]) هستند؛
  • خط ۲۰: دسته‌بندی‌ها با استفاده از روش [categoriesRepository.save(entities)] ذخیره می‌شوند. در حین آزمایش مشخص شد که فیلد [idCategorie] برای محصولات ذخیره‌شده (id==null) پر نمی‌شود. برای حل این مشکل، در خطوط ۴ تا ۱۷ محصولات مورد نظر برای درج را ثبت می‌کنیم و پس از ذخیره شدن، فیلد [idCategorie] آن‌ها را پر می‌کنیم (خطوط ۲۵ تا ۲۷)؛
  • خطوط ۵–۱۷: ما لیست دسته‌بندی‌ها را مرور می‌کنیم؛
  • خطوط ۸–۱۶: برای هر دسته‌بندی، روی فهرست محصولات آن تکرار می‌کنیم. در اینجا مشکلی وجود دارد. متد [saveEntities] هم برای ذخیره و هم برای اصلاح یک دسته‌بندی استفاده می‌شود. در مورد دوم، ممکن است دسته‌بندی در شکل کوتاه خود بازیابی شده باشد و بنابراین فیلد [produits] حاوی مرجعی به یک متد پروکسی است. استفاده از این مورد با Hibernate (هایبرنت) سپس باعث ایجاد یک استثنا (exception) می‌شود، زیرا کاتگوری مورد استفاده دیگر در زمینه پایداری JPA قرار ندارد، که با پایان یافتن تراکنش متدی که نسخه‌های کوتاه کاتگوری‌ها را بازیابی کرده بود، بسته شده است. سپس در خط ۸، از فیلد [EntityType] موجودیت [Categorie] استفاده می‌کنیم تا مشخص کنیم که آیا می‌توانیم به لیست محصولات در آن دسته دسترسی پیدا کنیم یا خیر؛
  • خط ۱۴: محصول به دسته‌بندی خود متصل می‌شود. به طور معمول، این باید از قبل انجام شده باشد. با این حال، ما نمی‌دانیم این محصول چگونه ایجاد شده یا اینکه آیا به دسته‌بندی خود متصل شده است یا خیر. بنابراین، برای جلوگیری از هرگونه مشکل (تا بتوان موجودیت [Produit] را مدیریت کرد، JPA نیاز دارد به موجودیتی که به آن متصل است یعنی [Categorie] ارجاع دهد)، ما خودمان این ارتباط را برقرار می‌کنیم.

با مقایسه این کد با کد کلاس [DaoProduit] در پیاده‌سازی Spring JDBC (به بخش 4.9 مراجعه کنید) می‌توانیم ببینیم که کتابخانه Spring Data JPA نوشتن لایه [DAO] را به شدت ساده می‌کند.

6.4.4.4. کلاس [DaoProduit]

  

کلاس [DaoProduit] رابط [IDao<Produit>] را به شرح زیر پیاده‌سازی می‌کند:


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) {
        // احیای (در صورت لزوم) پیوند بین یک محصول و دسته‌بندی آن
        for (Produit produit : entities) {
            if (produit.getEntityType() == EntityType.POJO) {
                produit.setCategorie(new Categorie(produit.getIdCategorie(), 0L, null, null));
            }
        }
        //محصولات ذخیره می‌شوند
        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 مستلزم آن است که هر انتهای [Produit] به یک انتهای [Categorie] متصل شود. از آنجایی که نمی‌دانیم آیا کاربر این کار را انجام داده است یا خیر، ما خودمان این کار را در خطوط 110–113 انجام می‌دهیم. تنها کاری که باید انجام دهیم این است که [Produit] را به یک موجودیت [Categorie] متصل کنیم که کلید اصلی آن با فیلد [idCategorie] از [Produit] مطابقت داشته باشد. هنگام تست متوجه می‌شویم که اگر null را برای نسخهٔ دسته‌بندی تنظیم کنیم، خطایی رخ می‌دهد. بنابراین در اینجا مقدار آن را روی 0 قرار می‌دهیم، اما می‌توانیم هر مقداری که بخواهیم برای آن تعیین کنیم. به جز کلید اصلی، هیچ فیلدی از انتیت [Categorie] برای درج یا به‌روزرسانی انتیت [Produit] توسط لایه JPA مورد نیاز نیست؛

6.4.5. لایهٔ آزمایشی

  

تست‌های فوق با تست‌های پیاده‌سازی Spring JDBC یکسان هستند. در صورت لزوم به صفحات زیر مراجعه کنید:

  • [JUnitTestCheckArguments]: بخش 4.11.1;
  • [JUnitTestDao]: بند 4.11.2;
  • [JUnitTestPushTheLimits]: بخش 4.11.3;

ما از پیکربندی‌های اجرایی زیر استفاده می‌کنیم:

نتایج حاصل از آزمون‌های مختلف به شرح زیر است:

در [1]، آزمون [JUnitTestPushTheLimits] از پیاده‌سازی Spring Data Hibernate JPA استفاده می‌کند، در حالی که [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 {

    //لایه [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    @Before
    public void clean() {
        // پایگاه داده قبل از هر آزمون پاک می‌شود
        log("Vidage de la base de données", 1);
        //جدول [CATEGORIES] تخلیه می‌شود و جدول [PRODUITS] به‌صورت آبشاری تخلیه می‌شود
        daoCategorie.deleteAllEntities();
    }

    @Test
    public void doNothing() {
        System.out.println("doNothing");
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        //جداول پر می‌شوند
        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);
        }
        // کategori را اضافه کنید – محصولات نیز به‌صورت آبشاری درج خواهند شد
        //وارد شده
        daoCategorie.saveEntities(categories);
        // نتیجه
        return categories;
    }

    @Test
    public void getShortCategoriesByName1() {
        // پر کردن
        fill(1, 1);
        // آزمایش
        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() {
        //پر کردن
        fill(1, 1);
        // آزمایش
        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() {
        // پر کردن
        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() {
        // پر کردن
        fill(1, 1);
        // آزمایش
        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) {
        // نمایش پیام
        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] در هر دو مورد (خطوط ۷ و ۱۷) رخ می‌دهد.