Skip to content

11. [Cours]: مدیریت پایگاه‌های داده رابطه‌ای با Spring Data

کلیدواژه‌ها: معماری چندلایه، اسپرینگ، تزریق وابستگی، API JPA (پایداری جاوا API)، اسپرینگ دیتا.

ما لایه [DAO] از TD را با استفاده از [Spring Data]، شاخه‌ای از اکوسیستم Spring، پیاده‌سازی خواهیم کرد. [Spring Data] بر لایه JPA متکی است (Java Persistence API) که به لایه [DAO] امکان می‌دهد تا به جای دستورات SQL، اشیاء را دستکاری کند. در نهایت، لایه [DAO] از اینکه در حال ارتباط با یک پایگاه داده است، بی‌خبر است. این لایه تنها رابط لایه [Spring Data] را تشخیص می‌دهد.

ابتدا [Spring Data] را با دو مثال بررسی خواهیم کرد.

11.1. Support

  • در [1]، پوشه [support / chap-11] شامل سه پروژه Eclipse است؛
  • در [2]، اسکریپت SQL برای ایجاد پایگاه دادهٔ نمونهٔ این فصل استفاده می‌شود؛

11.2. مثال ۱

وب‌سایت Spring حاوی آموزش‌های متعددی است که به شما کمک می‌کند کار با Spring [http://spring.io/guides] را آغاز کنید. ما از یکی از این آموزش‌ها برای معرفی Spring Data استفاده خواهیم کرد. برای این کار، از Spring Tool Suite (STS) استفاده می‌کنیم.

  • در [1]، ما یکی از آموزش‌های موجود در [spring.io/guides] را وارد می‌کنیم؛
  • در [2]، ما آموزش [Accessing Data Jpa] را انتخاب می‌کنیم که نشان می‌دهد چگونه با استفاده از Spring Data به یک پایگاه داده دسترسی پیدا کنیم؛
  • در [3]، ما پروژه‌ای را که توسط Maven پیکربندی شده است، انتخاب می‌کنیم؛
  • در [4]، آموزش در دو قالب در دسترس است: [initial] که نسخه‌ای خالی است و با دنبال کردن آموزش تکمیل می‌شود، یا [complete] که نسخه نهایی آموزش است. ما دومی را انتخاب می‌کنیم؛
  • در [5]، می‌توانید انتخاب کنید که آموزش را در یک مرورگر مشاهده کنید؛
  • [6] شامل پروژه نهایی است.

11.2.1. پیکربندی Maven پروژه

وابستگی‌های Maven پروژه در فایل [pom.xml] پیکربندی شده‌اند:


    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.1.10.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!-- برای همه چیز از UTF-8 استفاده کنید -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <start-class>hello.Application</start-class>
</properties>
  • خطوط ۵–۹: تعریف یک پروژه Maven والد. این کار بیشتر وابستگی‌های پروژه را مشخص می‌کند. این وابستگی‌ها ممکن است کافی باشند، که در این صورت هیچ وابستگی دیگری اضافه نمی‌شود، یا ممکن است کافی نباشند، که در این صورت وابستگی‌های missing اضافه می‌شوند؛
  • خطوط ۱۲–۱۵: وابستگی به [spring-boot-starter-data-jpa] را تعریف می‌کند. این آرتیفکت شامل کلاس‌های Spring Data است؛
  • خطوط 16–19: یک وابستگی به SGBD و H2 تعریف کنید که امکان ایجاد و مدیریت پایگاه‌های داده در حافظه را فراهم می‌کنند.

بیایید نگاهی به کلاس‌های ارائه‌شده توسط این وابستگی‌ها بیندازیم:

تعداد زیادی از آن‌ها وجود دارد:

  • برخی به اکوسیستم Spring (همان‌هایی که با «spring» شروع می‌شوند) تعلق دارند؛
  • سایر متعلق به اکوسیستم هایبرنیت (hibernate, jboss) هستند که ما در اینجا پیاده‌سازی JPA آن را استفاده می‌کنیم؛
  • سایر کتابخانه‌ها مربوط به تست هستند (junit، hamcrest
  • سایر کتابخانه‌های لاگ‌گیری هستند (log4j، logback، slf4j

ما قصد داریم همه‌ی آن‌ها را نگه داریم. برای یک برنامه‌ی تولیدی، باید فقط آن‌هایی را که ضروری هستند نگه داریم.

در خط ۲۶ فایل [pom.xml]، خط زیر را می‌یابیم:


<start-class>hello.Application</start-class>

این خط به خطوط زیر مرتبط است:


<build>
        <plugins>
            <plugin> 
                <artifactId>maven-compiler-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

خطوط ۶–۹: افزونه [spring-boot-maven-plugin] برای تولید فایل JAR قابل اجرای برنامه استفاده می‌شود. بنابراین خط ۲۶ از فایل [pom.xml] کلاس قابل اجرا را در این JAR مشخص می‌کند.

11.2.2. لایه [JPA]

دسترسی به پایگاه داده از طریق لایه [JPAJava Persistence API:

  

این برنامه ساده است و مشتریان [Customer] را مدیریت می‌کند. کلاس [Customer] بخشی از لایه [JPA] را تشکیل می‌دهد و به شرح زیر است:


package hello;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Customer {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    private String firstName;
    private String lastName;

    protected Customer() {
    }

    public Customer(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    @Override
    public String toString() {
        return String.format("Customer[id=%d, firstName='%s', lastName='%s']", id, firstName, lastName);
    }

}

یک مشتری دارای شناسه [id]، نام [firstName] و نام خانوادگی [lastName] است. هر نمونه [Customer] نمایانگر یک سطر در یک جدول پایگاه داده است.

  • خط ۸: anotation JPA، که به این معنی است که پایداری نمونه‌های [Customer] (ایجاد، خواندن، به‌روزرسانی، حذف) توسط یک پیاده‌سازی JPA مدیریت خواهد شد. طبق وابستگی‌های Maven، می‌توان دید که پیاده‌سازی JPA / Hibernate در حال استفاده است؛
  • خطوط ۱۱–۱۲: حاشیه‌نویسی‌های JPA که فیلد [id] را با کلید اصلی جدول [Customer] مرتبط می‌کنند. خط ۱۲ نشان می‌دهد که پیاده‌سازی JPA از روش تولید کلید اصلی خاص SGBD مورد استفاده، در این مورد H2، استفاده خواهد کرد؛

هیچ یادداشت دیگری برای JPA وجود ندارد. بنابراین از مقادیر پیش‌فرض استفاده خواهد شد:

  • جدول مربوط به [Customer] به نام کلاس نام‌گذاری خواهد شد، یعنی [Customer
  • ستون‌های این جدول بر اساس فیلدهای کلاس نام‌گذاری خواهند شد: [id, firstName, lastName]، با در نظر گرفتن اینکه در نام‌های ستون جدول به حروف بزرگ و کوچک توجه نمی‌شود؛

شایان ذکر است که پیاده‌سازی JPA هرگز نام‌گذاری نمی‌شود.

11.2.3. لایه [Spring Data]

کلاس [CustomerRepository] لایه دسترسی به جدول [Customer] را پیاده‌سازی می‌کند. کد آن به شرح زیر است:

  

package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}

بنابراین این یک رابط است و نه یک کلاس (خط ۷). این رابط، رابط [CrudRepository] را که یک رابط Spring Data است (خط ۵)، گسترش می‌دهد. این رابط توسط دو نوع تعریف می‌شود: اولی نوع عناصر مدیریت‌شده است، در این مورد نوع [Customer]؛ دومی نوع کلید اصلی عناصر مدیریت‌شده است، در این مورد نوع [Long]. رابط [CrudRepository] به شرح زیر است:


package org.springframework.data.repository;

import java.io.Serializable;

@NoRepositoryBean
public interface CrudRepository<T, ID extends Serializable> extends Repository<T, ID> {

    <S extends T> S save(S entity);

    <S extends T> Iterable<S> save(Iterable<S> entities);

    T findOne(ID id);

    boolean exists(ID id);

    Iterable<T> findAll();

    Iterable<T> findAll(Iterable<ID> ids);

    long count();

    void delete(ID id);

    void delete(T entity);

    void delete(Iterable<? extends T> entities);

    void deleteAll();
}

این رابط عملیات CRUD (ایجاد – خواندن – به‌روزرسانی – حذف) را که می‌توان روی یک نوع JPA T انجام داد، تعریف می‌کند:

  • خط ۸: متد save اجازه می‌دهد یک موجودیت T در پایگاه داده پایدارسازی شود. این متد موجودیت را با استفاده از کلید اصلی که توسط SGBD به آن اختصاص داده شده است، پایدارسازی می‌کند. همچنین اجازه می‌دهد یک موجودیت T، که با کلید اصلی id شناسایی می‌شود، به‌روزرسانی شود. انتخاب بین این دو عمل به مقدار کلید اصلی id بستگی دارد: اگر مقدار آن null باشد، عملیات پایداری انجام می‌شود؛ در غیر این صورت، عملیات به‌روزرسانی انجام می‌شود؛
  • خط ۱۰: همین امر صدق می‌کند، اما برای فهرستی از موجودیت‌ها؛
  • خط ۱۲: متد findOne برای بازیابی یک موجودیت T که با کلید اصلی id شناسایی شده است، استفاده می‌شود؛
  • خط ۲۲: متد حذف برای حذف یک موجودیت T که با کلید اصلی آن id شناسایی شده است، استفاده می‌شود؛
  • خطوط ۲۴–۲۸: گونه‌های متد [delete
  • خط 16: متد [findAll] تمام اشیاء پایدار T را بازیابی می‌کند؛
  • خط ۱۸: مشابه، اما محدود به آن انتهادی‌هایی که فهرستی از شناسه‌ها برای آن‌ها ارائه شده است؛

بیایید به رابط [CustomerRepository] بازگردیم:


package hello;

import java.util.List;

import org.springframework.data.repository.CrudRepository;

public interface CustomerRepository extends CrudRepository<Customer, Long> {

    List<Customer> findByLastName(String lastName);
}
  • خط ۹ به شما امکان می‌دهد یک [Customer] را با نام [lastName] آن بازیابی کنید؛

و این تمام چیزی است که در مورد لایه [DAO] وجود دارد. هیچ کلاس پیاده‌سازی‌ای برای رابط قبلی وجود ندارد. این توسط [Spring Data] در زمان اجرا تولید می‌شود. متدهای رابط [CrudRepository] به‌طور خودکار پیاده‌سازی می‌شوند. در مورد متدهای اضافه شده به رابط [CustomerRepository]، بستگی دارد. بیایید به تعریف [Customer] بازگردیم:


private long id;
private String firstName;
private String lastName;

متد در خط ۹ به‌طور خودکار توسط [Spring Data] پیاده‌سازی می‌شود زیرا به فیلد [lastName] (خط ۳) از [Customer] ارجاع می‌دهد. وقتی Spring Data با متد [findBySomething] در رابطی که باید پیاده‌سازی شود مواجه می‌شود، آن را با استفاده از پرس‌وجوی زیر JPQL (زبان پرس‌وجوی پایداری جاوا) پیاده‌سازی می‌کند:

select t from T t where t.something=:value

بنابراین نوع T باید دارای فیلدی با نام [something] باشد. بدین ترتیب، متد

List<Customer> findByLastName(String lastName);

پیاده‌سازی خواهد شد با استفاده از کدی مشابه کد زیر:

return [em].createQuery("select c from Customer c where  c.lastName=:value").setParameter("value",lastName).getResultList()

که در آن [em] به زمینه پایداری JPA اشاره دارد. این تنها در صورتی امکان‌پذیر است که کلاس [Customer] دارای فیلدی به نام [lastName] باشد، که چنین است.

در نتیجه، در موارد ساده، Spring Data به ما امکان می‌دهد لایه [DAO] را با استفاده از یک رابط ساده پیاده‌سازی کنیم.

11.2.4. لایه [console]

  

کلاس [Application] به شرح زیر است:


package hello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    CustomerRepository repository;
    
    public static void main(String[] args) {
        SpringApplication.run(Application.class);
    }

    @Override
    public void run(String... strings) throws Exception {
        // ذخیره چند مشتری
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        // بازیابی همه مشتریان
        System.out.println("Customers found with findAll():");
        System.out.println("-------------------------------");
        for (Customer customer : repository.findAll()) {
            System.out.println(customer);
        }
        System.out.println();

        //بازیابی یک مشتری منفرد با ID
        Customer customer = repository.findOne(1L);
        System.out.println("Customer found with findOne(1L):");
        System.out.println("--------------------------------");
        System.out.println(customer);
        System.out.println();

        //بازیابی مشتریان بر اساس نام خانوادگی
        System.out.println("Customer found with findByLastName('Bauer'):");
        System.out.println("--------------------------------------------");
        for (Customer bauer : repository.findByLastName("Bauer")) {
            System.out.println(bauer);
        }
    }

}
  • خط ۹: این کلاس رابط [CommandLineRunner] را پیاده‌سازی می‌کند، که رابطی از [Spring Boot] (خط ۴) است. این رابط تنها یک متد دارد، همان متد موجود در خط ۱۹؛
  • خط ۸: @SpringBootApplication یک آنوتیشن است که شامل چندین آنوتیشن [Spring Boot] می‌باشد:
    • @Configuration: نشان می‌دهد که این کلاس یک کلاس پیکربندی است؛
    • @EnableAutoConfiguration: به [Spring Boot] دستور می‌دهد که بر اساس ویژگی‌های مختلف، به‌ویژه محتوای کلاس‌پث پروژه، تعداد مشخصی از بیین‌ها را خود ایجاد کند. از آنجا که کتابخانه‌های Hibernate در classpath قرار دارند، bean با استفاده از Hibernate پیاده‌سازی خواهد شد. از آنجا که کتابخانه SGBD در مسیر کلاس (classpath) قرار دارد، بین H2 با استفاده از [dataSource] پیاده‌سازی خواهد شد. در بیون [dataSource]، نام کاربری و رمز عبور نیز باید تعریف شوند. در اینجا، Spring Boot از مدیر پیش‌فرض H2 که رمز عبور ندارد، استفاده خواهد کرد. از آنجایی که کتابخانه [spring-tx] در مسیر کلاس (classpath) قرار دارد، از مدیر تراکنش Spring استفاده خواهد شد؛
    • @EnableWebMvc: اگر کتابخانه [spring-mvc] در مسیر کلاس (classpath) موجود باشد. در این صورت، برنامه وب به‌طور خودکار پیکربندی می‌شود؛
    • @ComponentScan: که به Spring می‌گوید کجا به دنبال سایر beans، پیکربندی‌ها و سرویس‌ها بگردد. به طور پیش‌فرض، این موارد در بسته‌ای که شامل کلاس دارای آنوتیشن است، یعنی بسته [hello]، جستجو می‌شوند. بنابراین، کلاس‌های [Customer] و [CustomerRepository] پیدا خواهند شد. از آنجایی که اولی دارای آنوتیشن [@Entity] است، به عنوان یک انتیتا (entity) برای مدیریت توسط Hibernate کاتالوگ خواهد شد. از آنجایی که دومی اینترفیس [CrudRepository] را گسترش می‌دهد، به عنوان یک بین (bean) بهار (Spring) ثبت خواهد شد؛
  • خطوط ۱۱–۱۲: بین [CustomerRepository] به کد کلاس اصلی تزریق می‌شود؛
  • خط ۱۵: متد استاتیک [run] از کلاس [SpringApplication] در پروژه Spring Boot اجرا می‌شود. پارامتر آن کلاسی است که دارای انوتیشن [Configuration] یا [EnableAutoConfiguration] است. سپس همه چیزهایی که قبلاً توضیح داده شد، انجام می‌شود. نتیجه یک زمینهٔ برنامهٔ Spring است، یعنی مجموعه‌ای از بین‌ها که توسط Spring مدیریت می‌شوند؛

عملیات بعدی به سادگی از متدهای بیانی که رابط [CustomerRepository] را پیاده‌سازی می‌کند، استفاده می‌کنند. خروجی کنسول به شرح زیر است:

.   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.2.2.RELEASE)

2015-03-10 15:35:43.661  INFO 5784 --- [           main] hello.Application                        : Starting Application on Gportpers3 with PID 5784 (started by ST in C:\Users\Serge Tahé\Documents\workspace-sts-3.6.3.RELEASE\gs-accessing-data-jpa-complete)
2015-03-10 15:35:43.708  INFO 5784 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:45.230  INFO 5784 --- [           main] j.LocalContainerEntityManagerFactoryBean : Building JPA container EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:45.254  INFO 5784 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2015-03-10 15:35:45.331  INFO 5784 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {4.3.8.Final}
2015-03-10 15:35:45.332  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2015-03-10 15:35:45.334  INFO 5784 --- [           main] org.hibernate.cfg.Environment            : HHH000021: Bytecode provider name : javassist
2015-03-10 15:35:45.651  INFO 5784 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
2015-03-10 15:35:45.754  INFO 5784 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2015-03-10 15:35:45.877  INFO 5784 --- [           main] o.h.h.i.ast.ASTQueryTranslatorFactory    : HHH000397: Using ASTQueryTranslatorFactory
2015-03-10 15:35:46.154  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:46.169  INFO 5784 --- [           main] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
2015-03-10 15:35:46.779  INFO 5784 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']
2015-03-10 15:35:47.040  INFO 5784 --- [           main] hello.Application                        : Started Application in 3.623 seconds (JVM running for 4.324)
2015-03-10 15:35:47.042  INFO 5784 --- [       Thread-1] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@5d11346a: startup date [Tue Mar 10 15:35:43 CET 2015]; root of context hierarchy
2015-03-10 15:35:47.044  INFO 5784 --- [       Thread-1] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
2015-03-10 15:35:47.046  INFO 5784 --- [       Thread-1] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2015-03-10 15:35:47.047  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000227: Running hbm2ddl schema export
2015-03-10 15:35:47.051  INFO 5784 --- [       Thread-1] org.hibernate.tool.hbm2ddl.SchemaExport  : HHH000230: Schema export complete
  • خطوط ۱–۸: لوگوی پروژه Spring Boot؛
  • خط ۹: کلاس [hello.Application] اجرا می‌شود؛
  • خط ۱۰: [AnnotationConfigApplicationContext] کلاسی است که رابط [ApplicationContext] اسپرینگ را پیاده‌سازی می‌کند. این یک کانtejner بین (bean) است؛
  • خط ۱۱: بیون [entityManagerFactory] با استفاده از کلاس [LocalContainerEntityManagerFactory]، یک کلاس Spring، پیاده‌سازی شده است؛
  • خط ۱۲: [Hibernate] ظاهر می‌شود. این پیاده‌سازی، JPA، است که انتخاب شده است؛
  • خط ۱۹: یک گویش Hibernate، به طور خاص نوع SQL، قرار است با SGBD استفاده شود. در اینجا، گویش [H2Dialect] نشان می‌دهد که Hibernate با SGBD و H2 کار خواهد کرد؛
  • سطور ۲۱–۲۲: پایگاه داده ایجاد می‌شود. جدول [CUSTOMER] ایجاد می‌شود. این بدان معناست که Hibernate برای تولید جدول‌ها از تعریف‌های JPA پیکربندی شده است؛ در اینجا، تعریف JPA از کلاس [Customer
  • خطوط ۲۶–۳۰: نتیجه متد [findAll] اینفریس؛
  • خط ۳۴: نتیجه متد [findOne] اینفریس؛
  • خطوط ۳۸–۳۹: نتایج متد [findByLastName
  • از خط ۴۱ به بعد: لاگ‌های مربوط به بسته‌شدن کانکست Spring.

11.2.5. پیکربندی دستی پروژه Spring Data

ما پروژه قبلی را در پروژه [gs-accessing-data-jpa-02] کپی می‌کنیم:

  

در این پروژه جدید، ما به پیکربندی خودکار ارائه‌شده توسط Spring Boot متکی نخواهیم بود. ما آن را به‌صورت دستی پیکربندی خواهیم کرد. این کار در صورتی مفید است که پیکربندی‌های پیش‌فرض با نیازهای ما سازگار نباشند.

ابتدا، وابستگی‌های لازم را در فایل [pom.xml] مشخص می‌کنیم:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-accessing-data-jpa-02</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- Spring Data -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- H2 پایگاه داده -->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <!--برای همه چیز از UTF-8 استفاده کنید -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </repository>
        <repository>
            <id>org.jboss.repository.releases</id>
            <name>JBoss Maven Release Repository</name>
            <url>https://repository.jboss.org/nexus/content/repositories/releases</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>https://repo.spring.io/libs-release</url>
        </pluginRepository>
    </pluginRepositories>

</project>
  • خطوط ۱۰–۱۴: پروژهٔ والد Maven که کتابخانه‌های آن را استفاده خواهیم کرد؛
  • خطوط ۱۸–۲۱: Spring Data، که برای دسترسی به پایگاه داده استفاده می‌شود؛
  • خطوط 23–26: پیاده‌سازی Hibernate از مشخصات JPA؛
  • خطوط ۲۸–۳۱: SGBD و H2؛
  • خطوط ۳۳–۳۶: پایگاه‌های داده اغلب با استخرهای اتصالات باز استفاده می‌شوند که از باز و بسته کردن مکرر اتصالات جلوگیری می‌کنند. در اینجا، پیاده‌سازی مورد استفاده [tomcat-jdbc] است؛

در پروژه جدید، انتیت [Customer] و اینترفیس [CustomerRepository] بدون تغییر باقی می‌مانند. ما کلاس [Application] را اصلاح خواهیم کرد که به دو کلاس تقسیم خواهد شد:

  • [Config]، که کلاس پیکربندی خواهد بود:
  • [Main]، که کلاس اجرایی خواهد بود؛
  

کلاس اجرایی [Application] اکنون به شرح زیر است:


package console;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import repositories.CustomerRepository;
import config.AppConfig;
import entities.Customer;

public class Application {
    public static void main(String[] args) {
        // آشکارسازی زمینهٔ Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        CustomerRepository repository = context.getBean(CustomerRepository.class);

        // ذخیره چند مشتری
        repository.save(new Customer("Jack", "Bauer"));
        repository.save(new Customer("Chloe", "O'Brian"));
        repository.save(new Customer("Kim", "Bauer"));
        repository.save(new Customer("David", "Palmer"));
        repository.save(new Customer("Michelle", "Dessler"));

        ...

        //بستن زمینه
        context.close();
    }
}
  • خط ۹: کلاس [Application] دیگر هیچ تذکری برای پیکربندی ندارد؛
  • خطوط ۳–۷: توجه داشته باشید که دیگر هیچ وارداتی از پکیج [Spring Boot] وجود ندارد؛
  • خط ۱۲: بین‌های Spring ایجاد می‌شوند. ما کانکست Spring را به دست می‌آوریم که حاوی ارجاعات به بین‌های ایجاد شده است؛
  • خط ۱۳: یک مرجع به باینی با نوع [CustomerRepository] درخواست می‌شود؛

کلاس [Config] که پروژه را پیکربندی می‌کند، به شرح زیر است:


package config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;
import org.springframework.transaction.annotation.EnableTransactionManagement;

//@EnableTransactionManagement
@EnableJpaRepositories(basePackages = { "repositories" })
@Configuration
// @ComponentScan(basePackages={"package1","package2"})
public class AppConfig {

    // پایگاه داده H2
    @Bean
    public DataSource dataSource() {
        //منبع داده TomcatJdbc
        DataSource dataSource = new DataSource();
        // پیکربندی دسترسی JDBC
        dataSource.setDriverClassName("org.h2.Driver");
        dataSource.setUrl("jdbc:h2:./demo");
        dataSource.setUsername("sa");
        dataSource.setPassword("");
        //یک اتصال اولیه باز
        dataSource.setInitialSize(1);
        // نتیجه
        return dataSource;
    }

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

    //EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan("entities");
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

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

}
  • خط 17: آناوتیشن [@EnableTransactionManagement] مشخص می‌کند که متدهای اینترفیس‌های [CrudRepository] باید در داخل یک تراکنش اجرا شوند. این بخش به صورت کامنت‌گذاری شده است زیرا این رفتار پیش‌فرض است؛
  • خط ۱۸: تگ [@EnableJpaRepositories] برای مشخص کردن دایرکتوری‌هایی که رابط‌های Spring Data با شناسه [CrudRepository] در آن‌ها قرار دارند، استفاده می‌شود. این رابط‌ها به کامپوننت‌های Spring تبدیل شده و در کانتکست Spring در دسترس خواهند بود؛
  • خط ۱۹: آناوتیشن [@Configuration] کلاس [Config] را به یک کلاس پیکربندی Spring تبدیل می‌کند؛
  • خط ۲۰: anotation [@ComponentScan] فهرست دایرکتوری‌هایی را که باید در آن‌ها به دنبال کامپوننت‌های Spring گشت، مشخص می‌کند. کامپوننت‌های Spring کلاس‌هایی هستند که با آنتیشن‌های Spring مانند @Service، @Component، @Controller و غیره تگ شده‌اند. در اینجا، آنتیشن‌های دیگری به جز آن‌هایی که در داخل کلاس [AppConfig] تعریف شده‌اند وجود ندارد، بنابراین آنتیشن کامنت شده است؛
  • خطوط ۲۴–۳۷: منبع داده، یعنی پایگاه داده H2 را تعریف می‌کنند. این @Bean annotation در خط ۲۵ است که شیء ایجادشده توسط این متد را به یک کامپوننت مدیریت‌شده توسط Spring تبدیل می‌کند. نام متد در اینجا می‌تواند هر چیزی باشد. با این حال، اگر EntityManagerFactory در خط 51 وجود نداشته باشد و از طریق پیکربندی خودکار تعریف شده باشد، باید [dataSource] نامیده شود؛
  • خط ۳۰: پایگاه داده با نام [demo] نام‌گذاری شده و در پوشه پروژه ایجاد خواهد شد؛
  • خطوط ۴۰–۴۷: پیاده‌سازی مورد استفاده JPA را تعریف می‌کند، در این مورد یک پیاده‌سازی Hibernate. نام متد در اینجا می‌تواند هر چیزی باشد؛
  • خط ۴۳: هیچ لاگی برای SQL ثبت نمی‌شود؛
  • خط ۴۴: پایگاه داده در صورت عدم وجود ایجاد خواهد شد؛
  • خطوط 50–58: متد EntityManagerFactory را که به پایداری JPA رسیدگی می‌کند، تعریف می‌کنند. این متد باید [entityManagerFactory] نامیده شود؛
  • خط ۵۱: متد دو پارامتر از همان نوع دو بیانی که قبلاً تعریف شده‌اند دریافت می‌کند. این‌ها سپس توسط اسپرینگ به عنوان پارامترهای متد نمونه سازی و تزریق می‌شوند؛
  • خط ۵۳: پیاده‌سازی JPA را که باید استفاده شود، مشخص می‌کند؛
  • خط ۵۴: دایرکتوری‌هایی را مشخص می‌کند که در آن‌ها می‌توان اشیاء JPA را یافت؛
  • خط ۵۵: منبع داده‌ای را که باید مدیریت شود مشخص می‌کند؛
  • خطوط ۶۱–۶۶: مدیر تراکنش. متد باید با نام [transactionManager] نامگذاری شود. این متد، بین (bean) موجود در خطوط ۵۱–۵۸ را به عنوان پارامتر دریافت می‌کند؛
  • خط ۶۴: مدیر تراکنش با EntityManagerFactory مرتبط است؛

روش‌های فوق را می‌توان به هر ترتیبی تعریف کرد.

اجرای پروژه نتایج یکسانی را تولید می‌کند. یک فایل جدید در پوشه پروژه ظاهر می‌شود: فایل پایگاه داده H2:

  

11.2.6. ایجاد یک آرشیو قابل اجرا

برای ایجاد یک آرشیو قابل اجرا از پروژه، مراحل زیر را دنبال کنید:

  • در [1]: یک پیکربندی اجرا ایجاد کنید؛
  • در [2]: از نوع [Java Application]
  • در [3]: پروژهٔ اجرایی را مشخص کنید (از دکمهٔ Browse استفاده کنید);
  • در [4]: کلاس قابل اجرا را مشخص می‌کند؛
  • در [5]: نام پیکربندی اجرا – می‌تواند هر چیزی باشد؛
  • در [6]: پروژه صادر می‌شود؛
  • در [7]: به صورت یک آرشیو اجرایی JAR؛
  • در [8]: مسیر و نام فایل اجرایی که باید ایجاد شود را مشخص می‌کند؛
  • در [9]: نام پیکربندی اجرای ایجاد شده در [5];
  • در [10]، آرشیو ایجاد شده؛

پس از انجام این کار، یک کنسول را در پوشه‌ای که آرشیو اجرایی را در خود دارد باز کنید:

.....\dist>dir
12/06/2014  09:11        15 104 869 gs-accessing-data-jpa-02.jar

آرشیو به صورت زیر اجرا می‌شود:


.....\dist>java -jar gs-accessing-data-jpa-02.jar

خروجی‌های نمایش‌داده‌شده در کنسول به شرح زیر است:

SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder برای جزئیات بیشتر.
mars 10, 2015 5:27:20 PM org.hibernate.jpa.internal.util.LogHelper logPersistenceUnitInformation
INFO: HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
mars 10, 2015 5:27:20 PM org.hibernate.Version logVersion
INFO: HHH000412: Hibernate Core {4.3.8.Final}
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment <clinit>
INFO: HHH000206: hibernate.properties not found
mars 10, 2015 5:27:20 PM org.hibernate.cfg.Environment buildBytecodeProvider
INFO: HHH000021: Bytecode provider name : javassist
mars 10, 2015 5:27:22 PM org.hibernate.annotations.common.reflection.java.JavaReflectionManager <clinit>
INFO: HCANN000001: Hibernate Commons Annotations {4.0.5.Final}
mars 10, 2015 5:27:22 PM org.hibernate.dialect.Dialect <init>
INFO: HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
mars 10, 2015 5:27:22 PM org.hibernate.hql.internal.ast.ASTQueryTranslatorFactory <init>
INFO: HHH000397: Using ASTQueryTranslatorFactory
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000228: Running hbm2ddl schema update
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000102: Fetching database metadata
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000396: Updating schema
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.DatabaseMetadata getTableMetadata
INFO: HHH000262: Table not found: Customer
mars 10, 2015 5:27:22 PM org.hibernate.tool.hbm2ddl.SchemaUpdate execute
INFO: HHH000232: Schema update complete
Customers found with findAll():
-------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=2, firstName='Chloe', lastName='O'Brian']
Customer[id=3, firstName='Kim', lastName='Bauer']
Customer[id=4, firstName='David', lastName='Palmer']
Customer[id=5, firstName='Michelle', lastName='Dessler']

Customer found with findOne(1L):
--------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']

Customer found with findByLastName('Bauer'):
--------------------------------------------
Customer[id=1, firstName='Jack', lastName='Bauer']
Customer[id=3, firstName='Kim', lastName='Bauer']

11.3. مثال ۲

11.3.1. مقدمه

ما از مثال جدول محصولات که برای معرفی API و JDBC استفاده کردیم، برای ایجاد معماری زیر استفاده خواهیم کرد:

پایگاه داده [dbintrospringjpa] شامل دو جدول است: [PRODUITS] و [CATEGORIES]. جدول [CATEGORIES] به شرح زیر است:

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

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

 
  • [ID]: کلید اصلی در حالت AUTO_INCREMENT;
  • [VERSION]: شماره نسخه رکورد;
  • [NOM]: نام محصول – یکتا;
  • [ID_CATEGORIE]: شماره دسته‌بندی – کلید خارجی روی فیلد [CATEGORIES.ID];
  • [PRIX]: قیمت آن؛
  • [DESCRIPTION]: توضیحی درباره محصول؛

وظیفه: ایجاد پایگاه داده [dbintrospringdata] با استفاده از اسکریپت SQL [dbintrospringdata.sql] از مستندات پشتیبانی:


11.3.2. ایجاد پروژه Maven

برای ایجاد قالب پروژه Spring Data، مراحل زیر را دنبال کنید:

  • در [1]، یک پروژه جدید ایجاد کنید؛
  • در [2]: از نوع [Spring Starter Project];
  • پروژه تولیدشده یک پروژه Maven خواهد بود. در [3]، نام گروه پروژه را مشخص کنید؛
  • در [4]: نام محصول (در این مورد یک JAR) را که هنگام ساخت پروژه ایجاد می‌شود، مشخص کنید؛
  • در [5]: نام اکلیپس پروژه – این می‌تواند هر چیزی باشد (لازم نیست که با [4] یکسان باشد)؛
  • در [7]: مشخص کنید که یک پروژه باید با یک لایه [JPA] حاوی SGBD و MySQL ایجاد شود. وابستگی‌های مورد نیاز برای چنین پروژه‌ای سپس در فایل [pom.xml] گنجانده خواهند شد؛
  • در [8]، نام پوشه پروژه را وارد کنید؛
  • در [9]، جادوگر را تکمیل کنید؛
  • در [10]: پروژه ایجاد شد؛

فایل [pom.xml] شامل وابستگی‌های مورد نیاز برای پروژه JPA است:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.2.RELEASE</version>
        <relativePath/> <!-- جستجوی والد از مخزن -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <start-class>demo.IntroSpringData01Application</start-class>
        <java.version>1.7</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • خطوط ۱۴–۱۹: پروژهٔ والد Maven – تعداد زیادی کتابخانه را به همراه نسخه‌هایشان تعریف می‌کند – این کتابخانه‌ها به عنوان وابستگی‌های Maven بدون مشخص کردن نسخه‌هایشان استفاده می‌شوند؛
  • خطوط ۲۸–۳۱: وابستگی مورد نیاز JPA – این شامل [Spring Data] خواهد بود؛
  • خطوط ۳۲–۳۶: وابستگی به درایور JDBC از MySQL;
  • خطوط ۳۷–۴۱: وابستگی‌های مورد نیاز برای تست‌های JUnit یکپارچه شده با Spring؛

کلاس اجرایی [Application] هیچ کاری انجام نمی‌دهد اما از پیش پیکربندی شده است:


package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class IntroSpringData01Application {

    public static void main(String[] args) {
        SpringApplication.run(IntroSpringData01Application.class, args);
    }
}
  • علامت‌گذاری [@SpringBootApplication] این کلاس را به یک کلاس پیکربندی خودکار پروژه تبدیل می‌کند؛

کلاس تست [ApplicationTests] هیچ کاری انجام نمی‌دهد اما از پیش پیکربندی شده است:


package demo;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = IntroSpringData01Application.class)
public class IntroSpringData01ApplicationTests {

    @Test
    public void contextLoads() {
    }

}
  • خط ۹: آناوتیشن [@SpringApplicationConfiguration] امکان استفاده از فایل پیکربندی [Application] را فراهم می‌کند. بنابراین کلاس تست از تمام بی́n‌های تعریف‌شده در این فایل بهره‌مند خواهد شد؛
  • خط ۸: آناوتیشن [@RunWith] یکپارچه‌سازی Spring با JUnit را فعال می‌کند: این کلاس قادر خواهد بود به‌عنوان یک تست JUnit اجرا شود. [@RunWith] یک آنوتیشن JUnit است (خط ۴)، در حالی که کلاس [SpringJUnit4ClassRunner] یک کلاس Spring است (خط ۶)؛

اکنون که اسکلت برنامه JPA را داریم، می‌توانیم آن را تکمیل کنیم تا پروژه لایه پایداری مرتبط با پایگاه داده محصولات را بنویسیم.

11.3.3. پروژهٔ Eclipse

ما پروژه قبلی را به شرح زیر گسترش می‌دهیم:

  
  • [AppConfig.java]: کلاس پیکربندی پروژه Spring؛
  • [Main.java]: کلاس اجرایی پروژه؛
  • [IDao.java]: رابط لایه [DAO
  • [Dao.java]: کلاس پیاده‌سازی برای لایه [DAO
  • [AbstractEntity.java]: کلاس والد کلاس‌های [Produit] و [Categorie];
  • [Produit.java]: کلاسی که با یک سطر در جدول [PRODUITS] در پایگاه داده مرتبط است؛
  • [Categorie.java]: کلاسی که با یک سطر در جدول [CATEGORIES] در پایگاه داده مرتبط است؛
  • [ProduitsRepository]: رابط Spring Data برای دسترسی به جدول [PRODUITS];
  • [CategoriesRepository]: رابط Spring Data برای دسترسی به جدول [CATEGORIES];
  • [pom.xml]: فایل پیکربندی پروژه Maven؛

این پروژه معماری زیر را پیاده‌سازی می‌کند:

لایه [DAO] تنها لایه‌ای را می‌بیند که توسط [Spring Data] پیاده‌سازی شده است.

11.3.4. پیکربندی Maven

فایل [pom.xml] برای پروژه Maven به شرح زیر است:


<?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>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>intro-spring-data-01</name>
    <description>démo spring data avec table de produits</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <!-- اسپرینگ دیتا -->
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-jpa</artifactId>
        </dependency>
        <!-- Hibernate -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-entitymanager</artifactId>
        </dependency>
        <!-- MySQL پایگاه داده -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- Tomcat JDBC -->
        <dependency>
            <groupId>org.apache.tomcat</groupId>
            <artifactId>tomcat-jdbc</artifactId>
        </dependency>
        <!--کتابخانه jSON -->
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-core</artifactId>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
        </dependency>
        <!-- گوگل گوآوا -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
        </dependency>
        <!-- آزمون Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- کتابخانهٔ لاگ‌گیری -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
    </dependencies>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</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>

این پیکربندی همان موردی است که در بخش 11.2.5 استفاده و توضیح داده شده است. ما کتابخانه‌های زیر را به آن اضافه می‌کنیم:

  • خطوط ۴۲–۴۹: کتابخانه‌ای به نام jSON که توسط متد [toString] در کلاس [Produit] استفاده می‌شود؛
  • خطوط ۵۱–۵۵: کتابخانه [Google Guava]، که متدهای مفیدی برای مدیریت مجموعه‌های عناصر فراهم می‌کند. این کتابخانه توسط کلاس [Dao] که لایه [DAO] را پیاده‌سازی می‌کند، استفاده خواهد شد؛
  • خطوط ۵۶–۶۷: کتابخانه‌های مورد نیاز برای تست، JUnit;
  • خطوط ۶۹–۷۲: یک کتابخانهٔ لاگ؛
  • خطوط ۸۱–۸۶: پلاگین‌های Maven مورد نیاز برای پروژه؛

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

لایه

[DAO]

لایه

[console]

لایه

[JPA]

راننده

[JDBC]

لایه

[Spring Data]

اسپرینگ ۴

SGBD

  

11.3.5.1. کلاس [AbstractEntity]

کلاس [AbstractEntity] به شرح زیر است:


package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MappedSuperclass;
import javax.persistence.Version;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

@MappedSuperclass
public abstract class AbstractEntity {
    // ویژگی‌ها
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "ID")
    protected Long id;
    @Version
    @Column(name = "VERSION")
    protected Long version;

    // سازنده‌ها
    public AbstractEntity() {

    }

    public AbstractEntity(Long id, Long version) {
        this.id = id;
        this.version = version;
    }

    // override [equals] و [hashcode]
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

    @Override
    public boolean equals(Object entity) {
        if (!(entity instanceof AbstractEntity)) {
            return false;
        }
        String class1 = this.getClass().getName();
        String class2 = entity.getClass().getName();
        if (!class2.equals(class1)) {
            return false;
        }
        AbstractEntity other = (AbstractEntity) entity;
        return id != null && this.id.longValue() == other.id.longValue();
    }

    // امضا jSON
    public String toString() {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.writeValueAsString(this);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }
    
    // گیرنده و تنظیم‌کننده
....
}

هدف این کلاس فراهم کردن یک کلاس والد برای اشیاء JPA با محصور کردن ویژگی‌های [id, version] در یک مکان واحد است. (خطوط ۱۹ و ۲۲) که برای هر دو موجودیت [Produit] و [Categorie] که به پایگاه داده متصل هستند، مشترک است. این ویژگی‌ها به ستون‌های [ID, VERSION] در جداول (خطوط ۱۸ و ۲۱) متصل هستند.

  • خط ۱۳: حاشیه‌نویسی [@MappedSuperclass] نشان می‌دهد که این کلاس، کلاس والدِ موجودات JPA است؛
  • خط ۱۶: حاشیه‌نویسی [@Id] نشان می‌دهد که فیلد [id] (که ممکن است نام متفاوتی داشته باشد) با کلید اصلی یک جدول مرتبط است؛
  • خط ۱۷: انوتیشن [@GeneratedValue(strategy=GenerationType.IDENTITY)] حالت تولید کلیدهای اصلی را مشخص می‌کند. حالت [GenerationType.IDENTITY]، هنگامی که با MySQL استفاده شود، از حالت [AUTO_INCREMENT] بهره خواهد برد. با یک SGBD متفاوت، این حالت از یک روش متفاوت استفاده خواهد کرد. مزیت این است که توسعه‌دهنده نیازی به نگرانی در این مورد ندارد و کد او صرف‌نظر از SGBD استفاده‌شده، معتبر باقی می‌ماند؛
  • خط ۱۸: حاشیه‌نویسی [@Column] ستون مرتبط با فیلد را مشخص می‌کند. هنگامی که این حاشیه‌نویسی وجود ندارد، JPA فرض می‌کند که نام ستون با نام فیلد یکسان است. در اینجا نیز همین‌طور است. بنابراین، می‌توانست این حاشیه‌نویسی حذف شود؛
  • خط ۲۰: حاشیه‌نویسی [@Version] نشان می‌دهد که فیلد [version] با یک ستون نسخه‌بندی مرتبط است. پیاده‌سازی JPA هر بار که موجودیت اصلاح می‌شود، این شماره نسخه را افزایش خواهد داد. این عدد برای جلوگیری از به‌روزرسانی همزمان این موجودیت توسط دو کاربر مختلف استفاده می‌شود: دو کاربر، U1 و U2، موجودیت E را با شماره نسخه برابر با V1 می‌خوانند. U1، E را اصلاح می‌کند و این تغییر را در پایگاه داده ثبت می‌کند: شماره نسخه سپس به V1+1 تغییر می‌یابد. به نوبه خود، U2، E را اصلاح کرده و این تغییر را در پایگاه داده ثبت می‌کند: این یک استثنا (exception) را به وجود خواهد آورد زیرا نسخه آن (V1) با نسخه موجود در پایگاه داده (V1+1) متفاوت است؛
  • خطوط ۳۵–۵۲: تعریف مجدد متدهای [hashCode] و [equals]. به طور پیش‌فرض، [obj1.equals(obj2)] اگر [obj1==obj2] درست باشد، یعنی اگر ob1 و obj2 دو نشانگر برابر باشند، درست است. اگر می‌خواهید به جای خودِ اشاره‌گرها، اشیایی را که اشاره‌گرها به آن‌ها اشاره می‌کنند مقایسه کنید، باید متد [equals] و متد [hashCode] را بازنویسی (override) کنید. متد دوم باید برای دو شیئی که متد [equals] آن‌ها را برابر می‌داند، همان مقدار را بازگردانَد؛
  • خطوط ۴۲–۵۱: دو شیء از نوع [AbstractEntity] یا انواع مشتق شده در صورتی برابر در نظر گرفته می‌شوند که کلیدهای اصلی [id] آن‌ها برابر باشند؛
  • خطوط ۳۵–۳۸: متد [hashCode] در واقع برای دو شیء یکسان [AbstractEntity]، که در نتیجه کلید اصلی یکسانی [id] دارند، همان مقدار را برمی‌گرداند؛
  • خطوط ۵۵–۶۳: متد [toString] رشته jSon از شی [this] را بازمی‌گرداند. اگر این شیء به یک کلاس فرزند اشاره کند، این متد سپس رشته jSON را برای کلاس فرزند برمی‌گرداند. این بدان معناست که ما نیازی به ایجاد متد [toString] در کلاس‌های فرزند نداریم؛

11.3.5.2. این موجودیت JPA [Produit]

کلاس [Produit] یک انتیت (entity) از نوع JPA است که با یک سطر در جدول [PRODUITS] مرتبط است:

 

package spring.data.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "PRODUITS")
@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractEntity {

    // ویژگی‌ها
    @Column(name = "NOM")
    private String nom;

    @Column(name = "CATEGORIE_ID", insertable = false, updatable = false)    
    private Long idCategorie;

    @Column(name = "PRIX")
    private double prix;

    @Column(name = "DESCRIPTION")
    private String description;

    // کلاس
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "CATEGORIE_ID")    
    private Categorie categorie;

    // سازنده‌ها
    public Produit() {

    }

    public Produit(String nom, double prix, String description) {
        this.nom = nom;
        this.prix = prix;
        this.description = description;
    }

    // گیرنده و تنظیم‌کننده
...
}
  • ردیف ۱۲: حاشیه‌نویسی [@Entity] کلاس [Produit] را به‌عنوان یک موجودیت تحت مدیریت لایه [JPA] تعیین می‌کند؛
  • خط ۱۳: نشانه‌گذاری [@Table(name = "PRODUITS")] نشان می‌دهد که کلاس [Produit] ابجکت نماینده یک سطر در جدول [PRODUITS] در پایگاه داده است؛
  • خط 14: نام فیلتر jSON که باید بر روی اینتیتی اعمال شود. خواهیم دید که ویژگی [categorie] از خط 13 همیشه در دسترس نیست. بنابراین باید از نمایش شیء jSON حذف شود. برای این کار به یک فیلتر نیاز داریم. بنابراین در فیلتری به نام [jsonFilterCategorie] مشخص خواهیم کرد که آیا می‌خواهیم ویژگی [categorie] وجود داشته باشد یا خیر؛
  • خط ۱۸: حاشیه‌نویسی [@Column] فیلد [nom] را با ستون [NOM] در جدول [PRODUITS] مرتبط می‌سازد. وقتی فیلد نامی مشابه ستون مرتبط دارد، می‌توان حاشیه‌نویسی [@Column] را حذف کرد. در اینجا نیز همین‌طور است؛
  • خطوط ۳۱–۳۳: دسته‌بندی محصول؛
  • خط ۳۱: انوتیشن [@ManyToOne] نشان می‌دهد که ستون در انوتیشن در خط ۳۲، [@JoinColumn(name = "CATEGORIE_ID")]، یک کلید خارجی در جدول [PRODUITS] از نوعوجوده [Produit] در جدول [CATEGORIES] که با وجوده در خط ۳۳ مرتبط است. این حاشیه‌نویسی باید یک وجوده JPA را حاشیه‌نویسی کند. بنابراین، کلاس در سطر ۳۳ باید یک موجودیت JPA باشد؛
  • خط ۳۱: انوتیشن [fetch = FetchType.LAZY] مشخص می‌کند که وقتی یک محصول از جدول [PRODUITS] بازیابی می‌شود، دسته‌بندی آن (خط ۳۳) نباید بلافاصله بازیابی شود (بارگذاری تنبل). سپس در اولین فراخوانی متد [getCategorie] بازیابی می‌شود. این ویژگی الزامی نیست. پیاده‌سازی JPA مجاز است که آن را نادیده بگیرد. دلیل آن این است که ممکن است ویژگی [categorie] موجود باشد یا نباشد، از این رو ما فیلتر jSON را در خط 14 معرفی کرده‌ایم. پیاده‌سازی‌های موجود JPA (Hibernate، Eclipselink، OpenJPA) این anotation را به یک شکل مدیریت نمی‌کنند. Hibernate روش اصلی [getCategorie] را (که صرفاً فیلد categorie را بازمی‌گرداند) با فراخوانی SGBD برای بازیابی دسته‌بندی گسترش می‌دهد. برای اینکه این کار انجام شود، اتصال به SGBD—که در ابتدا برای بازیابی محصول استفاده می‌شد—باید همچنان باز باشد؛ در غیر این صورت، یک استثنا پرتاب می‌شود.

11.3.5.3. این موجودیت JPA [Categorie]

کلاس [Categorie] یک موجودیت JPA است که با یک سطر در جدول [CATEGORIES] مرتبط است:

 

کد آن به شرح زیر است:


package spring.data.entities;

import java.util.HashSet;
import java.util.Set;

import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.OneToMany;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFilter;

@Entity
@Table(name = "CATEGORIES")
@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractEntity {

    // ویژگی‌ها
    @Column(name = "NOM")
    private String nom;

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

    // سازنده‌ها
    public Categorie() {

    }

    public Categorie(String nom) {
        this.nom = nom;
    }

    // روش‌ها
    public void addProduit(Produit produit) {
        // افزودن محصول
        produits.add(produit);
        // کategori آن را تنظیم کنید
        produit.setCategorie(this);
    }

    //گیرنده و تنظیم‌کننده
...
}
  • خطوط ۲۱–۲۲: نام دسته‌بندی؛
  • خطوط ۲۵–۲۶: محصولات این دسته؛
  • خط ۲۵: حاشیه‌نویسی [@OneToMany] معکوس رابطه [@ManyToOne] است که در موجودیت [Produit] با آن مواجه شدیم. ویژگی [mappedBy = "categorie"] فیلد موجودیت [Produit] را که با رابطه معکوس [@ManyToOne] نشانه‌گذاری شده است، مشخص می‌کند. ویژگی [cascade = { CascadeType.ALL }] مشخص می‌کند که عملیات (persist، merge، remove) انجام‌شده بر روی یک @Entity [Categorie] باید به انتیته‌های [produits] در خط 26 سرایت کند. می‌توان آبشاری جزئی را با استفاده از ثابت [CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE] مشخص کرد؛
  • خط ۲۵: ویژگی [fetch = FetchType.LAZY] مشخص می‌کند که وقتی یک دسته‌بندی از جدول [CATEGORIES] بازیابی می‌شود، محصولات آن بلافاصله بازیابی نمی‌شوند. آنها در اولین فراخوانی متد [getProduits] بازیابی خواهند شد. پیاده‌سازی‌های موجود JPA (Hibernate، Eclipselink، OpenJPA) این anotation را به یک شکل مدیریت نمی‌کنند. هایبرنت متد اولیه [getProduits] را (که صرفاً فیلد produits را برمی‌گرداند) با فراخوانی SGBD برای بازیابی محصولات در آن دسته، گسترش می‌دهد. برای اینکه این امکان‌پذیر باشد، اتصال به SGBD که در ابتدا برای بازیابی دسته‌بندی استفاده شده بود، باید همچنان باز باشد. این یک الزام اجباری است. پیاده‌سازی JPA نمی‌تواند آن را نادیده بگیرد. از آنجایی که ممکن است ویژگی [produits] مقداردهی اولیه شود یا نشود، ما فیلتر jSON را در خط 17 معرفی کرده‌ایم که به ما امکان می‌دهد مشخص کنیم که آیا این ویژگی را می‌خواهیم یا نه؛
  • خط ۲۶: نوع [Set] یک رابط است. نوع [HashSet] کلاسی است که این رابط را پیاده‌سازی می‌کند. این کلاس یک مجموعه از عناصر به نام ensemble را پیاده‌سازی می‌کند. یک مجموعه نمی‌تواند شامل دو شیء یکسان باشد. در اینجا، اشیاء از نوع [Produit] هستند. بنابراین، مجموعه نمی‌تواند شامل دو شیء یکسان باشد. از آنجایی که متد [equals] در کلاس والد [AbstractEntity] مجدداً تعریف شده است تا بیان کند که دو محصول در صورتی یکسان هستند که کلید اصلی یکسانی داشته باشند، فیلد [produits] نمی‌تواند شامل دو محصول با کلید اصلی یکسان باشد؛
  • خطوط ۳۸–۴۳: متد [addProduit] اجازه می‌دهد یک محصول به دسته‌بندی اضافه شود؛

11.3.6. لایه [Spring Data]

لایه

[DAO]

لایه

[console]

لایه

[JPA]

ژاکت

[JDBC]

لایه

[Spring Data]

اسپرینگ ۴

SGBD

  

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


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Categorie;

public interface CategoriesRepository extends CrudRepository<Categorie, Long> {

    // دسته‌بندی با محصولاتش
    @Query("select c from Categorie c left join fetch c.produits p where c.id=?1")
    public Categorie getCategorieByIdWithProduits(Long id);

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

    //یک دسته‌بندی بدون محصولاتش، شناسایی‌شده با نامش
    public Categorie findByNom(String nom);
}
  • خط ۸: رابط [CrudRepository] در بخش ۱۱.۲.۳ استفاده و توضیح داده شد. باید توجه داشت که:
    • اولین نوع این رابط، انتیت JPA است که برای دسترسی به CRUD مدیریت می‌شود (findOne, findAll, ذخیره، حذف، deleteAll
    • نوع دوم، نوع کلید اصلی انتیت JPA است که در این مورد یک عدد صحیح [Long] است؛
  • خط ۱۲: متد در خط ۱۲ توسط پرس‌وجوی JPQL (زبان پرس‌وجوی پایداری جاوا) در خط ۱۱ پیاده‌سازی می‌شود. این پرس‌وجو اشیاء JPA را بازیابی می‌کند. در چنین پرس‌وجویی:
    • جدول‌ها با انتیته‌های مرتبط JPA جایگزین می‌شوند؛
    • ستون‌ها با فیلدهای موجود در انتیت‌های JPA مورد استفاده در پرس‌وجو جایگزین می‌شوند؛
  • خط ۱۱: پرس‌وجوی JPQL یک دسته‌بندی را به همراه محصولاتش بازمی‌گرداند. به یاد داشته باشید که در موجودیت [Categorie]، فیلد [produits] دارای ویژگی [fetch = FetchType.LAZY] بود. (بارگذاری تنبلانه). در پرس‌وجوی JPQL، ما با استفاده از کلمه کلیدی [fetch]، محصولات را مجبور به بارگذاری می‌کنیم. پارامتر ؟1 در پرس‌وجو در زمان اجرا با مقدار اولین پارامتر متد در خط 12، یعنی پارامتر [Long id]، جایگزین خواهد شد؛
  • خطوط ۱۴–۱۵: روشی مشابه برای یک دسته‌بندی که با نام آن شناسایی می‌شود؛
  • خط ۱۸: متد [findByNom] به‌طور خودکار توسط [Spring Data] پیاده‌سازی خواهد شد زیرا نوع [Category] دارای فیلدی به نام [nom] است؛

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


package spring.data.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.data.entities.Produit;

public interface ProduitsRepository extends CrudRepository<Produit, Long> {

    // محصول با دسته‌بندی خود
    @Query("select p from Produit p left join fetch p.categorie c where p.id=?1")
    public Produit getProduitByIdWithCategorie(Long id);

    @Query("select p from Produit p left join fetch p.categorie c where p.nom=?1")
    public Produit getProduitByNameWithCategorie(String nom);

    // یک محصول بدون دسته‌بندی، شناسایی‌شده با نامش
    public Produit findByNom(String nom);
}

توضیحات مشابه رابط [CategoriesRepository] است.

این رابط‌ها توسط کلاس‌هایی که هنگام اجرای پروژه توسط [Spring Data] تولید می‌شوند، پیاده‌سازی خواهند شد. به چنین کلاس‌هایی [proxy] گفته می‌شود. به طور پیش‌فرض، متدهای کلاس پیاده‌سازی در داخل یک تراکنش اجرا می‌شوند. این که این رابط‌ها کلاس [CrudRepository] را ارث می‌برند، آن‌ها را به کامپوننت‌های Spring تبدیل می‌کند.

11.3.7. لایه [DAO]

لایه

[DAO]

لایه

[console]

لایه

[JPA]

ژاکت

[JDBC]

پوشک

[Spring Data]

اسپرینگ ۴

SGBD

  

رابط [IDao] لایه [DAO] به شرح زیر است:


package spring.data.dao;

import java.util.List;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public interface IDao {

    // افزودن یک فهرست از محصولات
    public List<Produit> addProduits(List<Produit> produits);

    // حذف تمام محصولات
    public void deleteAllProduits();

    // به‌روزرسانی یک فهرست محصولات
    public List<Produit> updateProduits(List<Produit> produits);

    //بازیابی تمام محصولات
    public List<Produit> getAllProduits();

    // وارد کردن فهرست دسته‌بندی‌ها
    public List<Categorie> addCategories(List<Categorie> categories);

    // حذف تمام دسته‌بندی‌ها
    public void deleteAllCategories();

    //به‌روزرسانی فهرست دسته‌بندی‌ها
    public List<Categorie> updateCategories(List<Categorie> categories);

    // بازیابی همهٔ دسته‌بندی‌ها
    public List<Categorie> getAllCategories();

    //یک محصول خاص، با یا بدون دسته‌بندی آن
    public Produit getProduitByIdWithoutCategorie(Long idProduit);

    public Produit getProduitByIdWithCategorie(Long idProduit);

    public Produit getProduitByNameWithCategorie(String nom);

    public Produit getProduitByNameWithoutCategorie(String nom);

    //یک دسته‌بندی خاص، با یا بدون محصولات آن
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie);

    public Categorie getCategorieByIdWithProduits(Long idCategorie);

    public Categorie getCategorieByNameWithProduits(String nom);

    public Categorie getCategorieByNameWithoutProduits(String nom);
}

قانون پذیرفته‌شده در اینجا این است که هر متدی که اشیاء ارسال‌شده به‌عنوان پارامترهای ورودی را تغییر می‌دهد، باید آن‌ها را در نتیجه خود بازگرداند. دلیل این قانون در بخش 4.2 توضیح داده شد: این امر به یک لایه و مشتری آن اجازه می‌دهد تا در دو JVM جداگانه قرار گیرند و بدین ترتیب در پیکربندی مشتری/سرور عمل کنند.

پیاده‌سازی [Dao] این رابط به شرح زیر است:


package spring.data.dao;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.google.common.collect.Lists;

import spring.data.entities.Categorie;
import spring.data.entities.Produit;
import spring.data.repositories.CategoriesRepository;
import spring.data.repositories.ProduitsRepository;

@Component
public class Dao implements IDao {

    @Autowired
    private ProduitsRepository produitsRepository;

    @Autowired
    private CategoriesRepository categoriesRepository;

    @Override
    public List<Produit> addProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(101, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllProduits() {
        try {
            produitsRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(102, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> updateProduits(List<Produit> produits) {
        try {
            return Lists.newArrayList(produitsRepository.save(produits));
        } catch (Exception e) {
            throw new DaoException(103, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> addCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(104, getMessagesForException(e));
        }
    }

    @Override
    public void deleteAllCategories() {
        try {
            categoriesRepository.deleteAll();
        } catch (Exception e) {
            throw new DaoException(105, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> updateCategories(List<Categorie> categories) {
        try {
            return Lists.newArrayList(categoriesRepository.save(categories));
        } catch (Exception e) {
            throw new DaoException(106, getMessagesForException(e));
        }
    }

    @Override
    public List<Categorie> getAllCategories() {
        try {
            return Lists.newArrayList(categoriesRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(107, getMessagesForException(e));
        }
    }

    @Override
    public List<Produit> getAllProduits() {
        try {
            return Lists.newArrayList(produitsRepository.findAll());
        } catch (Exception e) {
            throw new DaoException(108, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithCategorie(Long idProduit) {
        try {
            return produitsRepository.getProduitByIdWithCategorie(idProduit);
        } catch (Exception e) {
            throw new DaoException(109, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithProduits(Long idCategorie) {
        try {
            return categoriesRepository.getCategorieByIdWithProduits(idCategorie);
        } catch (Exception e) {
            throw new DaoException(110, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithProduits(String nom) {
        try {
            return categoriesRepository.getCategorieByNameWithProduits(nom);
        } catch (Exception e) {
            throw new DaoException(111, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithCategorie(String nom) {
        try {
            return produitsRepository.getProduitByNameWithCategorie(nom);
        } catch (Exception e) {
            throw new DaoException(112, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByIdWithoutCategorie(Long idProduit) {
        try {
            return produitsRepository.findOne(idProduit);
        } catch (Exception e) {
            throw new DaoException(113, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByIdWithoutProduits(Long idCategorie) {
        try {
            return categoriesRepository.findOne(idCategorie);
        } catch (Exception e) {
            throw new DaoException(114, getMessagesForException(e));
        }
    }

    @Override
    public Produit getProduitByNameWithoutCategorie(String nom) {
        try {
            return produitsRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(115, getMessagesForException(e));
        }
    }

    @Override
    public Categorie getCategorieByNameWithoutProduits(String nom) {
        try {
            return categoriesRepository.findByNom(nom);
        } catch (Exception e) {
            throw new DaoException(116, getMessagesForException(e));
        }
    }

}
  • خط ۱۶: انوتیشن [@Component] کلاس [Dao] را به عنوان یک کامپوننت Spring مشخص می‌کند؛
  • خطوط ۱۹–۲۳: تزریق ارجاعات به دو رابط [CrudRepository] از [Spring Data]. این تزریق زمانی انجام می‌شود که اشیاء Spring ایجاد شوند، معمولاً در ابتدای اجرای پروژه Spring؛
  • توجه کنید که در خطوط ۲۸ و ۴۶ متد [save] از رابط [produitsRepository] برای هر دو عملیات درج و به‌روزرسانی محصولات استفاده می‌شود. [Spring Data] از کلید اصلی محصول برای تعیین اینکه آیا باید درج یا به‌روزرسانی انجام دهد، استفاده می‌کند. اگر کلید اصلی [null] باشد، عملیات درج خواهد بود؛ در غیر این صورت، عملیات به‌روزرسانی خواهد بود؛
  • خط ۸۲: متد [Lists.newArrayList] از کتابخانه Guava برای بازیابی فهرستی از محصولات استفاده می‌شود. متد [produitsRepository.findAll()] نوع [Iterable<Produit>] را برمی‌گرداند؛
  • خط ۲۸: متد [produitsRepository.save(produits)] یک نوع [Iterable<Produit>] را برمی‌گرداند. همین امر در مورد سایر عملیات [save] در کلاس نیز صدق می‌کند؛

در کلاس [Dao] بالا، استثناهایی که ممکن است رخ دهند در نوع زیر [DaoException] محصور شده‌اند:


package spring.data.dao;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

//کلاس استثنا برای برنامه انتخابات
// استثناء رسیدگی نشده است

public class DaoException extends RuntimeException implements Serializable {

    // سریالی ID
    private static final long serialVersionUID = 1L;

    // میدان‌های محلی
    private int code;
    private List<String> erreurs;

    // سازنده‌ها
    public DaoException() {
        super();
    }

    public DaoException(int code, Throwable e) {
        // والد
        super(e);
        // محلی
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message, Throwable e) {
        // والد
        super(message, e);
        // محلی
        this.code = code;
        this.erreurs = getErreursForException(e);
    }

    public DaoException(int code, String message) {
        // والد
        super(message);
        // محلی
        this.code = code;
        List<String> erreurs = new ArrayList<>();
        erreurs.add(message);
        this.erreurs = erreurs;
    }

    public DaoException(int code, List<String> erreurs) {
        // والد
        super();
        // محلی
        this.code = code;
        this.erreurs = erreurs;
    }

    //فهرست پیام‌های خطا برای یک استثنا
    private List<String> getErreursForException(Throwable th) {
        // فهرست پیام‌های خطا برای استثنا را بازیابی می‌کند
        Throwable cause = th;
        List<String> erreurs = new ArrayList<>();
        while (cause != null) {
            // پیام را تنها در صورتی بازیابی می‌کند که != null و خالی نباشد
            String message = cause.getMessage();
            if (message != null) {
                message = message.trim();
                if (message.length() != 0) {
                    erreurs.add(message);
                }
            }
            // علت بعدی
            cause = cause.getCause();
        }
        return erreurs;
    }

    // گیرنده و تنظیم‌کننده
...
}
  • خط ۱۰: این کلاس از کلاس [RuntimeException] ارث می‌برد و بنابراین یک استثنای رسیدگی‌نشده است؛
  • خط 16: یک کد خطا؛
  • خط 17: فهرستی از پیام‌های خطا، پیام‌های مرتبط با استک استثناهایی که باعث [DaoException] شده‌اند؛
  • خطوط ۵۹–۷۶: متد خصوصی [getMessagesForException] لیست پیام‌های خطا را که با استک استثناها مرتبط هستند، بازیابی می‌کند. در واقع می‌توان با استفاده از سازندهای زیر کلاس Exception، استثناها را روی هم قرار داد:
    • Exception(String message, Throwable cause): یک استثنا با یک پیام و استثنایی که باید دربرگرفته شود، ایجاد می‌کند؛
    • Exception(Throwable cause): یک استثنا ایجاد می‌کند که شامل استثنای مورد نظر برای جاسازی شدن است؛

نوع [Throwable] کلاس والد کلاس [Exception] است. اگر سازنده‌های فوق به طور مکرر اجرا شوند، استثنای نهایی حاوی چندین استثنا خواهد بود. به این حالت، پشته استثنا گفته می‌شود.

  • آخرین علت یک استثنا e1 با عبارت [e1.getCause()] به دست می‌آید؛
  • دومین علت یک استثنا e1 با عبارت [e1.getCause().getCause()] به دست می‌آید؛
  • و به همین ترتیب تا زمانی که [getCause()==null] به دست آید؛

11.3.8. پیکربندی پروژه Spring

  

کلاس [DaoConfig] لایه [DAO] را پیکربندی می‌کند:


package spring.data.config;

import javax.persistence.EntityManagerFactory;

import org.apache.tomcat.jdbc.pool.DataSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
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;

@EnableJpaRepositories(basePackages = { "spring.data.repositories" })
@Configuration
@ComponentScan(basePackages = { "spring.data.dao" })
public class DaoConfig {

    // ثوابت
    final static String URL = "jdbc:mysql://localhost:3306/dbIntroSpringData";
    final static String USER = "root";
    final static String PASSWD = "";
    final static String DRIVER_CLASSNAME = "com.mysql.jdbc.Driver";
    final static String[] ENTITIES_PACKAGES = { "spring.data.entities" };

    // منبع داده [tomcat-jdbc]
    @Bean
    public DataSource dataSource() {
        //منبع داده TomcatJdbc
        DataSource dataSource = new DataSource();
        // پیکربندی دسترسی JDBC
        dataSource.setDriverClassName(DRIVER_CLASSNAME);
        dataSource.setUsername(USER);
        dataSource.setPassword(PASSWD);
        dataSource.setUrl(URL);
        //یک اتصال در ابتدا باز
        dataSource.setInitialSize(1);
        // نتیجه
        return dataSource;
    }

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

    //EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

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

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}

یک پیکربندی مشابه در بخش 11.2.5 مورد بحث و توضیح قرار گرفت. ما حاشیه‌نویسی‌های زیر از Spring را اضافه کرده‌ایم:

  • خط 17: آناوتیشن [@EnableJpaRepositories] برای مشخص کردن پکیج‌های حاوی اینترفیس‌های [CrudRepository] و [Spring Data] استفاده می‌شود؛
  • خط ۱۸: این کلاس یک کلاس پیکربندی Spring است. این اطلاعات مهم است. اگر این بخش حذف شود، پروژه همچنان کار می‌کند. با این حال، در ادامه مستندات، هنگامی که پروژه‌هایی بر اساس این پروژه ساخته می‌شوند، برخی از آن‌ها دیگر کار نخواهند کرد اگر حاشیه نویسی روی خط ۱۸ حذف شود؛
  • خط ۱۹: آناوتیشن [@ComponentScan] بسته‌هایی را که اشیاء Spring در آن‌ها قرار دارند، مشخص می‌کند. این‌ها کلاس‌هایی هستند که با [@Component, @Service, @Controller, ...] آناوتیشن شده‌اند. در اینجا، کامپوننت Spring با نام [Dao] پیدا شده و نمونه آن ایجاد خواهد شد؛
  • خطوط ۷۳–۷۶: ما یک بین تعریف کرده‌ایم که نمایانگر آرایهٔ بسته‌هایی است که باید برای اشیاء JPA اسکن شوند. این امر به پروژه‌ای که کلاس [DaoConfig] را وارد می‌کند اجازه می‌دهد تا این بین را مجدداً تعریف کرده و در نتیجه بسته‌های در حال اسکن را تغییر دهد (خط 59). ما بعداً در این سند با این موضوع مواجه خواهیم شد؛

کلاس [AppConfig] کل پروژه را پیکربندی می‌کند:


package spring.data.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ser.impl.SimpleBeanPropertyFilter;
import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider;

@Configuration
@Import({DaoConfig.class})
public class AppConfig {
    // فیلترها jSON
    @Bean(name = "jsonMapper")
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean(name = "jsonMapperCategorieWithProduits")
    public ObjectMapper jsonMapperCategorieWithProduits() {
        // نقشه‌بردار jSON
        ObjectMapper mapper = new ObjectMapper();
        // فیلترها
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // نتیجه
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithCategorie")
    public ObjectMapper jsonMapperProduitWithCategorie() {
        // نقشه‌ساز jSON
        ObjectMapper mapper = new ObjectMapper();
        // فیلترها
        mapper.setFilters(
                new SimpleFilterProvider().addFilter("jsonFilterProduit", SimpleBeanPropertyFilter.serializeAllExcept())
                        .addFilter("jsonFilterCategorie", SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // نتیجه
        return mapper;
    }

    @Bean(name = "jsonMapperCategorieWithoutProduits")
    public ObjectMapper jsonMapperCategorieWithoutProduits() {
        // نقشه‌کش jSON
        ObjectMapper mapper = new ObjectMapper();
        // فیلترها
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        // نتیجه
        return mapper;
    }

    @Bean(name = "jsonMapperProduitWithoutCategorie")
    public ObjectMapper jsonMapperProduitWithoutCategorie() {
        // نقشه‌ریز jSON
        ObjectMapper mapper = new ObjectMapper();
        // فیلترها
        mapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        // نتیجه
        return mapper;
    }
}
  • خط ۱۱: این کلاس یک کلاس پیکربندی Spring است؛
  • خط ۱۲: که بیین‌های تعریف‌شده توسط کلاس [DaoConfig] را که همین‌الان دیدیم، وارد می‌کند؛
  • لایه [console] از mapperهای jSON که در اینجا تعریف شده‌اند، استفاده می‌کند؛
  • خطوط 14–64: پنج مپر jSON را تعریف می‌کنند؛
  • خطوط ۱۵–۱۸: فیلتر jSON [jsonMapper] هیچ فیلتری ندارد؛
  • خطوط ۲۰–۳۰: فیلترهای jSON و [jsonMapperCategorieWithProduits] اجازه می‌دهند یک شیء [Categorie] به همراه محصولات آن سریالیزه و دِسریالیزه شود؛
  • خطوط ۳۲–۴۲: فیلتر jSON [jsonMapperProduitWithCategorie] امکان سریالی‌سازی/دسریالی‌سازی یک شیء [Produit] را به همراه دسته‌بندی آن فراهم می‌کند؛
  • خطوط ۴۳–۵۳: فیلتر jSON [jsonMapperCategorieWithoutProduits] امکان سریالی‌سازی/دسریالی‌سازی یک شیء [Categorie] را بدون محصولات آن فراهم می‌کند؛
  • خطوط ۵۵–۶۴: فیلتر jSON [jsonMapperProduitWithoutCategorie] امکان سریالی‌سازی/دسریالی‌سازی یک شیء [Produit] را بدون دسته‌بندی آن فراهم می‌کند؛

باید توجه داشت که هنگام ساختن یک فیلتر jSON برای یک موجودیت T، باید نه تنها فیلتر مربوط به موجودیت T بلکه فیلترهای مربوط به موجودیت‌های Ti که ممکن است خود شامل آن‌ها باشد را نیز پیکربندی کرد.

11.3.9. لایه [console]

لایه

[DAO]

لایه

[console]

لایه

[JPA]

ژاکت

[JDBC]

لایه

[Spring Data]

اسپرینگ ۴

SGBD

  

کلاس [Main] به شرح زیر است:


package spring.data.console;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

public class Main {

    public static void main(String[] args) throws JsonProcessingException {
        AnnotationConfigApplicationContext context = null;
        try {
            // آشکارسازی زمینهٔ بهار
            context = new AnnotationConfigApplicationContext(AppConfig.class);
            ObjectMapper jsonMapperCategorieWithProduits = context.getBean("jsonMapperCategorieWithProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithCategorie = context.getBean("jsonMapperProduitWithCategorie",
                    ObjectMapper.class);
            ObjectMapper jsonMapperCategorieWithoutProduits = context.getBean("jsonMapperCategorieWithoutProduits",
                    ObjectMapper.class);
            ObjectMapper jsonMapperProduitWithoutCategorie = context.getBean("jsonMapperProduitWithoutCategorie",
                    ObjectMapper.class);
            IDao dao = context.getBean(IDao.class);
            // --------------------------------------------------------------------------------------
            // پایگاه داده خالی می‌شود
            log("Vidage de la base de données", 1);
            // جدول تخلیه می‌شود [CATEGORIES] – جدول [PRODUITS] به عنوان اثر آبشاری تخلیه خواهد شد
            dao.deleteAllCategories();
            // --------------------------------------------------------------------------------------
            log("Remplissage de la base", 1);
            //جداول پر شده‌اند
            List<Categorie> categories = new ArrayList<Categorie>();
            for (int i = 0; i < 2; i++) {
                Categorie categorie = new Categorie(String.format("categorie%d", i));
                for (int j = 0; j < 5; j++) {
                    categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                            String.format("desc%d%d", i, j)));
                }
                categories.add(categorie);
            }
            // افزودن دسته‌بندی – در نتیجه، محصولات نیز درج خواهند شد
            dao.addCategories(categories);
            // --------------------------------------------------------------------------------------
            log("Affichage de la base", 1);
            // فهرست دسته‌بندی‌ها
            log("Liste des catégories", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            //فهرست محصولات
            log("Liste des produits", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            // دسته‌بندی ۱ و محصولات آن
            Categorie categorie = dao.getCategorieByNameWithProduits("categorie1");
            log("Catégorie 1 avec ses produits", 2);
            affiche(categorie, jsonMapperCategorieWithProduits);
            //محصول [produit14] و دسته‌بندی آن
            Produit p = dao.getProduitByNameWithCategorie("produit14");
            log("Produit [produit14] avec sa catégorie", 2);
            affiche(p, jsonMapperProduitWithCategorie);
            // --------------------------------------------------------------------------------------
            log("Mise à jour du prix des produits de [categorie1]", 1);
            log("Produits de la catégorie [categorie1] avant la mise à jour", 2);
            Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
            Set<Produit> produits = categorie1.getProduits();
            affiche(categorie1, jsonMapperCategorieWithProduits);
            for (Produit produit : produits) {
                produit.setPrix(1.1 * produit.getPrix());
            }
            dao.updateProduits(Lists.newArrayList(produits));
            log("Produits de la catégorie [categorie1] après la mise à jour", 2);
            affiche(dao.getCategorieByNameWithProduits("categorie1"), jsonMapperCategorieWithProduits);
            // --------------------------------------------------------------------------------------
            log("Vidage de la base de données", 1);
            //جدول [CATEGORIES] پاکسازی شده است – در نتیجه، جدول [PRODUITS] نیز پاکسازی خواهد شد
            dao.deleteAllCategories();
            // نمایش پایگاه داده
            log("Liste des categories avant l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits avant l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
            log("Ajout d'une catégorie [cat1] avec deux produits de même nom", 1);
            // واردسازی انجام می‌شود
            categorie = new Categorie("cat1");
            categorie.addProduit(new Produit("x", 1.0, ""));
            categorie.addProduit(new Produit("x", 1.0, ""));
            // افزودن دسته‌بندی – در نتیجه، محصولات نیز درج خواهند شد
            try {
                dao.addCategories(Lists.newArrayList(categorie));
            } catch (DaoException e) {
                System.out.println(e);
            }
            //بررسی
            log("Liste des categories après l'ajout", 2);
            affiche(dao.getAllCategories(), jsonMapperCategorieWithoutProduits);
            log("Liste des produits après l'ajout", 2);
            affiche(dao.getAllProduits(), jsonMapperProduitWithoutCategorie);
        } catch (DaoException e) {
            System.out.println(e);
        } finally {
            if (context != null) {
                // انجام شد
                context.close();
            }
        }
        System.out.println("Travail terminé");
    }

    // نمایش یک عنصر از نوع T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // نمایش فهرستی از عناصر از نوع T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static 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);
    }
}
  • خط ۲۵: نمونه‌سازی Spring beans از کلاس پیکربندی [AppConfig];
  • خطوط 26–33: بازیابی ارجاعات به مپ‌کننده‌های jSON. از امضای زیر برای متد [ApplicationContext].getBean استفاده می‌شود:
    • [ApplicationContext].getBean(String id, Class class): که زمانی استفاده می‌شود که چندین bean از نوع [classe] وجود داشته باشد. در این صورت، شناسه‌ی bean درخواستی مشخص می‌شود. اگر بیون با استفاده از anotation [@Bean] تعریف شده باشد، شناسه‌ی آن نام متد anotation خورده است. اگر با استفاده از anotation [@Bean(« identifiant »] تعریف شده باشد، شناسه‌ی آن همان چیزی است که در anotation مشخص شده است؛
  • خط ۳۴: بازیابی یک مرجع به لایه [DAO
  • خطوط ۳۷–۳۹: خالی کردن پایگاه داده. جدول دسته‌بندی‌ها خالی می‌شود (خط ۳۹). این به این دلیل است که عبارت زیر نوشته شده بود:

    @OneToMany(fetch = FetchType.LAZY, mappedBy = "categorie", cascade = { CascadeType.ALL })
    public Set<Produit> produits = new HashSet<Produit>();

وقتی یک دسته‌بندی حذف می‌شود، تمام محصولات مرتبط با آن نیز حذف می‌شوند؛

  • خطوط ۴۳–۵۳: پر کردن جدول با دو دسته‌بندی که هر کدام شامل پنج محصول هستند. در خط ۵۰، درج این دو دسته‌بندی به طور همزمان محصولاتشان را نیز درج می‌کند، و این دوباره به این دلیل است که ما [cascade = { CascadeType.ALL }] را نوشته‌ایم؛
  • خط ۵۸: دسته‌بندی‌ها نمایش داده می‌شوند. مپر jSON [jsonMapperCategorieWithoutProduits] برای نمایش دسته‌بندی‌ها بدون محصولاتشان استفاده می‌شود. این به این دلیل است که متد [dao.getAllCategories()] دسته‌بندی‌ها را بدون محصولاتشان رندر می‌کند (بارگذاری تنبلانه)؛
  • خط ۶۱: محصولات بدون دسته‌بندی‌شان نمایش داده می‌شوند. این به این دلیل است که متد [dao.getAllProduits()] محصولات را بدون دسته‌بندی‌شان رندر می‌کند (بارگذاری تنبلانه)؛
  • خطوط ۶۳–۶۵: دسته با نام [categorie1] را به همراه محصولات آن نمایش می‌دهد (بارگذاری فوری)؛
  • خطوط ۶۷–۶۹: نمایش یک محصول همراه با دسته‌بندی آن؛
  • خطوط ۷۱–۸۱: قیمت تمام محصولات در دسته‌بندی [categorie1] به میزان ۱۰ درصد افزایش می‌یابد؛
  • خطوط ۹۱–۱۰۱: یک دسته با دو محصول با نام یکسان اضافه می‌شود. با این حال، در جدول [PRODUITS]، یک قید یکتایی (unique constraint) روی ستون [NOM] وجود دارد. بنابراین، درج محصول دوم رد شده و یک استثنا (exception) پرتاب می‌شود. با این حال، متد [dao.addProduits] در داخل یک تراکنش اجرا می‌شود. بنابراین، شکست دومین درج، باید درج محصول اول و همچنین درج دسته‌بندی آن [cat1] را نیز لغو کند. این چیزی است که می‌خواهیم تأیید کنیم؛
  • خطوط ۱۱۹–۱۲۱: یک متد عمومی که قادر به نمایش رشته jSON برای هر عنصر از نوع T است. سریالی‌سازی jSON توسط مپی که به‌عنوان پارامتر ارسال می‌شود کنترل می‌گردد؛
  • خطوط ۱۲۴–۱۲۸: متدی مشابه، این بار برای یک لیست از عناصر از نوع T؛

اجرای کلاس [Main] نتایج زیر را تولید می‌کند (بدون احتساب لاگ‌های Spring):


Vidage de la base de données --------------------------------
Remplissage de la base --------------------------------
Affichage de la base --------------------------------
-- Liste des catégories
{"id":4,"version":0,"nom":"categorie0"}
{"id":5,"version":0,"nom":"categorie1"}
-- Liste des produits
{"id":13,"version":0,"nom":"produit00","idCategorie":4,"prix":100.0,"description":"desc00"}
{"id":14,"version":0,"nom":"produit01","idCategorie":4,"prix":101.0,"description":"desc01"}
{"id":15,"version":0,"nom":"produit02","idCategorie":4,"prix":102.0,"description":"desc02"}
{"id":16,"version":0,"nom":"produit03","idCategorie":4,"prix":103.0,"description":"desc03"}
{"id":17,"version":0,"nom":"produit04","idCategorie":4,"prix":104.0,"description":"desc04"}
{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"}
{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"}
{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"}
{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"}
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}
-- Catégorie 1 avec ses produits
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produit [produit14] avec sa catégorie
{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14","categorie":{"id":5,"version":0,"nom":"categorie1"}}
Mise à jour du prix des produits de [categorie1] --------------------------------
-- Produits de la catégorie [categorie1] avant la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":0,"nom":"produit10","idCategorie":5,"prix":110.0,"description":"desc10"},{"id":19,"version":0,"nom":"produit11","idCategorie":5,"prix":111.0,"description":"desc11"},{"id":20,"version":0,"nom":"produit12","idCategorie":5,"prix":112.0,"description":"desc12"},{"id":21,"version":0,"nom":"produit13","idCategorie":5,"prix":113.0,"description":"desc13"},{"id":22,"version":0,"nom":"produit14","idCategorie":5,"prix":114.0,"description":"desc14"}]}
-- Produits de la catégorie [categorie1] après la mise à jour
{"id":5,"version":0,"nom":"categorie1","produits":[{"id":18,"version":1,"nom":"produit10","idCategorie":5,"prix":121.0,"description":"desc10"},{"id":19,"version":1,"nom":"produit11","idCategorie":5,"prix":122.1,"description":"desc11"},{"id":20,"version":1,"nom":"produit12","idCategorie":5,"prix":123.2,"description":"desc12"},{"id":21,"version":1,"nom":"produit13","idCategorie":5,"prix":124.3,"description":"desc13"},{"id":22,"version":1,"nom":"produit14","idCategorie":5,"prix":125.4,"description":"desc14"}]}
Vidage de la base de données --------------------------------
-- Liste des categories avant l'ajout
-- Liste des produits avant l'ajout
Ajout d'une catégorie [cat1] avec deux produits de même nom --------------------------------
Les erreurs suivantes se sont produites : 
- org.hibernate.exception.ConstraintViolationException: could not execute statement
- could not execute statement
- Duplicate entry 'x' for key 'NOM'
-- Liste des categories après l'ajout
-- Liste des produits après l'ajout
Travail terminé
  • خطوط ۴–۱۷: دسته‌بندی‌ها و محصولاتی که در جدول درج شده‌اند؛
  • خطوط ۱۸–۱۹: یک دسته‌بندی با محصولاتش؛
  • خطوط ۲۰–۲۱: یک محصول با دسته‌بندی آن؛
  • خطوط 22–26: قیمت برخی محصولات به‌روزرسانی شده است. در خط 24، می‌بینیم که قیمت‌ها واقعاً 10 درصد افزایش یافته‌اند؛
  • رده‌های ۲۷–۳۶: افزودن دسته‌بندی [cat1] با دو محصول با نام یکسان. می‌بینیم که جدول قبل از افزودن (رده‌های ۲۸–۲۹) و بعد از افزودن (رده‌های ۳۵–۳۶) یکسان است، که نشان می‌دهد تمام عملیات درج در تراکنش در واقع لغو شده است؛
  • خطوط ۳۱–۳۴: استثنایی که در حین درج محصول دوم رخ داده و باعث شکست کل تراکنش شده است؛

11.3.10. تست واحد JUnit

  

کلاس [Test01] به شرح زیر است:


package spring.data.tests;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.data.config.AppConfig;
import spring.data.dao.DaoException;
import spring.data.dao.IDao;
import spring.data.entities.Categorie;
import spring.data.entities.Produit;

@SpringApplicationConfiguration(classes = AppConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    //لایه [DAO]
    @Autowired
    private IDao dao;

    // فیلتر می‌کند jSON
    @Autowired
    @Qualifier("jsonMapper")
    private ObjectMapper jsonMapper;
    @Autowired
    @Qualifier("jsonMapperCategorieWithProduits")
    private ObjectMapper jsonMapperCategorieWithProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithCategorie")
    private ObjectMapper jsonMapperProduitWithCategorie;
    @Autowired
    @Qualifier("jsonMapperCategorieWithoutProduits")
    private ObjectMapper jsonMapperCategorieWithoutProduits;
    @Autowired
    @Qualifier("jsonMapperProduitWithoutCategorie")
    private ObjectMapper jsonMapperProduitWithoutCategorie;

    @Before
    public void cleanAndFill() {
        // پایگاه داده قبل از هر آزمون پاک می‌شود
        log("Vidage de la base de données", 1);
        //جدول [CATEGORIES] تخلیه می‌شود – در نتیجه، جدول [PRODUITS] نیز تخلیه خواهد شد
        dao.deleteAllCategories();
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        //جداول پر می‌شوند
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // کategori را اضافه کنید – در نتیجه، محصولات نیز درج خواهند شد
        categories = dao.addCategories(categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        //فهرست دسته‌بندی‌ها
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories();
        affiche(categories, jsonMapperCategorieWithoutProduits);
        // فهرست محصولات
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits();
        affiche(produits, jsonMapperProduitWithoutCategorie);
        // چند بررسی
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }

    @Test
    public void getCategorieByNameWithProduits() {
        log("getCategorieByNameWithProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals(5, categorie1.getProduits().size());
    }

    @Test
    public void getCategorieByNameWithoutProduits() {
        log("getCategorieByNameWithoutProduits", 1);
        Categorie categorie1 = dao.getCategorieByNameWithoutProduits("categorie1");
        Assert.assertNotNull(categorie1);
        Assert.assertEquals("categorie1", categorie1.getNom());
    }

    @Test
    public void getProduitByIdWithCategorie() {
        log("getProduitByNameWithCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
        Assert.assertEquals(produit.getCategorie().getId(), produit2.getCategorie().getId());
    }

    @Test
    public void getProduitByIdWithoutCategorie() {
        log("getProduitByIdWithoutCategorie", 1);
        Produit produit = dao.getProduitByNameWithCategorie("produit03");
        Produit produit2 = dao.getProduitByIdWithoutCategorie(produit.getId());
        Assert.assertNotNull(produit2);
        Assert.assertEquals(produit2.getNom(), produit.getNom());
        Assert.assertEquals(produit2.getId(), produit.getId());
    }
...
    // -------------- متدهای خصوصی
    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

    private Categorie findCategorieByName(String nom, List<Categorie> categories) {
        for (Categorie categorie : categories) {
            if (categorie.getNom().equals(nom)) {
                return categorie;
            }
        }
        return null;
    }

    //نمایش یک عنصر از نوع T
    static private <T> void affiche(T element, ObjectMapper jsonMapper) throws JsonProcessingException {
        System.out.println(jsonMapper.writeValueAsString(element));
    }

    // نمایش فهرستی از عناصر از نوع T
    static private <T> void affiche(List<T> elements, ObjectMapper jsonMapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, jsonMapper);
        }
    }

    private static 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);
    }

    private static void show(String title, List<String> messages) {
        // عنوان
        System.out.println(String.format("%s : ", title));
        // پیام‌ها
        for (String message : messages) {
            System.out.println(String.format("- %s", message));
        }
    }

}
  • خط ۲۷: تست واحد توسط کلاس [AppConfig] پیکربندی می‌شود که قبلاً در بخش ۱۱.۳.۸ توضیح داده شده است؛
  • خطوط ۳۲–۳۳: تزریق مرجعی به لایه [DAO
  • خطوط ۳۶–۵۰: تزریق پنج مپر jSON;
  • خطوط 60–71: پس از خالی کردن پایگاه داده (خط 57)، پایگاه داده با دو دسته، که هر کدام شامل پنج محصول هستند، پر می‌شود. این متد به دلیل انوتیشن [@Before] در خط 52، قبل از هر تست اجرا می‌شود؛
  • خطوط ۷۵–۹۳: محتویات پایگاه داده را نمایش می‌دهد؛
  • خطوط ۹۵–۱۰۱: یک دسته‌بندی و محصولات آن را بازیابی می‌کند، که دسته‌بندی با نامش شناسایی می‌شود؛
  • خطوط ۱۰۳–۱۰۹: یک دسته‌بندی را بدون محصولات آن بازیابی می‌کند، و دسته‌بندی با نامش شناسایی می‌شود؛
  • خطوط ۱۱۱–۱۲۰: یک محصول را به همراه دسته‌اش بازیابی می‌کند، محصول با شماره‌اش شناسایی می‌شود؛
  • خطوط ۱۲۲–۱۳۰: یک محصول را بدون دسته‌بندی آن بازیابی می‌کند، محصول با شماره خود شناسایی می‌شود؛
  • خطوط ۱۳۳–۱۸۴: متدهای خصوصی مشترک میان تست‌های مختلف؛

وظیفه: اجرای تست. باید با موفقیت انجام شود.


11.3.11. مدیریت لاگ‌ها

لاگ‌های کنسول یا تست JUnit توسط فایل زیر پیکربندی می‌شوند: [logback.xml]:

  

این فایل باید با نام [logback.xml] نام‌گذاری شود و در کلاس‌پث پروژه قرار گیرد. برای اطمینان از این موضوع، آن را در پوشه [src/main/resources] که بخشی از کلاس‌پث است، قرار داده‌ایم. محتویات آن به شرح زیر است:


<configuration> 

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 
    <!-- کدگذارها به‌طور پیش‌فرض از این نوع برخوردار می‌شوند
         ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <!--کنترل سطح لاگ -->
  <root level="info"> <!-- اطلاعات، اشکال‌زدایی، هشدار -->
    <appender-ref ref="STDOUT" />
  </root>
</configuration>
  • خط ۱۲: تگ [<root level="info">] لاگ‌ها را در سطح [info] نمایش می‌دهد. به جای [info]، می‌توانید مشخص کنید:
    • [debug]: این دقیق‌ترین سطح لاگ است. توصیه می‌شود در فاز عیب‌یابی پروژه از آن استفاده کنید، زیرا لاگ‌های بسیار مفیدی در مورد ارتباطات کلاینت/سرور ارائه می‌دهد. این روشی برای درک آنچه در «زیر کاپوت» در حال وقوع است، می‌باشد؛
    • [off]: هیچ لاگی ثبت نمی‌شود؛
    • [warn]: یک سطح لاگ‌گیری میانی است که در آن Spring ناهنجاری‌هایی را گزارش می‌کند که لزوماً خطا نیستند. اگر به نتیجه مورد انتظار دست نیافتید، باید این موارد را بررسی کنید؛

وظیفه: سطح را در خط ۱۲ به [debug] تغییر دهید، سپس تست واحد را اجرا کنید. به تفاوت در لاگ‌ها توجه کنید.


11.3.12. ایجاد آرشیو Maven پروژه

برای نصب آرشیو پروژه در مخزن محلی Maven، به شرح زیر عمل کنید [1-3]:

آرشیو با استفاده از شناسه‌های موجود در فایل [pom.xml] ایجاد خواهد شد:


    <groupId>istia.st.springdata</groupId>
    <artifactId>intro-spring-data-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

مکان مخزن محلی Maven را می‌توان در پیکربندی Eclipse یافت:

 

سپس می‌توانید بررسی کنید که آرتفکت Maven به درستی نصب شده است:

 

از این پس، یک پروژه محلی دیگر در Maven قادر خواهد بود از این آرشیو استفاده کند.