Skip to content

4. Spring'e Giriş JDBC

Bu bölümde, aşağıdaki mimariyi inceleyeceğiz:

Yani bu, öncekiyle aynı mimari. İki değişiklik yapacağız:

  • veritabanında yabancı anahtar ilişkisi ile birbirine bağlı iki tablo olacak;
  • [DAO] katmanı, API ve JDBC yönetiminde kolaylık sağlayan [Spring JDBC] kütüphanesi ile uygulanacaktır;

4.1. Çalışma ortamının kurulması

STS ile, [<exemples>/spring-database-generic/spring-jdbc] klasöründe bulunan [spring-jdbc-04] projesini içe aktarın

Ayrıca, [MyManager] istemcisi ile yeni bir MySQL veritabanı oluşturmamız gerekiyor (bkz. paragraf 3.1):

  • [3]'te, aşağıdaki örnekler MySQL adlı ve [dbproduitscategories] olarak adlandırılan bir veritabanı üzerinde çalışmaktadır;
  • [9]'e root kullanıcısının şifresini girin (bu belgede bu şifre root'tir);
  • [18]'te, [dbproduitscategories] veritabanı boş olarak oluşturulmuştur. Tablolar oluşturulur ve SQL [19-20] komut dosyası ile doldurulur;
  • komutuyla [21]'te [<exemples>/spring-database-config/mysql/databases] klasörüne gidin;
  • [25]'e geldiğinizde, [dbproduits] veritabanında değil, [dbproduitscategories] veritabanında olduğunuzdan emin olun;
  • [29]'e geçerken, SQL komut dosyası beş tablo oluşturmuştur. [ROLES, USERS, USERS_ROLES] tabloları, [dbproduitscategories] veritabanını web üzerinde sunmak için oluşturulan web hizmetinin güvenliğini sağlama aşamasına gelindiğinde kullanılacaktır;

4.2. [dbproduitscategories] veritabanı

[dbproduitscategories] veritabanı, daha önce incelenen [dbproduits] veritabanının bir uzantısıdır. [PRODUITS] tablosunda ürünün, özel bir anlamı olmayan bir numara ile tanımlanan bir kategorisi varken, burada bu numara [CATEGORIES] tablosunda bir yabancı anahtar olacaktır.

[PRODUITS] tablosu şu şekildedir:

  • [ID]: [2] tablosunun otomatik artan birincil anahtarı;
  • [NOM]: [4] ürününün benzersiz adı;
  • [PRIX]: ürünün fiyatı;
  • [DESCRIPTION]: ürün açıklaması;
  • [VERSIONING], ürünün sürüm numarasıdır. İlk sürümü 1 [3]'tir. Ürün her değiştirildiğinde, sürüm numarası tabloyu işleyen kod tarafından bir artırılır;
  • [CATEGORIE_ID]: Ürünün ait olduğu kategoriyi belirtmek için [CATEGORIES] tablosundaki yabancı anahtar;
  • [1-3]'te, [PRODUITS] tablosunun [CATEGORIE_ID] yabancı anahtarı. Bu, [CATEGORIES] ve [4-5] tablolarının [ID] sütununu hedefler;
  • bir kategori silindiğinde, ona bağlı tüm ürünler de silinir ([6]). Bu nokta, [dbproduitscategories] veritabanını kullanan [DAO] katmanının oluşturulmasında kullanıldığı için dikkat edilmesi gereken önemli bir husustur;

Kategoriler tablosu [CATEGORIES] şu şekildedir:

  • [ID]: otomatik artan birincil anahtar;
  • [VERSIONING]: kategorinin sürüm numarası;
  • [NOM]: kategorinin benzersiz adı;

4.3. Eclipse Projesi

  

[spring-jdbc-04] projesi aşağıdaki mimariyi uygular:

[spring-jdbc-04] projesi, aşağıdaki [pom.xml] dosyası ile yapılandırılmış bir Maven projesidir:

  

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

    <groupId>dvp.spring.database</groupId>
    <artifactId>spring-jdbc-generic-04</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>spring-jdbc-generic-04</name>
    <description>Demo project for Spring JdbcTemplate</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
        <relativePath /> <!-- depo'dan üst öğeyi arama -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <!-- SGBD'in JDBC yapılandırması -->
        <dependency>
            <groupId>dvp.spring.database</groupId>
            <artifactId>generic-config-jdbc</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring JdbcTemplate -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • 28-32. satırlar: proje, [mysql-config-jdbc] projesine dayanmaktadır; bu proje, JDBC katmanını yapılandırmaktadır;
  • 34-37. satırlar: [spring-boot-starter-jdbc] artefaktı, Spring kütüphanelerini JDBC'e getirir;

Sonuç olarak, bağımlılıklar şu şekildedir:

  

4.4. Spring Yapılandırması

  

Spring projesini yapılandıran [AppConfig] sınıfı şöyledir:


package spring.jdbc.config;

import generic.jdbc.config.ConfigJdbc;

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.context.annotation.Import;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@Configuration
@ComponentScan(basePackages = { "spring.jdbc.dao" })
@EnableTransactionManagement
@Import({ generic.jdbc.config.ConfigJdbc.class })
public class AppConfig {

    // veri kaynağı
    @Bean
    public DataSource dataSource() {
        // veri kaynağı TomcatJdbc
        DataSource dataSource = new DataSource();
        // erişim yapılandırması JDBC
        dataSource.setDriverClassName(ConfigJdbc.DRIVER_CLASSNAME);
        dataSource.setUsername(ConfigJdbc.USER_DBPRODUITSCATEGORIES);
        dataSource.setPassword(ConfigJdbc.PASSWD_DBPRODUITSCATEGORIES);
        dataSource.setUrl(ConfigJdbc.URL_DBPRODUITSCATEGORIES);
        // başlangıçta açık olan bağlantılar
        dataSource.setInitialSize(5);
        // sonuç
        return dataSource;
    }

    // İşlem yöneticisi
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    // JdbcTemplate
    @Bean
    public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) {
        return new NamedParameterJdbcTemplate(dataSource);
    }

    // ürün ekleme
    @Bean
    public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_PRODUITS).usingGeneratedKeyColumns(
                ConfigJdbc.TAB_PRODUITS_ID);
    }

    // kategori ekleme
    @Bean
    public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES).usingGeneratedKeyColumns(
                ConfigJdbc.TAB_CATEGORIES_ID);
    }

}
  • 16. satır: sınıf, bir Spring yapılandırma sınıfıdır;
  • 17. satır: [spring.jdbc.dao] paketi, [AppConfig] sınıfında bulunanların dışında başka Spring bileşenleri olup olmadığını aramak üzere taranacaktır. Burada, [DAO] katmanını uygulayan bileşen bulunacaktır;
  • 18. satır: İşlemleri kendimiz yönetmeyeceğiz, bunun sorumluluğunu Spring JDBC'e bırakacağız. Yapmamız gereken tek şey, bir işlem içinde yürütülmesi gereken yöntemleri Spring [@Transactional] anotasyonuyla işaretlemektir. 18. satır, bu anotasyonun yönetilmesini ve göz ardı edilmemesini sağlar. İşlem yönetimi, [pom.xml] dosyası tarafından içe aktarılan Spring JDBC projesinin bağımlılıklarından biri tarafından sağlanır;
  • 19. satır: [mysql-config-jdbc] projesinin [generic.jdbc.config.ConfigJdbc] sınıfında önceden tanımlanmış olan bean'ler içe aktarılır;
  • 23-36. satırlar: [spring-jdbc-02] örneğine eklenen [tomcat-jdbc] veri kaynağı;
  • satır 40-42: Daha önce tanımlanan veri kaynağına bağlı işlem yöneticisi. Bean'in adı mutlaka [transactionManager] olmalıdır, çünkü [@EnableTransactionManagement] anotasyonu bu adı kullanır. [DataSourceTransactionManager] işleyicisi, Spring kütüphanesi JDBC (satır 12) tarafından sağlanır;
  • 45-48. satırlar: [DAO] katmanının uygulamasının dayandırılacağı [namedParameterJdbcTemplate] bean'i. Bu bean, Spring JDBC kütüphanesi tarafından sağlanır (satır 10). Bu bean da daha önce tanımlanan veri kaynağına bağlıdır (satır 47);
  • 51-55. satırlar: [simpleJdbcInsertProduit] bean'i (ad serbesttir), [PRODUITS] tablosuna bir ürün eklemek ve oluşturulan birincil anahtarı almak için kullanılacaktır. Kullanılan çeşitli parametreler şunlardır:
    • [dataSource]: 24-36. satırlardaki [tomcat-jdbc] veri kaynağı;
    • [ConfigJdbc.TAB_PRODUITS]: [PRODUITS] tablosu;
    • [ConfigJdbc.TAB_CATEGORIES_ID]: [PRODUITS] tablosunun birincil anahtar sütunu. PostgreSQL için bu sütunun adının küçük harflerle yazılması gerektiği hatırlatılmalıdır;
  • 58-62. satırlar: [simpleJdbcInsertCategorie] bean'i, [CATEGORIES] tablosuna bir kategori eklemek ve oluşturulan birincil anahtarı almak için kullanılacaktır;

4.5. Projedeki istisnalar

  

[spring-jdbc-03] projesinde [UncheckedException, DaoException, ShortException] sınıflarını daha önce görmüştük. Buna yeni bir tane daha ekliyoruz:


package spring.jdbc.infrastructure;

public class MyIllegalArgumentException extends UncheckedException {

    private static final long serialVersionUID = 1L;

    // Üreticiler
    public MyIllegalArgumentException() {
        super();
    }

    public MyIllegalArgumentException(int code, Throwable e, String className) {
        super(code, e, className);
    }

}
  • [MyIllegalArgumentException] sınıfı, [UncheckedException] sınıfından türetilmiştir ve bu nedenle denetlenmeyen bir sınıftır. Bu sınıf, [DAO] katmanındaki bir yöntemin yanlış argümanlarla çağrıldığını bildirmek için kullanılacaktır. Bu sınıfa [IllegalArgumentException] adı verilmemiştir, çünkü bu istisna JDK sınıfında zaten mevcuttur ve bu durum bazen derleyicinin hatalı bir [import] sınıfı oluşturmasına neden oluyordu;

4.6. Proje varlıkları

  

[spring.jdbc.entities] paketindeki sınıflar, [dbproduitscategories] veritabanındaki tabloların satırlarının görüntüleri. Şimdilik [USERS, ROLES, USERS_ROLE] tablolarının görüntülerini göz ardı edeceğiz.

Tüm varlıklar, [AbstractCoreEntity] ana sınıfını genişletir:


package spring.jdbc.entities;

public abstract class AbstractCoreEntity {
    // özellikler
    protected Long id;
    protected Long version;

    // üreticiler
    public AbstractCoreEntity() {

    }

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

    public AbstractCoreEntity(AbstractCoreEntity entity) {
        this.id = entity.id;
        this.version = entity.version;
    }

    public void setAbstractCoreEntity(AbstractCoreEntity entity) {
        this.id = entity.id;
        this.version = entity.version;
    }

    // ------------------------------------------------------------
    // [equals] ve [hashcode]'in yeniden tanımlanması
    @Override
    public int hashCode() {
        return (id != null ? id.hashCode() : 0);
    }

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

    // alıcı ve ayarlayıcılar
...
}
  • 5. satır: [id] alanı, tabloların birincil anahtarı olan [ID] sütunuyla ilişkilendirilecektir;
  • 6. satır: [version] alanı, tabloların [VERSIONING] sütunuyla ilişkilendirilecektir;
  • satır 8-26: bir [AbstractCoreEntity] nesnesini oluşturmak veya başlatmak için çeşitli oluşturucular ve yöntemler;
  • satır 35-47: [equals] yöntemi, iki [AbstractCoreEntity] nesnesinin, aynı [id] alanına sahip olmaları durumunda eşit olduğunu belirtir. Burada, [AbstractCoreEntity] nesnelerinin, [id]'in birincil anahtar olduğu ve dolayısıyla aynı [id] değerine sahip iki satırın bulunamayacağı tablo satırlarının görüntüleri olacağı unutulmamalıdır;
  • 30-33. satırlar: [hashCode]'e ait bir teklif;

[Produit] sınıfı, [PRODUITS] tablosundaki bir satırın görüntüsü olacaktır:


package spring.jdbc.entities;

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {
    // özellikler
    private String nom;
    private Long idCategorie;
    private double prix;
    private String description;
    private Categorie categorie;

    // yapıcılar
    public Produit() {

    }

    public Produit(Long id, Long version, String nom, Long idCategorie, double prix, String description,
            Categorie categorie) {
        super(id, version);
        this.nom = nom;
        this.idCategorie = idCategorie;
        this.prix = prix;
        this.description = description;
        this.categorie = categorie;
    }

    // imza
    public String toString() {
        return String.format("[id=%s, version=%s, nom=%s, prix=10.2f, desc=%s, idCategorie=%s]", id, version, nom, prix,
                description, idCategorie);
    }

    // getter ve setter'lar
...
}
  • 6. satır: [Produit] sınıfı, [AbstractCoreEntity] sınıfını genişletir;
  • 8-12. satırlar: [id, version, nom, idCategorie, prix, description] alanları, [PRODUITS] tablosundaki [ID, VERSIONING, NOM, CATEGORIE_ID, PRIX, DESCRIPTION] sütunlarının görüntüleri;
  • 12. satır: [Categorie] türündeki nesne, birincil anahtarı [idCategorie]'tir. Bu alan duruma göre doldurulabilir veya doldurulmayabilir. Doldurulduğunda, uzun versiyonlu ürün [LongProduit] olarak adlandırılır; aksi takdirde kısa versiyonlu ürün [ShortProduit] olarak adlandırılır;
  • 5. satır: bir filtre jSON. [mysql-config-jdbc] projesinin bir jSON kütüphanesini içerdiği hatırlatılmalıdır. Filtreye ihtiyaç duyulmasının nedeni, [categorie] alanının doldurulup doldurulmayabilmesidir. Bu durumda, ürünün jSON gösterimi farklılık gösterir. Bu iki durumu yönetmek için 5. satırdaki [jsonFilterProduit] filtresini yapılandıracağız. jSON filtresi, jSON gösteriminden hariç tutulacak alanları dinamik olarak belirlemeye olanak tanır. [categorie] alanının doldurulmadığını tespit ettiğimizde, bu alanı ürünün jSON temsilinden hariç tutacağız;

[Categorie] sınıfı, [CATEGORIES] tablosundaki bir satırın görüntüsüdür:


package spring.jdbc.entities;

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

import com.fasterxml.jackson.annotation.JsonFilter;

@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {

    // özellikler
    private String nom;
    public List<Produit> produits;

    // yapıcılar
    public Categorie() {

    }

    public Categorie(Long id, Long version, String nom, List<Produit> produits) {
        super(id, version);
        this.nom = nom;
        this.produits = produits;
    }

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

    // yöntemler
    public void addProduit(Produit produit) {
        // ürün ekleme
        if (produits == null) {
            produits = new ArrayList<Produit>();
        }
        if (produit != null) {
            // ürün ekleniyor
            produits.add(produit);
            // kategorisi belirleniyor
            produit.setCategorie(this);
            produit.setIdCategorie(this.id);
        }
    }

    // alıcı ve ayarlayıcılar
...
}
  • 9. satır: [Categorie] sınıfı, [AbstractCoreEntity] sınıfını genişletir;
  • 12. satır: [id, version, nom] alanları, [CATEGORIES] tablosundaki [ID, VERSIONING, NOM] sütunlarının görüntüleridir;
  • 13. satır: [produits] alanı, kategorideki ürünlerin listesini temsil eder. Bu alan her zaman doldurulmaz. Doldurulmadığında, kısa versiyon kategori [ShortCategorie], aksi takdirde uzun versiyon kategori [LongCategorie] olarak adlandırılır;
  • satır 32-44: [addProduit] yöntemi, kategoriye bir ürün eklemeye (satır 39) ve eklenen üründe, kategorisinin özelliklerini (idCategorie ve kategori) belirlemeye olanak tanır;
  • satır 8: bir jSON filtresi. jSON kütüphanesi bir [Categorie] nesnesini serileştirip/serileştirmeyi kaldırması gerektiğinde, [jsonFilterCategorie] adlı filtreyi nasıl işleyeceği belirtilmelidir;

4.7. Idao<T> arayüzü

  

[DAO] katmanına ait [IDao] arayüzünün imzası şöyledir:


package spring.jdbc.dao;

import java.util.List;

import spring.jdbc.entities.AbstractCoreEntity;

public interface IDao<T extends AbstractCoreEntity> {

    // tüm T varlıklarının listesi
    public List<T> getAllShortEntities();

    public List<T> getAllLongEntities();

    // belirli varlıkların listesi - kısa versiyon
    public List<T> getShortEntitiesById(Iterable<Long> ids);

    public List<T> getShortEntitiesById(Long... ids);

    public List<T> getShortEntitiesByName(Iterable<String> names);

    public List<T> getShortEntitiesByName(String... names);

    // belirli varlıklar - uzun sürüm
    public List<T> getLongEntitiesById(Iterable<Long> ids);

    public List<T> getLongEntitiesById(Long... ids);

    public List<T> getLongEntitiesByName(Iterable<String> names);

    public List<T> getLongEntitiesByName(String... names);

    // birden fazla varlığın güncellenmesi
    public List<T> saveEntities(Iterable<T> entities);

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

    // tüm varlıkların silinmesi
    public void deleteAllEntities();

    // birden fazla öğenin silinmesi
    public void deleteEntitiesById(Iterable<Long> ids);

    public void deleteEntitiesById(Long... ids);

    public void deleteEntitiesByName(Iterable<String> names);

    public void deleteEntitiesByName(String... names);

    public void deleteEntitiesByEntity(Iterable<T> entities);

    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities);
}
  • 7. satır: Burada, bir T türüyle parametreleştirilmiş ve bir koşula sahip bir [IDao] arayüzü vardır: bu tür, [AbstractCoreEntity] sınıfını genişletmeli veya [AbstractCoreEntity] arayüzünü uygulamalıdır. [extends] anahtar sözcüğü her iki durum için de kullanılır. Burada T, ya [Produit] türüyle ya da [Categorie] türüyle örneklenecektir. Nitekim, [Produit] ve [Categorie] türleri üzerinde aynı tür işlemlerin (ekleme, değiştirme, silme, seçme) yapıldığı kısa sürede fark edilir. Dolayısıyla, bu yöntemleri genel bir arayüzde bir araya getirmek mantıklı görünür;
  • duruma göre, [LongEntity] ve [ShortEntity] terimleri farklı durumları ifade eder:
    • T, [Produit] türü olduğunda:
      • [ShortEntity], [Categorie categorie] alanı doldurulmamış haldeki üründür;
      • [LongEntity], [Categorie categorie] alanı doldurulmuş üründür;
    • T, [Categorie] türü olduğunda:
      • [ShortEntity], [List<Produit> produits] alanı doldurulmamış kategoridir;
      • [LongEntity], [List<Produit> produits] alanı doldurulmuş üründür;

Dolayısıyla, 19 yöntemden oluşan zengin bir arayüzümüz var. Yöntemlerin çoğu iki kez mevcut. [getShortEntitiesById] yöntemini örnek olarak alalım:


    public List<T> getShortEntitiesById(Iterable<Long> ids);

    public List<T> getShortEntitiesById(Long... ids);
  • 1. ve 3. satırlar: Parametre, kısa sürümünü almak istediğimiz varlıkların birincil anahtarlarının listesidir. Bu liste iki farklı biçimde sunulur:
    • 1. satır: [Iterable<Long>] arayüzünü uygulayan bir liste. [List<Long>] türü bu arayüzü uygular, ancak bunun dışında pek çok başka tür de mevcuttur. Eğer [List<Long> ids] yazılsaydı, bu örneklerimiz için yeterli olurdu; ancak bu durumda, örneğimizdeki kullanıcı, parametresi tam olarak beklenen türde değilse dönüştürme yapmak zorunda kalırdı;
    • 3. satır: Ne yazık ki Long[] türü, [Iterable<Long>] arayüzünü uygulamamaktadır. Bu durumda, 3. satırdaki versiyonu kullanacağız. [Long... ids] (3 nokta) biçimindeki formal parametre, hem bir diziden hem de bir dizi kimlikten değer alabilir: getShortEntitiesById(id1, id2, ...);

Aynı IDao<T> arayüzü, aşağıdaki mimari ile uygulanacaktır:

burada bir [JPA] katmanı (Java Persistence API), [DAO] katmanı ile SGBD'in JDBC sürücüsü arasına yerleştirilecektir. Bu sayede, her iki mimari için ortak bir test katmanına sahip olacağız. Her iki durumda da, [DAO] katmanı iki arayüz sunacaktır:

  • IDao<Ürün>, [PRODUITS] tablosuna erişmek için;
  • IDao<Kategori>, [CATEGORIES] tablosuna erişim için;

4.8. IDao<T> arayüzünün uygulanması

  
  • IDao<Ürün> arayüzü, [DaoProduit] sınıfı tarafından uygulanır;
  • IDao<Kategori> arayüzü, [DaoCategorie] sınıfı tarafından uygulanmaktadır;

[DaoProduit] ve [DaoCategorie] sınıflarının her ikisi de aşağıdaki [AbstractDao] soyut sınıfını genişletir:


package spring.jdbc.dao;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.transaction.annotation.Transactional;

import spring.jdbc.entities.AbstractCoreEntity;
import spring.jdbc.infrastructure.MyIllegalArgumentException;

import com.google.common.collect.Lists;

public abstract class AbstractDao<T extends AbstractCoreEntity> implements IDao<T> {

    // eklemeler
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

    // yerel
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argümanın geçerliliği
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // parça parça alma
        entities = new ArrayList<T>();
        int taille = maxPreparedStatementParameters;
        List<Long> listIds = Lists.newArrayList(ids);
        int nbIds = listIds.size();
        for (int i = 0; i < nbIds; i += taille) {
            int limit = Math.min(nbIds, i + taille);
            entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
        }
        // sonuç
        return entities;
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // argümanın geçerliliği
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
        // sonuç
        return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesByName(Iterable<String> names) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesByName(String... names) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getLongEntitiesById(Iterable<Long> ids) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getLongEntitiesById(Long... ids) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getLongEntitiesByName(Iterable<String> names) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public List<T> getLongEntitiesByName(String... names) {
    ...
    }

    @Override
    @Transactional
    public List<T> saveEntities(Iterable<T> entities) {
    ...
    }

    @Override
    @Transactional
    public List<T> saveEntities(@SuppressWarnings("unchecked") T... entities) {
    ...
    }

    @Override
    public void deleteEntitiesById(Iterable<Long> ids) {
    ...
    }

    @Override
    public void deleteEntitiesById(Long... ids) {
    ...
    }

    @Override
    public void deleteEntitiesByName(Iterable<String> names) {
    ...
    }

    @Override
    public void deleteEntitiesByName(String... names) {
    ...
    }

    @Override
    public void deleteEntitiesByEntity(Iterable<T> entities) {
    ...
    }

    @Override
    public void deleteEntitiesByEntity(@SuppressWarnings("unchecked") T... entities) {
    ...
    }

    protected void deleteEntitiesByEntity(List<T> entities) {
    ...
    }

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllShortEntities();

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllLongEntities();

    @Override
    public abstract void deleteAllEntities();

    // özel yöntemler ----------------------------------------------
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
...
    }

    @SuppressWarnings("unchecked")
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
    ...
    }

    // korumalı yöntemler ----------------------------------------------
    abstract protected List<T> getShortEntitiesById(List<Long> ids);

    abstract protected List<T> getShortEntitiesByName(List<String> names);

    abstract protected List<T> getLongEntitiesById(List<Long> ids);

    abstract protected List<T> getLongEntitiesByName(List<String> names);

    abstract protected List<T> saveEntities(List<T> entities);

    abstract protected void deleteEntitiesById(List<Long> ids);

    abstract protected void deleteEntitiesByName(List<String> names);

}
  • 15. satır: [AbstractDao] sınıfı soyuttur (abstract anahtar sözcüğü). Bu nedenle, bu sınıfın bir örneği oluşturulamaz. Yalnızca bu sınıftan türetilebilir. Bu sınıfın birkaç işlevi vardır:
    • her yöntemin gerçekleştirildiği işlemin niteliğini belirlemek;
    • [IDao<Produit>] ve [IDao<Categorie>] arayüzlerinin her iki uygulamasında da ortak olan işlemleri mümkün olduğunca birleştirmek. Bu, esas olarak argümanların geçerliliğini kontrol etmektir. null argümanı veya boş listeler kabul edilmeyecektir;
    • T... params ve Iterable<T> params parametre türlerini tek bir tür olan List<T> params olarak birleştirmek;
    • iş, iki arayüzden birine özgü hale gelir gelmez işin alt sınıflara devredilmesi;

[AbstractDao] sınıfı tarafından gerçekleştirilen farklı yöntemlerin parametrelerinin tek tip hale getirilmesi sayesinde, alt sınıflar [DaoProduit] ve [DaoCategorie], 19 yerine sadece 10 yöntemi uygulamak zorunda kalacak:


    // alt sınıflar tarafından uygulanan yöntemler ----------------------------------------------
    abstract protected List<T> getShortEntitiesById(List<Long> ids);

    abstract protected List<T> getShortEntitiesByName(List<String> names);

    abstract protected List<T> getLongEntitiesById(List<Long> ids);

    abstract protected List<T> getLongEntitiesByName(List<String> names);

    abstract protected List<T> saveEntities(List<T> entities);

    abstract protected void deleteEntitiesById(List<Long> ids);

    abstract protected void deleteEntitiesByName(List<String> names);

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllShortEntities();

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllLongEntities();

    @Override
public abstract void deleteAllEntities();

[AbstractDao] sınıfındaki bazı yöntemlere bir göz atalım.

[getShortEntitiesById] yöntemi

Bu yöntem, birincil anahtarları verilen varlıkların kısa sürümünü elde etmeyi amaçlar.


    // enjeksiyonlar
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

    // yerel
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
    ...
}
  • 2-4. satırlar: Belirli bir SGBD nesnesinin JDBC katmanını yapılandıran [ConfigJdbc] yapılandırma dosyasında tanımlanan [maxPreparedStatementParameters] bean'i enjekte edilir:

    // bir [PreparedStatement]'in maksimum parametre sayısı
    public final static int MAX_PREPAREDSTATEMENT_PARAMETERS = 10000;

    @Bean(name = "maxPreparedStatementParameters")
    public int maxPreparedStatementParameters() {
        return MAX_PREPAREDSTATEMENT_PARAMETERS;
}
  • 1-7. satırlar: [maxPreparedStatementParameters] bean'ini tanımlar; bu bean, bir [PreparedStatement] türüne verilebilecek parametre sayısının üst sınırını belirleyecektir. Bu ihtiyaç, [PreparedStatement] türü için 10.000 parametre kabul eden SGBD ve MySQL'te ortaya çıkmamıştı. SGBD ve SQL Sunucusu ile yapılan testler sırasında, bu sunucu, bir [PreparedStatement] türü için maksimum parametre sayısının 2100 olduğunu belirten bir istisna oluşturdu. Bu nedenle bu sayı, çeşitli SGBD sunucularının yapılandırma parametrelerinden biri haline gelmiştir. Dolayısıyla, her bir SGBD sunucusunun [sgbd-config-jdbc] yapılandırma projesine eklenmelidir;

Şimdi [getShortEntitiesById] yönteminin koduna geri dönelim:


    // enjeksiyonlar
    @Autowired
    @Qualifier("maxPreparedStatementParameters")
    protected int maxPreparedStatementParameters;

    // yerel
    protected String simpleClassName = getClass().getSimpleName();

    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
    ...
}
  • 7. satır: sınıf adı. [DaoException] istisna sınıfının oluşturucularından birinin parametresi olarak kullanılır;
  • 10. satır: [@Transactional(readOnly = true)] açıklaması, yöntemin salt okunur bir işlem içinde gerçekleştirilmesi gerektiğini belirtir. Yöntem sadece okuma işlemleri gerçekleştirdiği ve dolayısıyla bir hata durumunda iptal edilecek bir işlem olmadığı için, böyle bir işlemin yararı sorgulanabilir. [Spring Data] kütüphanesinin yazarı bunu tavsiye ediyor ve nedenini açıklıyor. Ben de onun tavsiyesine uydum;

Yöntemin gövdesi şu şekildedir:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argümanın geçerliliği
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        if (entities != null) {
            return entities;
        }
...
}
  • 5. satır: [ids] parametresinin geçerliliği aşağıdaki yöntemle kontrol edilir:

    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, Iterable<T2> elements) {
        // boş öğeler mi?
        if (elements == null) {
            throw new MyIllegalArgumentException(222, new NullPointerException("L'argument ne peut être null"), simpleClassName);
        }
        // boş öğeler mi?
        if (!elements.iterator().hasNext()) {
            if (checkEmpty) {
                throw new MyIllegalArgumentException(223, new RuntimeException("l'argument ne peut être une liste vide"),
                        simpleClassName);
            } else {
                return new ArrayList<T>();
            }
        }
        // varsayılan sonuç
        return null;
}
  • 1. satır: [checkNullOrEmptyArgument] yöntemi, <T2> türüyle parametreleştirilen genel bir yöntemdir. T2, yöntemin ikinci parametresine aktarılan öğelerin türüdür. Bu, [Long, String, AbstractCoreEntity] olabilir;
  • 1. satır: [checkNullOrEmptyArgument] yöntemi iki parametre kabul eder:
    • [Iterable<T2> elements]: test edilecek parametre;
    • [checkEmpty]: önceki parametrenin boş olmayan bir liste olup olmadığını test etmek gerekiyorsa true olarak ayarlanır;
  • 4-6. satırlar: [elements] parametresinin null olmadığı kontrol edilir. Aksi takdirde, [MyIllegalArgumentException] türünde bir istisna atılır;
  • satır 8-15: Liste boşsa ve boş olmadığı kontrol edilmesi gerekiyorsa, [MyIllegalArgumentException] türünde bir istisna atılır;
  • 13. satır: Liste boşsa ve boş olmadığı kontrol edilmemesi gerekiyorsa, T türünde öğelerden oluşan boş bir liste döndürülür. [Iterable<T2>] arayüzü, bu arayüzü uygulayan listenin öğeleri üzerinde yineleme yapmayı sağlayan [iterator()] adlı bir yönteme sahiptir. Bu yineleyicinin iki yöntemi kullanışlıdır:
    • [itérateur].hasNext(): Listede işlenecek başka bir eleman varsa true, yoksa false değerini döndürür;
    • [iterateur].next(): Listenin geçerli öğesini döndürür ve bir öğe ilerler;
  • sonuç olarak,
    • [T2... elements] argümanı null ise veya boşsa, [MyIllegalArgumentException] türünde bir istisna atılır;
    • [T2... elements] argümanı boş bir liste ise ve bu geçerliyse, T türünde öğelerden oluşan boş bir liste döndürülür;

Test edilecek argüman [T2... elements] türündeyse benzer bir yöntem mevcuttur:


@SuppressWarnings("unchecked")
    private <T2> List<T> checkNullOrEmptyArgument(boolean checkEmpty, T2... elements) {
    ...
    }

[getShortEntitiesById] yönteminin koduna geri dönelim:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Iterable<Long> ids) {
        // argümanın geçerliliği
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // kısımlar halinde alma
        entities = new ArrayList<T>();
        int taille = maxPreparedStatementParameters;
        List<Long> listIds = Lists.newArrayList(ids);
        int nbIds = listIds.size();
        for (int i = 0; i < nbIds; i += taille) {
            int limit = Math.min(nbIds, i + taille);
            entities.addAll(getShortEntitiesById(listIds.subList(i, limit)));
        }
        // sonuç
        return entities;
}
  • 7. satır: buraya gelindiğine göre, [Iterable<Long> ids] argümanı geçerlidir;
  • 7-14. satırlar: Daha sonra göreceğiz ki, [getShortEntitiesById] yöntemi, aranacak birincil anahtarların listesini parametre olarak alan [PreparedStatement] türü tarafından uygulanacaktır. Örneğin:

public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";

: ids, etkin değeri List<Long> türü olan bir parametredir. Bu listenin her bir elemanı, [PreparedStatement] türündeki bir parametreye konu olacaktır. Ancak bu türün, sınıfın [maxPreparedStatementParameters] alanı tarafından belirlenen maksimum sayıda parametre kabul ettiğini belirtmiştik;

  • 7. satır: [getShortEntitiesById] yöntemi tarafından döndürülecek T varlıklarının listesi. Bu liste, [maxPreparedStatementParameters] elemanlarından oluşan parçalar halinde oluşturulacaktır;
  • 9. satır: [Iterable<Long> ids] argümanından yola çıkılarak bir [List<Long> listIds] türü oluşturulur. [Lists] sınıfı, nesne koleksiyonlarını işlemek için çok sayıda statik yöntem sunan Google Guava kütüphanesine ait bir sınıftır. Google Guava kütüphanesi, [mysql-config-jdbc] adlı Maven projesi tarafından (pom.xml) içe aktarılmıştır:

        <!-- Google Guava -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>16.0.1</version>
</dependency>
  • 10. satır: veritabanında aranacak T nesnesinin sayısı;
  • 11-13. satırlar: bunlar [taille = maxPreparedStatementParameters] öğelik gruplar halinde aranır;
  • 12. satır: [listIds] listesinin sonunu aşmamak için yapılan bir hesaplama;
  • satır 13: T varlıklarına [getShortEntitiesById(listIds.subList(i, limit))] çağrısı ile ulaşılır. Bu yöntem sınıfta şu şekilde tanımlanmıştır:

abstract protected List<T> getShortEntitiesById(List<Long> ids);

Dolayısıyla, veritabanında T varlıklarını arayacak olan alt sınıftır:

  • T, [Produit] türündeyse [DaoProduit];
  • T, [Categorie] türündeyse: [DaoCategorie];

Ana sınıfın bu işleminin iki avantajı vardır:

  • alt sınıftaki [getShortEntitiesById] yönteminin imzası benzersizdir: argümanı [List<Long> ids] türündedir;
  • alt sınıf, bir [PreparedStatement]'in [maxPreparedStatementParameters] parametreleri sorunuyla uğraşmak zorunda değildir. Ana sınıfı bunu onun yerine halletmiştir;
  • 13. satır: alt sınıf tarafından getirilen varlıklar, üst sınıf tarafından döndürülecek varlık listesinde birleştirilir (16. satır);

Şimdi, sınıfın diğer [getShortEntitiesById] yönteminin uygulamasını inceleyelim:


    @Override
    @Transactional(readOnly = true)
    public List<T> getShortEntitiesById(Long... ids) {
        // argümanın geçerliliği
        List<T> entities = checkNullOrEmptyArgument(true, ids);
        // sonuç
        return getShortEntitiesById((Iterable<Long>) Lists.newArrayList(ids));
}
  • 3. satır: Argümanın türü değişmiştir: Long... ids;
  • 5. satır: Bu argümanın geçerliliği test edilir;
  • 7. satır: az önce açıkladığımız [getShortEntitiesById] yöntemini çağırıyoruz. Burada da yine [Google Guava] kütüphanesindeki [Lists] sınıfından yararlanıyoruz. Derleyicinin doğru yöntemi seçmesine yardımcı olmak için [Iterable<Long>] türüne açık bir tür dönüştürme yapmak zorunda olduğumuzu unutmayın; çünkü [getShortEntitiesById] yöntemi sınıfta üç farklı imza ile tanımlanmıştır:
    • List<T> getShortEntitiesById(Long... ids) ;
    • List<T> getShortEntitiesById(Iterable<Long> ids);
    • List<T> getShortEntitiesById(List<Long> ids) (bu yöntem soyuttur ve alt sınıf tarafından uygulanır);

[AbstractDao] soyut sınıfı, [DaoProduit] ve [DaoCategorie] sınıflarının üst sınıfı olduğu için bu sınıf hakkında daha fazla yorum yapmayacağız. Sadece şunu unutmayalım: Bazen birden fazla sınıfta ortak olan davranışları, soylu sınıfta (soyut olsun ya da olmasın) bir araya getirmek faydalı olabilir. Bu çalışmanın ardından, alt sınıfların yalnızca aşağıdaki yöntemleri uygulaması kalır:


    // alt sınıflar tarafından uygulanan yöntemler ----------------------------------------------
    abstract protected List<T> getShortEntitiesById(List<Long> ids);

    abstract protected List<T> getShortEntitiesByName(List<String> names);

    abstract protected List<T> getLongEntitiesById(List<Long> ids);

    abstract protected List<T> getLongEntitiesByName(List<String> names);

    abstract protected List<T> saveEntities(List<T> entities);

    abstract protected void deleteEntitiesById(List<Long> ids);

    abstract protected void deleteEntitiesByName(List<String> names);

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllShortEntities();

    @Override
    @Transactional(readOnly = true)
    public abstract List<T> getAllLongEntities();

    @Override
public abstract void deleteAllEntities();

4.8. paragrafındaki kod, her yöntem için kullanılan farklı işlem türlerini göstermektedir. Birkaç noktaya dikkat çekelim:

  • veritabanını okuyan yöntemler [@Transactional(readOnly = true)] ile etiketlenmiştir;
  • veritabanını değiştiren yöntemler [@Transactional] ile etiketlenmiştir;
  • [delete] yöntemleri etiketlenmez ve bu nedenle bir işlem içinde yürütülmez. Buradaki mantık, bir silme işleminin başarısız olması durumunda, kullanıcının daha önce başarıyla gerçekleştirilen tüm silme işlemlerini iptal etmek istemeyeceğidir;

4.9. [DaoCategorie] sınıfı

  

[DaoCategorie] sınıfı, [IDao<Categorie>] arayüzünü uygular; bu arayüz,MySQL ve [dbproduitscategories] veritabanlarındaki [CATEGORIES] tablosundaki verilere erişim sağlar. Sınıfın iskeleti şu şekildedir:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;

import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;

import com.google.common.collect.Lists;

@Component
public class DaoCategorie extends AbstractDao<Categorie> {

    // sabitler

    // enjeksiyonlar
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertCategorie;
    @Autowired
    private IDao<Produit> daoProduit;

    @Override
    public List<Categorie> getAllShortEntities() {
    ...
    }

    @Override
    public List<Categorie> getAllLongEntities() {
    ...
    }

    @Override
    public void deleteAllEntities() {
    ...
    }

    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
    ...
    }

    @Override
    protected List<Categorie> getShortEntitiesByName(List<String> names) {
    ...
    }

    @Override
    protected List<Categorie> getLongEntitiesById(List<Long> ids) {
    ...
    }

    @Override
    protected List<Categorie> getLongEntitiesByName(List<String> names) {
    ...
    }

    @Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
    ...
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
    ...
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
    ...
    }

...
}

// --------------------- eşleştiriciler
class ShortCategorieMapper implements RowMapper<Categorie> {
....
}

class LongCategorieMapper implements RowMapper<Categorie> {
....
}
  • 28. satır: [DaoCategorie] sınıfı bir Spring bileşenidir ve bu nedenle diğer Spring bileşenlerine enjekte edilebilir;
  • 29. satır: [DaoCategorie] sınıfı, [AbstractDao<Categorie>] soyut sınıfını genişletir; bu da onu [IDao<Categorie>] arayüzünün bir uygulaması yapar;
  • 34-37. satırlar: 4.4. paragrafta açıklanan [AppConfig] sınıfında tanımlanan bean'lerin enjeksiyonu;
  • satır 38-39: [PRODUITS] tablosundaki verilere erişimi yöneten [IDao<Produit>] arayüzünü uygulayan [DaoProduit] sınıfına bir referans enjeksiyonu;
  • satır 41-89: [IDao<Categorie>] arayüzünün uygulanması;
  • 95-101. satırlar: [RowMapper<T>] arayüzünü uygulayan iki iç sınıf;

Yöntemleri sırayla inceleyelim.

4.9.1. [getAllShortEntities] yöntemi

[getAllShortEntities] yöntemi, [CATEGORIES] tablosundaki tüm kategorileri kısa biçimleriyle döndürür:


    @Override
    public List<Categorie> getAllShortEntities() {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
}

Tüm yöntemler, Spring yapılandırma dosyasında tanımlanan ve Spring kütüphanesi JDBC tarafından sağlanan [namedParameterJdbcTemplate] nesnesine dayanır. Bu nesnenin çok sayıda yöntemi vardır. Yukarıda kullanılan yöntem şöyledir:

Image

  • [sql], yürütülmesi gereken SQL komutudur;
  • [rowMapper], aşağıdaki [RowMapper<T>] arayüzünün bir örneğidir:

Image

Buradaki fikir şudur:

  • [namedParameterJdbcTemplate].query(String sql, RowMapper<T> rowMapper) yöntemi, [Select] türündeki SQL komutunu yürütür. Bu işlev, olası istisnaları yönetir ve SGBD ile olan bağlantının açılması ve kapatılmasını da gerçekleştirir. Yapamadığı tek şey,[ResultSet] türündeki öğeleri, aldığı nesneleri [Categorie] türünde kapsülleyememektir; çünkü [Categorie] türündeki alanlar ile [Resultset]'teki sütunlar arasındaki bağlantıyı bilmez. Daha sonra göreceğiz ki, bu bağlantı JPA teknolojisiyle oluşturulur ve bu sayede bir [ResultSet]'in öğelerinin T türü örnekleri içine kapsüllenmesi otomatik hale gelir. Şu an için, [query] yönteminin ikinci parametresi, bu kapsüllemeyi gerçekleştirebilen [RowMapper<T>] arayüzünün bir örneğidir;

Koda geri dönelim:


    @Override
    public List<Categorie> getAllShortEntities() {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLSHORTCATEGORIES, new ShortCategorieMapper());
        } catch (Exception e) {
            throw new DaoException(202, e, simpleClassName);
        }
}

SQL [ConfigJdbc.SELECT_ALLSHORTCATEGORIES] sırası şu şekildedir:


public final static String SELECT_ALLSHORTCATEGORIES = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c";

Sorgu, [ID, VERSIONING, NOM] sütunlarını [CATEGORIES] tablosundaki öğelerden talep ediyor. Sistematik olarak şu sözdizimini kullanacağız:


SELECT t1.COL1 as t1_COL1, t1.COL2 as t1_COL2 FROM TABLE1 t1, TABLE2 t2 WHERE ...

Önemli olan, SELECT ile elde edilen sütunların, [as nom_colonne] özniteliği kullanılarak adlandırılmasıdır. SGBD arasında taşınabilir olmanın tek yolu budur; çünkü bunların hepsi, farklı tablolardaki sütunların aynı ada sahip olduğu bir SELECT ile elde edilen sütunları adlandırmak için kendine özgü bir yönteme sahiptir (örneğin bizim durumumuzda ID, NOM veya VERSIONING). Bu belirsizliği, bu sütunların alması gereken adları kendimiz belirterek ortadan kaldırıyoruz.

[ShortCategorieMapper] iç sınıfı şu şekildedir:


class ShortCategorieMapper implements RowMapper<Categorie> {

    @Override
    public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSIONING"), rs.getString("c_NOM"), null);
    }
}
  • 1. satır: [ShortCategorieMapper] sınıfı, [RowMapper<Categorie>] arayüzünü uygular ve bu nedenle, [SELECT] komutuyla üretilen [ResultSet rs]'in bir satırını kapsüllemek üzere tasarlanmış olan 4-5 arasındaki [mapRow] yöntemini uygulamak zorundadır; bu yöntemin görevi, [SELECT] emri tarafından üretilen [ResultSet rs] satırını bir [Categorie] türünde kapsüllemektir;
  • 5. satır: Bu kapsülleme işlemi gerçekleştirilmiştir. [rs.getType(nom)] yöntemleri tarafından kullanılan adın, SELECT'in sütunlarındaki [as nom] özniteliklerinde kullanılan ad olduğu dikkatle belirtilmelidir;

Böylece, istisnaları veya bağlantıları yönetmeden kategorilerin kısa versiyonlarından oluşan listeyi elde ettik. Spring JDBC kütüphanesinin avantajı da budur; tablo öğelerinin yönetiminde faktörleştirilebilecek her şeyi yönetir ve faktörleştirilemeyen kısımları geliştiriciye bırakır.

4.9.2. [getAllLongEntities] yöntemi

[getAllLongEntities] yöntemi, [CATEGORIES] tablosundaki tüm kategorileri uzun biçimleriyle döndürür:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
                    new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(223, e, simpleClassName);
        }
}

SQL [ConfigJdbc.SELECT_ALLLONGCATEGORIES] komutunun sırası şöyledir:


public final static String SELECT_ALLLONGCATEGORIES = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID";    

Burada amaç, kategorileri ürünleriyle birlikte getirmektir. Bu, [CATEGORIES] tablosunu, [PRODUITS] tablosuyla, [CATEGORIE_ID] yabancı anahtarı aracılığıyla birleştirerek elde edilir; bu yabancı anahtar,[PRODUITS] tablosundan [CATEGORIES] tablosuna uzanır. [FROM PRODUITS p RIGHT JOIN CATEGORIES c ON p.CATEGORIE_ID=c.ID] sözdizimi, ilişkili ürünü olmayan kategorileri de getirmemizi sağlar. Bu durumda, SELECT sorgusu, tüm sütunları NULL'te bulunan bir kategori ve bir ürünü getirir.

[LongCategorieMapper] sınıfı şu şekildedir:


class LongCategorieMapper implements RowMapper<Categorie> {

    @Override
    public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
        Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
        List<Produit> produits = new ArrayList<Produit>();
        long idProduit = rs.getLong("p_ID");
        // ürün içermeyen kategori durumu
        if (!rs.wasNull()) {
            produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
                    rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
        }
        categorie.setProduits(produits);
        return categorie;
    }
}
  • 4. satır: [mapRow] yöntemi, [Categorie] nesnesini, [produits] alanı doldurulmuş olarak döndürmelidir; bunu, önceki SELECT emrinden gelen [ResultSet] satırından kullanarak yapmalıdır;

Sonuç olarak, işlem:


[namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,new LongCategorieMapper())]

şu türde bir liste döndürecektir:

1
2
3
4
5
6
7
c1, produits11
c1, produit12
...
c1,produits1n
c2, produits21
c2, produits22
...

burada her bir [ci] kategorisi, tek bir [produitsij] öğesini içeren bir ürün listesi olan [produits] alanına sahip olacaktır. Oysa bizim ihtiyacımız olan liste şudur:

c1, produits1
c2, produits2

Burada her bir [ci] kategorisi, [produits] adlı bir alana sahip olacak ve bu alan, [produiti1, produiti2, ...] ürün listesini içerecektir. Bu, elde edilen kategori listesini özel bir [filterCategories] yöntemine aktararak gerçekleştirilir:


    @Override
    public List<Categorie> getAllLongEntities() {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_ALLLONGCATEGORIES,
                    new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(223, e, simpleClassName);
        }
}

[filterCategories] yöntemi şu şekildedir:


    private List<Categorie> filterCategories(List<Categorie> categories) {
        if (categories.size() == 0) {
            return categories;
        }
        // görüntülenecek kategoriler
        List<Categorie> cats = new ArrayList<Categorie>();
        // elde edilen kategori listesi taranıyor
        for (Categorie categorie : categories) {
            boolean trouve = false;
            for (Categorie cat : cats) {
                if (categorie.equals(cat)) {
                    cat.addProduit(categorie.getProduits().get(0));
                    trouve = true;
                    break;
                }
            }
            // bulundu mu?
            if (!trouve) {
                cats.add(categorie);
            }
        }
        // sonuç
        return cats;
}
  • 1. satır: [List<Categorie> categories], filtrelenecek (veya gruplandırılacak) kategori listesidir;
  • 6. satır: arayan tarafa geri gönderilecek kategorilerin listesi;
  • satır 8-21: filtrelenecek listedeki her kategori işlenir;
  • satır 10-16: mevcut kategori [categorie]'in, oluşturulacak [cats] kategori listesinde zaten mevcut olup olmadığına bakılır (iki kategorinin, aynı birincil anahtara sahip olması durumunda eşit olduğu belirtilmiştir, bkz. paragraf 4.6);
  • satır 11-14: Eğer zaten mevcutsa, [categorie] içinde kapsüllenmiş ürün, [cat] ürün listesine eklenir;
  • satır 18-20: mevcut kategori [categorie], oluşturulacak [cats] kategori listesinde henüz mevcut değilse, tek bir öğe içeren ürün listesiyle birlikte bu listeye eklenir;

SQL Select komutunun, ilişkili ürün içermeyen kategoriler döndürdüğü durumu ele alalım. [LongCategorieMapper] sınıfını hangi varlık oluşturur?


class LongCategorieMapper implements RowMapper<Categorie> {

    @Override
    public Categorie mapRow(ResultSet rs, int rowNum) throws SQLException {
        Categorie categorie = new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null);
        List<Produit> produits = new ArrayList<Produit>();
        long idProduit = rs.getLong("p_ID");
        // ürün içermeyen kategori durumu
        if (!rs.wasNull()) {
            produits.add(new Produit(idProduit, rs.getLong("p_VERSION"), rs.getString("p_NOM"), rs.getLong("p_CATEGORIE_ID"),
                    rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), categorie));
        }
        categorie.setProduits(produits);
        return categorie;
    }
}

SQL Select emrinin ürün içermeyen bir kategori döndürdüğü durumda, kategori ile birlikte döndürülen ürün sütunlarının tümü SQL NULL değerini içerir. Bu durum 7-9. satırlarda işlenir:

  • 7. satır: Ürünün birincil anahtarı uzun tamsayı olarak alınır;
  • 9. satır: Okunan değerin SQL, NULL (rs.wasNull) olup olmadığına bakılır. Eğer değilse, ürün 6. satırdaki listeye eklenir; aksi takdirde hiçbir şey eklenmez ve ürün listesi boş kalır.

Her durumda, [produits] alanına sahip ve null olmayan bir kategori döndürüldüğüne dikkat edilmelidir.

4.9.3. [getShortEntitiesById] yöntemi

[getShortEntitiesById] yöntemi, [getAllShortEntities] yöntemine benzerdir; tek farkı, bir listede belirtilen birincil anahtarlara sahip varlıkların yalnızca döndürülmesidir:


    @Override
    protected List<Categorie> getShortEntitiesById(List<Long> ids) {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYID,
                    Collections.singletonMap("ids", ids), new ShortCategorieMapper());
        } catch (Exception e) {
            throw new DaoException(203, e, simpleClassName);
        }
}
  • 4. satırda, kullanılan [query] yönteminin imzası şöyledir:

Image

İlk parametre, parametreleri belirlenmiş bir SQL [Select] komutudur. İkinci parametre ise her bir parametreyi bir değere eşleyen bir sözlüktür. Üçüncüsü, [Select] komutunun sonucu olan [ResultSet]'in bir satırını T türünde bir nesne içinde kapsülleyen sınıfın örneğidir;

  • 4. satır: Parametreleri girilmiş SQL [Select] komutu şu şekildedir:

public final static String SELECT_SHORTCATEGORIE_BYID = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.ID in (:ids)";

Bu komut, [CATEGORIES] tablosundan birincil anahtarları ids listesinde bulunan kategorileri çıkarır.

  • 5. satır: [query] yönteminin ikinci parametresi, burada 'ids' anahtarını (1. parametre) 1. satırda [getShortEntitiesById] yöntemine parametre olarak geçirilen [ids] listesiyle eşleştiren bir sözlüktür. [Collections] sınıfı, daha önce bahsettiğimiz [Google Guava] kütüphanesine aittir. [Collections.singleMap], tek elemanlı bir sözlük döndürür;
  • 5. satır: [Select]'in sonucu olan [ResultSet]'teki bir satırı [Categorie] türünde bir nesneye kapsüllemekle görevli sınıf, daha önce incelediğimiz [ShortCategorieMapper] sınıfıdır;

Tipik olarak burada [maxPreparedStatementParameters] bean'i devreye girer. Nitekim, birincil anahtar listesini temsil eden SQL emrinin [:ids] parametresi, 1'den birkaç bine kadar parametre içerebilir. Bu sayı için, her bir SGBD'e bağlı olarak bir sınır vardır. MySQL için 10.000 parametreyi hatasız bir şekilde geçebildik ve bunun ötesini test etmedik. SQL Sunucusu için resmi sınır 2100’dür. Firebird için ise 1000 bile fazla geldi. Bu sayıyı 100’e düşürdük. Genel olarak, farklı SGBD’ler için bu sayının maksimum sınırını test etmedik.

4.9.4. [getLongEntitiesById] yöntemi

[getLongEntitiesById] yöntemi, [getShortEntitiesById] yöntemine benzerdir; tek farkı, kategorilerin uzun sürümlerini döndürmesidir:


    @Override
    protected List<Categorie> getLongEntitiesById(List<Long> ids) {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYID,
                    Collections.singletonMap("ids", ids), new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(205, e, simpleClassName);
        }
}
  1. satırda, SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYID] sorgusu şu şekildedir:

public final static String SELECT_LONGCATEGORIE_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.ID in (:ids)";

4.9.5. [getShortEntitiesByName] yöntemi

[getShortEntitiesByName] yöntemi, kategorilerin birincil anahtarları yerine adları üzerinden aranması dışında [getShortEntitiesById] yöntemine benzerdir:


    @Override
    protected List<Categorie> getShortEntitiesByName(List<String> names) {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME,
                    Collections.singletonMap("noms", names), new ShortCategorieMapper());
        } catch (Exception e) {
            throw new DaoException(204, e, simpleClassName);
        }
}

4. satırda, SQL [ConfigJdbc.SELECT_SHORTCATEGORIE_BYNAME] sıralaması şu şekildedir:


public final static String SELECT_SHORTCATEGORIE_BYNAME = "SELECT c.ID as c_ID, c.VERSIONING as c_VERSIONING, c.NOM as c_NOM FROM CATEGORIES c WHERE c.NOM in (:noms)";

4.9.6. [getLongEntitiesByName] yöntemi

[getLongEntitiesByName] yöntemi, kategorilerin uzun versiyonlarında aranması dışında [getShortEntitiesByName] yöntemine benzerdir:


    @Override
    protected List<Categorie> getLongEntitiesByName(List<String> names) {
        try {
            return filterCategories(namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME,
                    Collections.singletonMap("noms", names), new LongCategorieMapper()));
        } catch (Exception e) {
            throw new DaoException(215, e, simpleClassName);
        }
}

4. satırda, SQL [ConfigJdbc.SELECT_LONGCATEGORIE_BYNAME] emri şu şekildedir:


public final static String SELECT_LONGCATEGORIE_BYNAME = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p RIGHT JOIN CATEGORIES c ON c.ID=p.CATEGORIE_ID WHERE c.NOM in(:noms)";

4.9.7. [deleteAllEntities] yöntemi

[deleteAllEntities] yöntemi, [CATEGORIES] tablosundaki tüm kategorileri siler:


    @Override
    public void deleteAllEntities() {
        try {
            // tüm kategoriler ve buna bağlı olarak tüm ürünler siliniyor
            namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_ALLCATEGORIES, (Map<String, Object>) null);
        } catch (Exception e) {
            throw new DaoException(208, e, simpleClassName);
        }
}
  • 4. satır: Kullanılan [namedParameterJdbcTemplate.update] yöntemi aşağıdaki imzaya sahiptir:

Image

İlk parametre, güncelleme için yapılandırılmış bir SQL emridir (INSERT, UPDATE, DELETE). İkinci parametre, SQL emrinin çeşitli parametrelerine değerler atayan sözlüktür. Yöntem, SQL emriyle güncellenen satır sayısını döndürür.

  • 4. satır: SQL [ConfigJdbc.DELETE_ALLCATEGORIES] emri şöyledir:

public final static String DELETE_ALLCATEGORIES = "DELETE FROM CATEGORIES";

Dolayısıyla bu, parametreli bir komut değildir. Bu nedenle, [update] yönteminin ikinci parametresinin değeri null’tir.

4.9.8. [deleteAllEntitiesById] yöntemi

[deleteAllEntitiesById] yöntemi, birincil anahtarları geçirilen [CATEGORIES] tablosundaki kategorileri siler:


    @Override
    protected void deleteEntitiesById(List<Long> ids) {
        try {
            namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYID, Collections.singletonMap("ids", ids));
        } catch (Exception e) {
            throw new DaoException(209, e, simpleClassName);
        }
}

4. satırda, SQL [ConfigJdbc.DELETE_CATEGORIESBYID] komutu şu şekildedir:


public final static String DELETE_CATEGORIESBYID = "DELETE FROM CATEGORIES WHERE ID in (:ids)";

4.9.9. [deleteAllEntitiesByName] yöntemi

[deleteAllEntitiesByName] yöntemi, adları geçirilen [CATEGORIES] tablosundaki kategorileri siler:


    @Override
    protected void deleteEntitiesByName(List<String> names) {
        try {
            namedParameterJdbcTemplate.update(ConfigJdbc.DELETE_CATEGORIESBYNAME, Collections.singletonMap("noms", names));
        } catch (Exception e) {
            throw new DaoException(225, e, simpleClassName);
        }
}

4. satırda, SQL [ConfigJdbc.DELETE_CATEGORIESBYNAME] komutu şu şekildedir:


public final static String DELETE_CATEGORIESBYNAME = "DELETE FROM CATEGORIES WHERE NOM in (:noms)";

4.9.10. [saveEntities] yöntemi

4.9.10.1. Kod

Bu yöntemin imzası şöyledir:


    @Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {

Yöntem, parametre olarak bir kategori listesi alır. Bu liste üzerinde aşağıdaki işlemleri gerçekleştirir:

  • eğer kategorinin birincil anahtarı null ise, SQL INSERT işlemi yapılır; aksi takdirde SQL UPDATE işlemi yapılır;
  • bu işlem, kategorideki her bir ürün için tekrarlanır;

Yöntem, kalıcı hale getirilmiş veya güncellenmiş kategorilerin listesini döndürür. Döndürülen liste, sürüm numaraları hariç olmak üzere tablolarda bulunan kategorilerin ve ürünlerin tam bir yansımasıdır: çünkü veritabanında sürüm numaraları artırılmış olsa da, güncellenen varlıklar içinde bu sürüm numaraları değiştirilmez.

Bu, açık ara en karmaşık yöntemdir. Kod şöyledir:


@Override
    protected List<Categorie> saveEntities(List<Categorie> entities) {
        try {
            // --------------------------------------------- kategoriler
            List<Categorie> insertCategories = new ArrayList<Categorie>();
            List<Categorie> updateCategories = new ArrayList<Categorie>();
            // kategoriler taranıyor
            for (Categorie categorie : entities) {
                // ekle mi, güncelle mi?
                if (categorie.getId() == null) {
                    insertCategories.add(categorie);
                } else {
                    updateCategories.add(categorie);
                }
            }
            // kategoriler ekleniyor
            if (insertCategories.size() > 0) {
                insertCategories(insertCategories);
            }
            // kategorileri güncelle
            if (updateCategories.size() > 0) {
                updateCategories(updateCategories);
            }

            // --------------------------------------------- ürünler
            // kategorilerdeki ürünler güncelleniyor
            List<Produit> allProduits = new ArrayList<Produit>();
            for (Categorie categorie : entities) {
                List<Produit> produits = categorie.getProduits();
                Long idCategorie = categorie.getId();
                if (produits != null) {
                    // tüm ürünler listesine ekleniyor
                    allProduits.addAll(produits);
                    // ürünler tek tek taranarak kendi kategorilerine eklenir
                    for (Produit produit : produits) {
                        // ürün, kategorisine ekleniyor
                        produit.setIdCategorie(idCategorie);
                        produit.setCategorie(categorie);
                    }
                }
            }
            // ürünlerin eklenmesi / güncellenmesi
            daoProduit.saveEntities(allProduits);
            // sonuç
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(207, e, simpleClassName);
        }
    }
  • 5-23. satırlar: kategorilerin eklenmesi veya güncellenmesi;
  • 26-43. satırlar: ürünlerin eklenmesi veya güncellenmesi;
  • 35-39. satırlar: bu kod, her ürünü kendi kategorisine bağlar. Önceki kategori ekleme aşamasında, kategorilere bir birincil anahtar atanmıştır; bu anahtarın, ürünün [idCategorie] alanına (37. satır) girilmesi gerekir. Ayrıca, 37-38. satırlar, kullanıcının her bir ürünü kendi kategorisine doğru şekilde bağlamadığı durumları düzeltmeye olanak tanır. Bu ilişkinin doğru olması için [Categorie] .add(Ürün p) yöntemini kullanmak gerekir; ancak bir kullanıcının bu yöntemi kullanmadan bir ürünü doğrudan kategorinin ürün listesine eklemesine engel olan bir şey yoktur; ancak bu durumda ürün p'nin [idCategorie, categorie] alanlarının yanlış doldurulma riski vardır;
  • 43. satır: Ürünlerin kalıcı hale getirilmesi / güncellenmesi görevi, [IDao<Produit>] arayüzünün örneğine devredilir. Bu örneğin, [DaoCategorie] sınıfına enjekte edildiği hatırlatılmalıdır:

    @Autowired
    private IDao<Produit> daoProduit;

4.9.10.2. Kategorilerin eklenmesi

Kategoriler, aşağıdaki özel [insertCategories] yöntemi ile [CATEGORIES] tablosuna eklenir:


private List<Categorie> insertCategories(List<Categorie> categories) {
        Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
        try {
            // eklenecek kategoriler
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // birincil anahtarı kaydediyoruz
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // hepsi OK - birincil anahtarları kalıcı kategorilere atanır
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // sonuç
        return categories;
    }
  • 6. satır: Aşağıdaki satırlarla sınıfa enjekte edilen [simpleJdbcInsertCategorie] bean'i kullanılır:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertCategorie;

Bu bean, projenin [AppConfig] sınıfında şu şekilde tanımlanmıştır:


import org.springframework.jdbc.core.simple.SimpleJdbcInsert;


    @Bean
    public SimpleJdbcInsert simpleJdbcInsertCategorie(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource).withTableName(ConfigJdbc.TAB_CATEGORIES)
                .usingGeneratedKeyColumns(ConfigJdbc.TAB_CATEGORIES_ID)
                .usingColumns(ConfigJdbc.TAB_CATEGORIES_NOM);
}
  • 5. satırda, [SimpleJdbcInsert] sınıfı, Spring kütüphanesindeki JDBC sınıfına (1. satır) aittir:
    • [SimpleJdbcInsert] yapıcı parametresi, üzerinde işlem yapılan veri kaynağıdır;
    • [withTableName] cümlesi, bir öğenin ekleneceği tabloyu belirtmeye yarar; burada bu tablo [CATEGORIES]'tir;
    • [usingGeneratedKeyColumns] cümlesi, otomatik olarak oluşturulan birincil anahtar sütununu belirtmeye yarar; burada bu sütun [ID]'tir;
    • [usingColumns] cümlesi, ekleme işlemini belirli sütunlarla sınırlandırmaya yarar. Burada, SGBD tarafından otomatik olarak oluşturulan [ID] sütunu ve varsayılan değeri 1 olan [VERSIONING] sütunu hariç tutulmaktadır;

[insertCategories] yönteminin koduna geri dönelim:


private List<Categorie> insertCategories(List<Categorie> categories) {
        Map<Long, Categorie> mapCategories=new HashMap<Long,Categorie>();
        try {
            // eklenecek kategoriler
            for (Categorie categorie : categories) {
                Number newId = simpleJdbcInsertCategorie.executeAndReturnKey(getMapForCategorie(categorie));
                // birincil anahtar kaydedilir
                mapCategories.put(newId.longValue(), categorie);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // her şey OK - kalıcı kategorilere birincil anahtarlar atanıyor
        for(Long id : mapCategories.keySet()){
            Categorie categorie=mapCategories.get(id);
            categorie.setId(id);
        }        
        // sonuç
        return categories;
}
  • 6. satır: [simpleJdbcInsertCategorie.executeAndReturnKey] yöntemi kullanılmaktadır:

Image

Yöntem, parametre olarak tablonun sütunları ile bu sütunlara eklenecek değerler arasında bağlantı kuran bir sözlük bekler. Sonuç olarak, birincil anahtarı [Number] türü biçiminde döndürür. [Number.longValue()] yöntemi, birincil anahtarı [Long] türü biçiminde elde etmeyi sağlar.

[getMapForCategorie] yöntemi, aşağıdaki özel yöntemdir:


    private Map<String, ?> getMapForCategorie(Categorie categorie) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put(ConfigJdbc.TAB_CATEGORIES_NOM, categorie.getNom());
        return map;
}

Sözlükteki anahtarlar, doldurulması gereken sütunların adlarıdır ([NOM]) ve sözlükteki değerler ise bu sütunlara eklenecek değerlerdir.

  • 8. satır [insertCategories]: Alınan birincil anahtar bir sözlükte saklanır. Tüm varlıkların eklendiğinden emin olana kadar bekleyeceğiz, ardından onlara birincil anahtarlarını atayacağız. Zira bir istisna durumunda tüm eklemeler iptal edilecek ve bu durumda 1. satırdaki [categories] varlıklarının da değişmeden kalmasını istiyoruz;
  • 14-17. satırlar: Artık her şeyin yolunda gittiğinden emin olduğumuza göre, oluşturulan birincil anahtarları kategorilere atıyoruz;
  • 19. satır: Kategorilerin bir listesini birincil anahtarlarıyla birlikte döndürüyoruz;

4.9.10.3. Kategorilerin güncellenmesi

Kategoriler, aşağıdaki özel [updateCategories] yöntemi ile güncellenir:


    private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // kategorinin veritabanında güncellenmesi
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // başarılı mıyız?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // başarılı olamadı - nedenini araştırıyoruz
                    // veritabanında kategori aranıyor
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // kategori mevcut değil
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // sürüm doğru değildi
                        throw new RuntimeException(String.format(
                                "Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
                    }
                }
            }
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(206, e, simpleClassName);
        }
}

Veritabanındaki C1 kategorisi, bellekteki C2 kategorisiyle ancak C1 ve C2 kategorilerinin sürümleri aynıysa güncellenebilir. Bu sürüm numarası, iki farklı kullanıcı tarafından varlığın aynı anda güncellenmesini önlemek için kullanılır: U1 ve U2 adlı iki kullanıcı, sürüm numarası V1 olan E varlığını okur. U1, E'yi değiştirir ve bu değişikliği veritabanına kaydeder: sürüm numarası bu durumda V1+1'e geçer. U2 da E'yi değiştirir ve bu değişikliği veritabanına kaydeder: veritabanındaki sürümden (V1+1) farklı bir sürüme (V1) sahip olduğu için bir istisna alır.

  • 2-29. satırlar: try bloğunun iki catch bloğu vardır:
    • ilki, 25. satırda yer alır ve 13. satırdaki kod tarafından tetiklenen [DaoException] türündeki olası istisnayı geçirmek içindir;
    • ikincisi, 27. satırda yer alır ve diğer istisna türlerini yönetmek içindir;
  • 3. satır: Güncellenecek tüm kategoriler taranır;
  • 4. satır: [namedParameterJdbcTemplate.update] yöntemi kullanılarak geçerli kategori güncellenir:

Image

  • komutu inceleyelim:

            int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,                         new BeanPropertySqlParameterSource(categorie));

SQL [ConfigJdbc.UPDATE_CATEGORIES] komutunun sırası şöyledir:


public final static String UPDATE_CATEGORIES = "UPDATE CATEGORIES SET VERSIONING=VERSIONING+1, NOM=:nom WHERE ID=:id AND VERSIONING=:version";

Komutun üç parametresi vardır (:id, :version, :nom) ve bunların değerleri, değiştirilen [categorie] nesnesinin aynı adlı alanlarında bulunur. Bu özelliği, ikinci parametre olarak "parametrelerin değerleri bu Java bean'in aynı isimli alanlarında bulunur" anlamına gelen [new BeanPropertySqlParameterSource(categorie)] değerini geçirerek kullanırız;

Bu işlem normal şekilde gerçekleştiğinde, sonuç değiştirilen satır sayısıdır, yani 0 veya 1.

İncelediğimiz koda geri dönelim:


private void updateCategories(List<Categorie> categories) {
        try {
            for (Categorie categorie : categories) {
                // veritabanındaki kategori güncellendi
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_CATEGORIES,
                        new BeanPropertySqlParameterSource(categorie));
                // Başarılı olundu mu?
                Long idCategorie = null;
                if (nbLignes == 0) {
                    // başarılı olamadı - nedeni araştırılıyor
                    // veritabanında kategori aranıyor
                    idCategorie = categorie.getId();
                    List<Categorie> categoriesInBd = getShortEntitiesById(idCategorie);
                    if (categoriesInBd.size() == 0) {
                        // kategori mevcut değil
                        throw new RuntimeException(String.format("Erreur de mise à jour. La catégorie de clé [%s] n'existe pas",
                                idCategorie));
                    } else {
                        // sürüm doğru değildi
                        throw new RuntimeException(String.format(
                                "Erreur de mise à jour. La catégorie de clé [%s] n'a pas la bonne version", idCategorie));
                    }
                }
            }
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(206, e, simpleClassName);
        }
}
  • 9. satır: Değişikliğin başarılı olup olmadığına bakılır;
  • 10. satır: Değişiklik başarısız oldu. [WHERE] cümlesi, [ID] ve [VERSIONING] sütunlarını içerdiğinden, [WHERE]'in başarısız olmasına neden olan sütun aranır;
  • satır 12-18: Kategorinin [id] anahtarının veritabanında olup olmadığı kontrol edilir. Eğer yoksa, uygun bir hata mesajıyla birlikte bir [RuntimeException] çalıştırılır;
  • satır 19-22: sürümün yanlış olması durumunu işler;

4.10. [DaoProduit] sınıfı

  

[DaoProduit] sınıfı, [IDao<Produit>] arayüzünü uygular; bu arayüz,MySQL ve [dbproduitscategories] veritabanlarındaki [PRODUITS] tablosundaki verilere erişim sağlar. Sınıfın iskeleti şöyledir:


package spring.jdbc.dao;

import generic.jdbc.config.ConfigJdbc;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.stereotype.Component;

import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.DaoException;

import com.google.common.collect.Lists;

@Component
public class DaoProduit extends AbstractDao<Produit> {

    // eklemeler
    @Autowired
    private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertProduit;

    @Override
    public List<Produit> getAllShortEntities() {
...
    }

    @Override
    public List<Produit> getAllLongEntities() {
....
    }

    @Override
    public void deleteAllEntities() {
    ...
    }

    @Override
    protected List<Produit> getShortEntitiesById(List<Long> ids) {
...
    }

    @Override
    protected List<Produit> getShortEntitiesByName(List<String> names) {
    ....
    }

    @Override
    protected List<Produit> getLongEntitiesById(List<Long> ids) {
...
    }

    @Override
    protected List<Produit> getLongEntitiesByName(List<String> names) {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
                    Collections.singletonMap("noms", names), new LongProduitMapper());
        } catch (Exception e) {
            throw new DaoException(112, e, simpleClassName);
        }
    }

    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
    ...
    }

    @Override
    protected void deleteEntitiesById(List<Long> ids) {
    ....
    }

    @Override
    protected void deleteEntitiesByName(List<String> names) {
...
    }
}

// --------------------- haritalayıcılar
class ShortProduitMapper implements RowMapper<Produit> {

...
}

class LongProduitMapper implements RowMapper<Produit> {
...
}

Kod, [DaoCategorie] sınıfındakine çok benziyor. Burada sadece birkaç yöntemi inceleyeceğiz.

4.10.1. [getShortEntitiesById] yöntemi

[getShortEntitiesById] yöntemi, birincil anahtarları geçirilen ürünlerin kısa versiyonunu oluşturur:


    @Override
    protected List<Produit> getShortEntitiesById(List<Long> ids) {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_SHORTPRODUIT_BYID,
                    Collections.singletonMap("ids", ids), new ShortProduitMapper());
        } catch (Exception e) {
            throw new DaoException(109, e, simpleClassName);
        }
}
  • 4. satır: SQL Select [ConfigJdbc.SELECT_SHORTPRODUIT_BYID] komutu şu şekildedir:

public final static String SELECT_SHORTPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSIONING, p.NOM as p_NOM, p.CATEGORIE_ID as p_CATEGORIE_ID, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION FROM PRODUITS p WHERE p.ID in (:ids)";
  • 4. satır: [ResultSet]'i bir ürün listesi içinde kapsüllemekle görevli [ShortProduitMapper] sınıfı şöyledir:

class ShortProduitMapper implements RowMapper<Produit> {

    @Override
    public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSIONING"), rs.getString("p_NOM"),
                rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), null);
    }
}

4.10.2. [getLongEntitiesByName] yöntemi

[getShortEntitiesById] yöntemi, isimleri geçirilen ürünlerin uzun versiyonunu döndürür:


    @Override
    protected List<Produit> getLongEntitiesByName(List<String> names) {
        try {
            return namedParameterJdbcTemplate.query(ConfigJdbc.SELECT_LONGPRODUIT_BYNAME,
                    Collections.singletonMap("noms", names), new LongProduitMapper());
        } catch (Exception e) {
            throw new DaoException(112, e, simpleClassName);
        }
}
  • 4. satır: SQL Select [ConfigJdbc.SELECT_LONGPRODUIT_BYNAME] komutu şu şekildedir:

public final static String SELECT_LONGPRODUIT_BYID = "SELECT p.ID as p_ID, p.VERSIONING as p_VERSION, p.NOM as p_NOM, p.PRIX as p_PRIX, p.DESCRIPTION as p_DESCRIPTION, p.CATEGORIE_ID AS p_CATEGORIE_ID, c.ID as c_ID, c.NOM as c_NOM, c.VERSIONING as c_VERSION FROM PRODUITS p, CATEGORIES c WHERE p.ID in (:ids) AND p.CATEGORIE_ID=c.ID";
  • 4. satır: [ResultSet]'teki öğeleri uzun versiyonlu ürünler halinde kapsüllemekle görevli [LongProduitMapper] sınıfı şöyledir:

class LongProduitMapper implements RowMapper<Produit> {

    @Override
    public Produit mapRow(ResultSet rs, int rowNum) throws SQLException {
        return new Produit(rs.getLong("p_ID"), rs.getLong("p_VERSION"), rs.getString("p_NOM"),
                rs.getLong("p_CATEGORIE_ID"), rs.getDouble("p_PRIX"), rs.getString("p_DESCRIPTION"), new Categorie(rs.getLong("c_ID"), rs.getLong("c_VERSION"), rs.getString("c_NOM"), null));
    }
}

4.10.3. [saveEntities] yöntemi

[saveEntities] yöntemi, yeni ürünler eklemek (id==null) veya mevcut ürünleri güncellemek (id!=null) için aynı şekilde kullanılır:


    @Override
    protected List<Produit> saveEntities(List<Produit> entities) {
        try {
            // eklenecek ürünler
            List<Produit> insertProduits = new ArrayList<Produit>();
            // güncellenecek ürünler
            List<Produit> updateproduits = new ArrayList<Produit>();
            // alınan varlıkların listesi taranıyor
            for (Produit produit : entities) {
                Long id = produit.getId();
                if (id == null) {
                    insertProduits.add(produit);
                } else {
                    updateproduits.add(produit);
                }
            }
            // eklemeler
            insertProduits(insertProduits);
            // değişiklikler
            updateProduits(updateproduits);
            // sonuç
            return entities;
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(103, e, simpleClassName);
        }
}
  1. satırda, eklenecek ürünler aşağıdaki özel [insertProduits] yöntemi ile eklenir:

private List<Produit> insertProduits(List<Produit> produits) {
        Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();
        try {
            // eklenecek ürünler
            for (Produit produit : produits) {
                Number newId = simpleJdbcInsertProduit.executeAndReturnKey(getMapForProduit(produit));
                // birincil anahtar not edilir
                mapProduits.put(newId.longValue(), produit);
            }
        } catch (Exception e) {
            throw new DaoException(201, e, simpleClassName);
        }
        // hepsi OK - kalıcı ürünlere birincil anahtarlar atanır
        for (Long id : mapProduits.keySet()) {
            Produit produit = mapProduits.get(id);
            produit.setId(id);
        }
        // sonuç
        return produits;
    }

    private Map<String, ?> getMapForProduit(Produit produit) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put(ConfigJdbc.TAB_PRODUITS_NOM, produit.getNom());
        map.put(ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID, produit.getIdCategorie());
        map.put(ConfigJdbc.TAB_PRODUITS_PRIX, produit.getPrix());
        map.put(ConfigJdbc.TAB_PRODUITS_DESCRIPTION, produit.getDescription());
        return map;
    }

Bu yöntem, 4.9.10.3 numaralı paragrafta incelenen [insertCategories] yöntemine benzerdir.

  • 4. satır: Sınıfa enjekte edilen [simpleJdbcInsertProduit] bean'i kullanılır:

    @Autowired
    private SimpleJdbcInsert simpleJdbcInsertProduit;

Bu bean, projeyi yapılandıran [AppConfig] sınıfında tanımlanmıştır:


    @Bean
    public SimpleJdbcInsert simpleJdbcInsertProduit(DataSource dataSource) {
        return new SimpleJdbcInsert(dataSource)
                .withTableName(ConfigJdbc.TAB_PRODUITS)
                .usingGeneratedKeyColumns(ConfigJdbc.TAB_PRODUITS_ID)
                .usingColumns(ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION,ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID);
}
  • 3-6. satırlar: [simpleJdbcInsertProduit] bean'i
    • , [dbproduitscategories] veritabanındaki veri kaynağına (satır 3) ve bu kaynağın [ConfigJdbc.TAB_PRODUITS] tablosuna (satır 4) bağlanmıştır;
    • bu tablonun birincil anahtarı [ConfigJdbc.TAB_PRODUITS_ID] sütununda oluşturulur (satır 5);
    • sadece [ConfigJdbc.TAB_PRODUITS_NOM, ConfigJdbc.TAB_PRODUITS_PRIX, ConfigJdbc.TAB_PRODUITS_DESCRIPTION, ConfigJdbc.TAB_PRODUITS_CATEGORIE_ID] sütunlarına değerler atanır (satır 6);

Ürünleri güncelleyen [updateProduits] yöntemi ([saveEntities]'in 20. satırı) şu şekildedir:


private void updateProduits(List<Produit> updateProduits) {
        try {
            // ürünler taranıyor
            for (Produit produit : updateProduits) {
                // ürün veritabanında güncelleniyor
                int nbLignes = namedParameterJdbcTemplate.update(ConfigJdbc.UPDATE_PRODUITS,
                        new BeanPropertySqlParameterSource(produit));
                // başarılı mı?
                Long idProduit = null;
                if (nbLignes == 0) {
                    // başarılı olamadı - nedeni araştırılıyor
                    // veritabanında ürün aranıyor
                    idProduit = produit.getId();
                    List<Produit> produitsInBd = getShortEntitiesById(idProduit);
                    if (produitsInBd.size() == 0) {
                        // ürün mevcut değil
                        throw new RuntimeException(String.format("Erreur de mise à jour. Le produit de clé [%s] n'existe pas",
                                idProduit));
                    } else {
                        // sürüm doğru değildi
                        throw new RuntimeException(String.format(
                                "Erreur de mise à jour. Le produit de clé [%s] n'a pas la bonne version", idProduit));
                    }
                }
            }
        } catch (DaoException e) {
            throw e;
        } catch (Exception e) {
            throw new DaoException(106, e, simpleClassName);
        }
    }

Bu, kategorileri güncelleyen komutla benzerdir (bkz. paragraf 4.9.10.3). 23. satırda, ürünleri güncellemek için yürütülen SQL [ConfigJdbc.UPDATE_PRODUITS] komutu şu şekildedir:


public final static String UPDATE_PRODUITS = "UPDATE PRODUITS SET VERSIONING=VERSIONING+1, NOM=:nom, PRIX=:prix, CATEGORIE_ID=:idCategorie, DESCRIPTION=:description WHERE ID=:id AND VERSIONING=:version";

[:id,:version,:nom,:prix,:idCategorie,:description] parametrelerinin adları, aynı zamanda [Produit] sınıfındaki alanların adlarıdır; bu sayede 6-7. satırlardaki komut kullanılarak mevcut ürün güncellenebilir.

4.11. Test katmanı

  

Test katmanı üç test sınıfından oluşur:

  • [JUnitTestCheckArguments]: Bu sınıftaki testler, [DAO] katmanındaki çeşitli yöntemleri geçersiz argümanlarla çağırır ve bunların doğru şekilde tepki verip vermediğini doğrular;
  • [JUnitTestDao]: Bu sınıftaki testler, [DAO] katmanındaki çeşitli yöntemleri çağırır ve bunların beklendiği gibi çalışıp çalışmadığını doğrular;
  • [JUnitTestPushTheLimits], [DAO] katmanını test etmek değil, performansını ölçmek amacıyla tasarlanmıştır;

Bu test katmanı, bu belgede önemli bir rol oynamaktadır. Nitekim bu katman, [IDao<T>] arayüzünün tüm uygulamaları için ortaktır. SGBD için altı tane vardır (1 adet JDBC uygulaması, 3 adet JPA uygulaması, 1 adet Spring MVC uygulaması, 1 adet güvenli Spring MVC uygulaması) vardır; dolayısıyla test edilen altı SGBD için toplam 36 test vardır. Test katmanı, tüm uygulamaların aynı şekilde tepki verdiğini doğrulamamızı sağlar.

4.11.1. [JUnitTestCheckArguments] testi

[JUnitTestCheckArguments] test sınıfı, [DAO] katmanındaki yöntemlerin yanlış argümanlarla çağrıldığında nasıl tepki verdiğini test eden 48 yönteme sahiptir. Temel yapısı şöyledir:


package spring.jdbc.tests;

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

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;
import spring.jdbc.infrastructure.MyIllegalArgumentException;

import com.google.common.collect.Lists;

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

    // katman [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    // yerel veriler
    private Iterable<String> names1 = null;
    private Iterable<String> names2 = Lists.newArrayList(new String[0]);
    private String[] names3 = null;
    private String[] names4 = new String[0];
    private Iterable<Long> ids1 = null;
    private Iterable<Long> ids2 = Lists.newArrayList(new Long[0]);
    private Long[] ids3 = null;
    private Long[] ids4 = new Long[0];
    private Iterable<Categorie> categories1 = null;
    private Iterable<Categorie> categories2 = Lists.newArrayList(new Categorie[0]);
    private Categorie[] categories3 = null;
    private Categorie[] categories4 = new Categorie[0];
    private Iterable<Produit> produits1 = null;
    private Iterable<Produit> produits2 = Lists.newArrayList(new Produit[0]);
    private Produit[] produits3 = null;
    private Produit[] produits4 = new Produit[0];

    ...

}
  • 19. satır: JUnit testi, Spring çerçevesiyle entegre olarak yapılacaktır;
  • 18. satır: Testlerden önce, projenin [AppConfig] sınıfında tanımlanan bean'ler örneklenecektir;
  • 23-26. satırlar: [DAO] katmanındaki iki arayüzün her birinden birer örnek enjekte edilecektir;
  • 29-44. satırlar: [DAO] katmanındaki yöntemlerin çağrı parametreleri hatalıdır;
  • 29. satır: isim listesi olarak [Iterable<String>] türünde bir null işaretçisi;
  • satır 30: isim listesi olarak [Iterable<String>] türünde boş bir liste;
  • 29. satır: isim dizisi olarak String[] türünde bir null işaretçisi;
  • satır 30: isim dizisi olarak String[] türünde boş bir dizi;
  • ...

[names1] alanı ile örneğin şu test yapılır:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName1() {
        daoProduit.getShortEntitiesByName(names1);
}
  • 1. satır: [getShortProduitsByName1] testinin [MyIllegalArgumentException] türünde bir istisna ile karşılaşması gerektiği belirtilir

[names2] alanı ile örneğin şu test yapılır:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName2() {
        daoCategorie.getLongEntitiesByName(names2);
}

[names3] alanı ile örneğin şu test yapılır:


    @Test(expected = MyIllegalArgumentException.class)
    public void getLongCategoriesByName3() {
        daoCategorie.getLongEntitiesByName(names3);
}

[names4] alanı ile örneğin şu test yapılır:


    @Test(expected = MyIllegalArgumentException.class)
    public void getShortProduitsByName4() {
        daoProduit.getShortEntitiesByName(names4);
}

Böylece tüm olası durumları test etmek için 48 test yapılır. [spring-jdbc-generic-04-JUnitTestCheckArguments] [1] adlı yürütme yapılandırması çalıştırılır. Elde edilen sonuç şöyledir: [2]:

4.11.2. [JUnitTestDao] testi

[JUnitTestDao] testi, [DAO] katmanındaki yöntemleri geçerli argümanlarla çağırır ve yöntemlerin beklendiği gibi çalıştığını doğrular. Varlıkların, kategorilerin veya ürünlerin eklenmesi, seçilmesi, güncellenmesi ve silinmesi işlemlerini doğrulayan toplam 74 test bulunmaktadır. Toplamda 1000'den fazla kod satırı bulunmaktadır. Bu yöntemlerden sadece birkaçını inceleyeceğiz.

4.11.2.1. Testin iskeleti

[JUnitTestDao] sınıfının iskeleti şu şekildedir:


package spring.jdbc.tests;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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.boot.test.SpringApplicationConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;

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

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

    // Spring bağlamı
    @Autowired
    private ApplicationContext context;
    // katman [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    // sabitler
    private final int NB_PRODUITS = 5;
    private final int NB_CATEGORIES = 2;

    // yerel
    // yerel
    private Map<Long, Categorie> mapCategories = new HashMap<Long, Categorie>();
    private Map<Long, Produit> mapProduits = new HashMap<Long, Produit>();

    @Before
    public void clean() {
        // her testten önce veritabanı temizlenir
        log("Vidage de la base de données", 1);
        // [CATEGORIES] tablosu ve buna bağlı olarak [PRODUITS] tablosu boşaltılır
        daoCategorie.deleteAllEntities();
        // sözlükler boşaltılır
        for (Long id : mapCategories.keySet()) {
            mapCategories.remove(id);
        }
        for (Long id : mapProduits.keySet()) {
            mapProduits.remove(id);
        }
    }
...
}
  • 27-28. satırlar: [JUnitTestCheckArguments] testinde olduğu gibi, burada da Spring ile entegre ve projenin [AppConfig] sınıfı tarafından yapılandırılmış bir testle karşı karşıyayız;
  • 32-33. satırlar: Tüm bean'lere erişim sağlayan Spring bağlamının enjeksiyonu;
  • satır 35-36: sınıf tarafından test edilen [IDao<Produit>] arayüzünün örneğinin enjeksiyonu;
  • 37-38. satırlar: Sınıf tarafından test edilen [IDao<Categorie>] arayüzünün örneğinin enjeksiyonu;
  • 41-42. satırlar: Bir testin veritabanındaki verilere ihtiyacı olduğunda, her biri [NB_PRODUITS] ürünlerini içeren [NB_CATEGORIES] kategorilerinden oluşan bir veritabanı oluşturulur. Böylece, [CATEGORIES] tablosunda [NB_CATEGORIES] kategorileri ve [PRODUITS] tablosunda [NB_CATEGORIES] * [NB_PRODUITS] ürünleri bulunur;
  • 46-47. satırlar: Ürünleri ve kategorileri depolayacağımız iki sözlük;
  • 49-62. satırlar: [clean] yöntemi her testten önce çalıştırılır (49. satır). 54. satırda, [CATEGORIES] tablosu boşaltılır. Burada şunu unutmamak gerekir: [PRODUITS] tablosunun, [CATEGORIES] tablosunun ID sütununda [CATEGORIE_ID] birincil anahtarı vardır ve bu anahtar şu şekilde tanımlanmıştır;
  • (devamı)
    • [1-3]'te, [PRODUITS] tablosunun [CATEGORIE_ID] yabancı anahtarı. Bu anahtar, [CATEGORIES] ve [4-5] tablolarının [ID] sütununu hedefler;
    • bir kategori silindiğinde, o kategoriye bağlı tüm ürünler de silinir ([6]). Bu nokta, [dbproduitscategories] veritabanını kullanan [DAO] katmanının oluşturulmasında kullanıldığı için dikkat edilmesi gereken önemli bir husustur;

Dolayısıyla, [CATEGORIES] tablosunun içeriği silindiğinde, [PRODUITS] tablosunun içeriği de silinecektir.

  • 56-58. satırlar: Kategori sözlüğü boşaltılır;
  • 59-61. satırlar: Ürün sözlüğü için de aynı işlem yapılır;

Unutulmamalıdır ki, her testten önce veritabanında boş tablolar ve bellekte boş sözlükler bulunur.

4.11.2.2. [verifyClean] yöntemi

[verifyClean] yöntemi, [clean] yönteminden sonra tabloların boş olup olmadığını kontrol eder:


    @Test
    public void verifyClean() {
        log("verifyClean", 1);
        List<Categorie> categories = daoCategorie.getAllShortEntities();
        Assert.assertEquals(0, categories.size());
        List<Produit> produits = daoProduit.getAllShortEntities();
        Assert.assertEquals(0, produits.size());
}

4.11.2.3. [fillDataBase] yöntemi

Bu yöntem, veritabanının test verileriyle doğru şekilde doldurulduğunu kontrol eder:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // veritabanı ve sözlükler doldurulur
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // görüntüleme
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // bazı kontroller
        Assert.assertEquals(NB_CATEGORIES, categories.size());
        Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
        for (Categorie categorie : categories) {
            checkShortCategorie(categorie);
        }
        for (Produit produit : produits) {
            checkShortProduit(produit);
        }
        // sözlükler tamamen boşaltılmış olmalıdır
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}

Bu test, birkaç özel yöntem kullanır:

  • [fill] 4. satır, veritabanını test verileriyle doldurur;
  • [registerCategories], 4. satır: [fill] yöntemi tarafından döndürülen verilerle sözlükleri doldurur. Bu iki sözlük, kalıcı varlıklarını temsil eder;
  • [showDataBase], 6. satırda [CATEGORIES] ve [PRODUITS] tablolarını okur ve okuduklarını döndürür;
  • [checkShortCategorie] satır 13, [showDataBase] tarafından okunan kategoriyi doğrular. Bu kategorinin kısa versiyonunun, kategori sözlüğüne kaydedilenle eşleşip eşleşmediğini kontrol eder;
  • [checkShortProduit] satır 16, ürünler için de aynısını yapar;
  • bir varlık sözlükte bulunduğunda, sözlükten kaldırılır. 19-20. satırlar, her iki sözlüğün de boş olup olmadığını kontrol eder. Bu iki koşul geçerliyse, bu şu anlama gelir:
    • [showDataBase] tarafından okunan tüm değerler sözlüklerde gerçekten bulunmuştur;
    • sözlüklerde okunanlar dışında başka varlıklar bulunmamaktadır;

[fill] özel yöntemi şu şekildedir:


    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // tablolar dolduruluyor
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, null, String.format("categorie[%d]", i), null);
            for (int j = 0; j < nbProduits; j++) {
                Produit produit = new Produit(null, null, String.format("produit[%d,%d]", i, j), null,
                        100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
                categorie.addProduit(produit);
            }
            categories.add(categorie);
        }
        // kategori ekleniyor - zincirleme olarak ürünler de
        // eklenecek
        categories = daoCategorie.saveEntities(categories);
        // sonuç
        return categories;
}
  • 3-12. satırlar: her biri [nbProduits] ürün içeren bir [nbCategories] kategori listesi oluşturulur;
  • 15. satır: Bu kategori listesi kalıcı hale getirilir. [daoCategorie.saveEntities] yönteminin, kategorilerde ürün varsa bu ürünleri de kalıcı hale getirdiğini görmüştük;
  • 17. satır: Kalıcı hale getirilen kategori listesi döndürülür. Kalıcı hale getirilen varlıklar (kategoriler ve ürünler) artık [id] alanında bir birincil anahtara sahiptir;

Özel [registerCategories] yöntemi, bu varlıkları iki sözlüğe de ekleyecektir:


    private void registerCategories(List<Categorie> categories) {
        // sözlükler
        for (Categorie categorie : categories) {
            mapCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                mapProduits.put(produit.getId(), produit);
            }
        }
}

Her bir sözlüğün erişim anahtarı, varlıkların birincil anahtarıdır.

Bu işlem tamamlandıktan sonra, daha önce doldurulan veritabanı aşağıdaki özel yöntem olan [showDataBase] tarafından okunacak ve görüntülenecektir:


    private Object[] showDataBase() throws BeansException, JsonProcessingException {
        // kategori listesi
        log("Liste des catégories", 2);
        List<Categorie> categories = daoCategorie.getAllShortEntities();
        affiche(categories, context.getBean("jsonMapperShortCategorie", ObjectMapper.class));
        // ürün listesi
        log("Liste des produits", 2);
        List<Produit> produits = daoProduit.getAllShortEntities();
        affiche(produits, context.getBean("jsonMapperShortProduit", ObjectMapper.class));
        // sonuç
        return new Object[] { categories, produits };
}
  • 4. ve 8. satırlar: kategorilerin ve ürünlerin kısa versiyonları alınır;
  • 11. satır: Alınan iki varlık listesini içeren bir dizi döndürülür;
  • 5. ve 9. satırlar: varlık listeleri, aşağıdaki özel [affiche] yöntemi ile görüntülenir:

    // T türündeki öğelerin listesinin görüntülenmesi
    private <T> void affiche(List<T> elements, ObjectMapper mapper) throws JsonProcessingException {
        for (T element : elements) {
            affiche(element, mapper);
        }
}

    // T türündeki bir öğenin görüntülenmesi
    private <T> void affiche(T element, ObjectMapper mapper) throws JsonProcessingException {
        System.out.println(mapper.writeValueAsString(element));
}

Varlıklar, jSON eşleştiricisi tarafından görüntülenir (satır 10). Bu eşleştirici, 2. satırdaki [affiche] yönteminin ikinci parametresidir. Spring bağlamı, Maven bağımlılığı [mysql-config-jdbc]'in [ConfigJdbc] dosyasında dört adet jSON eşleştiricisi tanımlar:


// filtreler jSON -------------------------------------
    @Bean
    public ObjectMapper jsonMapper() {
        return new ObjectMapper();
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortCategorie() {
        ObjectMapper jsonMapper = jsonMapper();
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongCategorie() {
        ObjectMapper jsonMapper = jsonMapper();
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperShortProduit() {
        ObjectMapper jsonMapper = jsonMapper();
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept("categorie")));
        return jsonMapper;
    }

    @Bean
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    ObjectMapper jsonMapperLongProduit() {
        ObjectMapper jsonMapper = jsonMapper();
        jsonMapper.setFilters(new SimpleFilterProvider().addFilter("jsonFilterProduit",
                SimpleBeanPropertyFilter.serializeAllExcept()).addFilter("jsonFilterCategorie",
                SimpleBeanPropertyFilter.serializeAllExcept("produits")));
        return jsonMapper;
    }
  • bu jSON eşleştiricileri (satır 7-9, 16-18, 26-28, 35-37) bir özniteliğe sahiptir

[@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)] 

özniteliğine sahiptir ve bu da onları Spring bağlamına yapılan her istekte örneklenen bean'ler haline getirir. Bu yeni bir durumdur. Şimdiye kadar gördüğümüz tüm Spring bean'leri singleton'lardı: tek bir örnek olarak oluşturulurlardı ve Spring bağlamından bir referans istendiğinde her seferinde bu örnek döndürülürdü. Neden bu değişiklik yapıldı? Aslında dört [jsonMapperShortCategorie, jsonMapperLongCategorie, jsonMapperShortProduit , jsonMapperLongProduit] bean'i, 2-5. satırlarda tanımlanan tek bir eşleştirici olan jSON'i (bu gerçekten bir singleton'dur) yapılandırır. Bu eşleştirici, bağlamın başlatılması sırasında tek bir kez değil, yukarıdaki dört bean'den herhangi birinin her çağrılmasında yeniden yapılandırılmalıdır. Eğer dört farklı jSON eşleştiricisi kullanmaya karar verilseydi (dört bean'in her biri için bir tane), o zaman bunlar singleton olabilirdi. Bu tamamen mümkündü. O durumda 10, 19, 29 ve 38. satırlara şunu yazardık:


ObjectMapper jsonMapper = new ObjectMapper();
  • Dört json eşleştiricisi, jSON filtrelerini [Produit] ve [Categorie] varlıklarına göre yapılandırmak için kullanılır. Nitekim (bkz. paragraf 4.6 ve 4.6) şu şekilde yazmıştık:

@JsonFilter("jsonFilterCategorie")
public class Categorie extends AbstractCoreEntity {

ve


@JsonFilter("jsonFilterProduit")
public class Produit extends AbstractCoreEntity {

[Categorie] varlığının jSON gösterimi, jSON [jsonFilterCategorie] filtresi tarafından kontrol edilirken,[produit] varlığının gösterimi ise jSON ve [jsonFilterProduit] filtreleri tarafından kontrol edilmektedir. Spring bağlamındaki dört jSON eşleştiricisi, bu iki filtreyi şu şekilde yapılandırır:

  • [jsonMapperShortCategorie] eşleştiricisi, kategorinin kısa bir sürümü için jSON ve [jsonFilterCategorie] filtrelerini yapılandırır: [produits] alanı, kategorinin jSON temsiline dahil edilmeyecektir;
  • [jsonMapperLongCategorie] eşleştiricisi, kategorinin uzun versiyonu için jSON ve [jsonFilterCategorie] filtrelerini yapılandırır: [produits] alanı, kategorinin jSON temsiline dahil edilecektir;
  • [jsonMapperShortProduit] eşleştiricisi, ürünün kısa sürümü için jSON ve [jsonFilterProduit] filtrelerini yapılandırır: [categorie] alanı, ürünün jSON temsiline dahil edilmeyecektir;
  • [jsonMapperLongProduit] eşleştiricisi, ürünün uzun sürümü için jSON ve [jsonFilterProduit] filtrelerini yapılandırır: [categorie] alanı, ürünün jSON temsiline dahil edilecektir;

[showDataBase] özel yöntemiyle işimiz bitti. [fillDataBase] test koduna geri dönelim:


    @Test
    public void fillDataBase() throws BeansException, JsonProcessingException {
        // temel ve sözlüklerin doldurulması
        registerCategories(fill(NB_CATEGORIES, NB_PRODUITS));
        // görüntüleme
        Object[] data = showDataBase();
        List<Categorie> categories = (List<Categorie>) data[0];
        List<Produit> produits = (List<Produit>) data[1];
        // bazı kontroller
        Assert.assertEquals(NB_CATEGORIES, categories.size());
        Assert.assertEquals(NB_PRODUITS * NB_CATEGORIES, produits.size());
        for (Categorie categorie : categories) {
            checkShortCategorie(categorie);
        }
        for (Produit produit : produits) {
            checkShortProduit(produit);
        }
        // sözlükler tükenmiş olmalıdır
        Assert.assertEquals(0, mapCategories.size());
        Assert.assertEquals(0, mapProduits.size());
}
  • 6-8. satırlar: veritabanından okunan ürün ve kategorilerin kısa sürümlerini alıyoruz;
  • 10-11. satırlar: ilk kontroller;
  • 12-14. satırlar: [showDataBase] yöntemi tarafından getirilen her kategori, aşağıdaki özel [checkShortCategorie] yöntemi ile kontrol edilir:

    private void checkShortCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = mapCategories.get(actual.getId());
        mapCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        // [produits] alanı, jPA uygulamalarıyla taşınabilir şekilde test edilemez
}
  • satır 1: [Categorie actual], veritabanından okunan kategoridir ve [mapCategories] sözlüğünde bulunan kategoriyle aynı olmalıdır;
  • 2. satır: Okunan kategorinin birincil anahtarı alınır;
  • 3. satır: Kategori sözlüğünde bu birincil anahtarla kayıtlı olan kategori alınır;
  • satır 4: başka bir okunan kategorinin aynı anahtarı kullanmadığından emin olmak için anahtar sözlükten kaldırılır;
  • 5. satır: İki kategorinin adının aynı olup olmadığı kontrol edilir;

[showDataBase] yöntemi ile getirilen ürünlerin kısa versiyonu, aşağıdaki özel [checkShortProduit] yöntemi ile doğrulanır:


    private void checkShortProduit(Produit actual) {
        Long id = actual.getId();
        Produit expected = mapProduits.get(id);
        mapProduits.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertEquals(expected.getDescription(), actual.getDescription());
        Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
        Assert.assertEquals(actual.getIdCategorie(), expected.getIdCategorie());
        // [categorie] alanı, jPA uygulamalarıyla taşınabilir bir şekilde test edilemez
}
  • 1. satır: [Produit actual], veritabanından okunan kısa üründür;
  • 2-3. satırlar: kalıcı ürün sözlüğünden, aynı birincil anahtara sahip ürün alınır;
  • 4. satır: sözlükte bulunan giriş silinir;
  • satır 5-8: iki ürünün alan değerlerinin aynı olup olmadığı kontrol edilir;

4.11.2.4. [getLongCategoriesByName3] yöntemi

Bu test şu şekildedir:


    @Test
    public void getLongCategoriesByName3() {
        // temel doldurma
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        log("getLongCategoriesByName3", 1);
        List<Categorie> categories2 = daoCategorie.getLongEntitiesByName("categorie[0]", "categorie[1]");
        Assert.assertEquals(2, categories2.size());
        registerCategories(Lists.newArrayList(categories.get(0), categories.get(1)));
        for (Categorie categorie : categories) {
            checkLongCategorie(categorie);
        }
        Assert.assertEquals(0, mapCategories.size());
}
  • 4. satır: veritabanı doldurulur ve kalıcı kategoriler ile ürünlerin listesi alınır;
  • 7. satır: [DAO] katmanındaki [daoCategorie.getLongEntitiesByName(Iterable<String> names)] yöntemini test ediyoruz. Uzun adlarıyla belirtilen iki ürünün listesini istiyoruz;
  • satır 8: [daoCategorie.getLongEntitiesByName(Iterable<String> names)] tarafından döndürülen listenin gerçekten iki öğe içerdiğini kontrol ediyoruz;
  • 9. satır: 4. satırda kalıcı hale getirilen iki öğe, kategori sözlüğüne eklenir;
  • 10-12. satırlar: Okunan iki öğenin, gerçekten de kalıcı hale getirilenler olup olmadığı kontrol edilir;
  • 13. satır: Kategori sözlüğünün boş olduğu kontrol edilir; bu, okunan tüm kategorilerin sözlükte bulunmuş olduğu ve sözlükte okunmamış değerlerin bulunmadığı anlamına gelir;
  1. satırda, [checkLongCategorie] yöntemi bir kategorinin uzun sürümünü kontrol eder:

    private void checkLongCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = mapCategories.get(actual.getId());
        mapCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertNotNull(actual.getProduits());
}
  • 6. satır, kategorinin [produits] alanının null olmadığını doğrular. Nitekim, bir kategorinin uzun formatta okunması, her zaman [produits] alanının null olmadığı bir sonuç verir. Kategoride ürün yoksa, [produits] alanı boş ancak mevcut bir listedir;

4.11.2.5. [updateDataBase1] yöntemi


@Test
    public void updateDataBase1() {
        // doldurma
        fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        log("Mise à jour du prix des produits de [categorie1]", 1);
        Categorie categorie1 = daoCategorie.getLongEntitiesByName("categorie[1]").get(0);
        List<Produit> produits = categorie1.getProduits();
        Map<Produit, Long> versions = new HashMap<Produit, Long>();
        for (Produit produit : produits) {
            produit.setPrix(1.1 * produit.getPrix());
            versions.put(produit, produit.getVersion());
        }
        daoProduit.saveEntities(produits);
        // düzeltme
        List<Produit> produitsInBd = daoCategorie.getLongEntitiesByName("categorie[1]").get(0)
                .getProduits();
        Assert.assertEquals(produits.size(), produitsInBd.size());
        // kontroller
        for (Produit produit2 : produitsInBd) {
            Produit produit = findProduitByName(produit2.getNom(), produits);
            Assert.assertEquals(produit2.getPrix(), produit.getPrix(), 1e-6);
            Assert.assertEquals(produit2.getVersion().longValue(), versions.get(produit) + 1);
        }
    }

    private Produit findProduitByName(String nom, List<Produit> produits) {
        for (Produit produit : produits) {
            if (produit.getNom().equals(nom)) {
                return produit;
            }
        }
        return null;
    }

[updateDataBase1] yöntemi, categorie[1] adlı kategorideki ürünlerin fiyatını %10 artırır ve iki şeyi kontrol eder:

  • temel fiyatın gerçekten değişip değişmediğini;
  • güncellenen ürünün sürüm numarasının 1 artırılmış olması;

Kod şu işlemleri gerçekleştirir:

  • 4. satır: veritabanının doldurulması;
  • 7. satır: veritabanından 'categorie[1]' adlı kategori alınır;
  • 8-13. satırlar: tüm ürünlerin fiyatı %10 artırılır (11. satır). Ayrıca, bir ürünü sürümüyle ilişkilendiren bir sözlük oluşturulur (9. ve 12. satırlar);
  • 14. satır: [daoProduit.saveEntities] yöntemi çağrılır. Bu yöntem, ürünlerin güncellenmesini gerçekleştirir;
  • satır 16: 'categorie[1]' adlı kategorideki ürünler veritabanından alınır;
  • satır 20-24: Bu kategorideki tüm ürünler için, fiyatın değiştirildiğinin (satır 22) ve sürümün 1 artırıldığının (satır 23) kontrolü yapılır;

4.11.2.6. [deleteProduitsByProduit1] yöntemi

[deleteProduitsByProduit1] yöntemi, [PRODUITS] tablosundaki ürünleri siler:


    @Test
    public void deleteProduitsByProduit1() {
        // doldurma
        fill(NB_CATEGORIES, NB_PRODUITS);
        // silme
        daoProduit.deleteEntitiesByEntity(daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]"));
        // doğrulama
        List<Produit> produits = daoProduit.getShortEntitiesByName("produit[0,0]", "produit[1,1]");
        Assert.assertEquals(0, produits.size());
}
  • 6. satır: iki ürün silinir;
  • satır 8-9: ürünlerin artık veritabanında bulunmadığını kontrol eder;

4.11.2.7. [getLongProduitsById3] yöntemi


    @Test
    public void getLongProduitsById3() {
        // doldurma
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        // test
        log("getLongProduitsById3", 1);
        List<Produit> produits = daoProduit.getLongEntitiesByName("produit[0,3]", "produit[1,4]");
        Assert.assertEquals(2, produits.size());
        registerProduits(Lists.newArrayList(categories.get(0).getProduits().get(3), categories.get(1).getProduits().get(4)));
        produits = daoProduit.getLongEntitiesById(produits.get(0).getId(), produits.get(1).getId());
        for (Produit produit : produits) {
            checkLongProduit(produit);
        }
        Assert.assertEquals(0, mapProduits.size());
}
  • 4. satır: veritabanı doldurulur ve kalıcı kategori listesi alınır;
  • 7. satır: Adlarıyla tanımlanan iki ürünün uzun versiyonları veritabanından alınır;
  • 9. satır: 4. satırdaki kategori listesinde bulunan [ürün[0,3], ürün[1,4]] ürünleri ürün sözlüğüne eklenir;
  • 10. satır: Bu iki ürün, birincil anahtarları aracılığıyla veritabanında aranır;
  • satır 11-14: okunan verilerin sözlükte kayıtlı verilerle aynı olup olmadığı kontrol edilir;

[checkLongProduit] özel yöntemi şu şekildedir:


    private void checkLongProduit(Produit actual) {
        Long id = actual.getId();
        Produit expected = mapProduits.get(id);
        mapProduits.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertEquals(expected.getDescription(), actual.getDescription());
        Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
        Assert.assertNotNull(actual.getCategorie());
}

4.11.2.8. Conclusion

Burada duralım. Şu anda 74 test var ve muhtemelen test edilmesi gereken bazı durumları unutmuş olduğum için daha fazlasını ekleyebiliriz. Her ne kadar kapsamlı olmasalar da, bu testler birçok hatayı tespit etmemizi sağladı; bunlar genellikle [DAO] katmanının ilk yazım aşamasında öngörülememiş sınır durumlarıydı. Her projede kapsamlı bir test aşaması şarttır.

Testi çalıştırmak için, içe aktarılan ve [spring-jdbc-generic-04.JUnitTestDao] olarak adlandırılan çalıştırma yapılandırması kullanılabilir.

4.11.3. [JUnitTestPushTheLimits] testi

[JUnitTestPushTheLimits] testi bir performans testidir. JUnit testlerinin yürütme sürelerini görüntülemesi özelliğinden yararlanarak, [DAO] katmanının performansını ölçüyoruz. Bu sonuçlar daha sonra [DAO] katmanının JPA uygulamalarıyla karşılaştırılacaktır.

4.11.3.1. Squelette

[JUnitTestPushTheLimits] sınıfının iskeleti şu şekildedir:


package spring.jdbc.tests;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

import spring.jdbc.config.AppConfig;
import spring.jdbc.dao.IDao;
import spring.jdbc.entities.Categorie;
import spring.jdbc.entities.Produit;

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

    // katman [DAO]
    @Autowired
    private IDao<Produit> daoProduit;
    @Autowired
    private IDao<Categorie> daoCategorie;

    // sabitler
    private final int NB_CATEGORIES = 2500;
    private final int NB_PRODUITS = 2;

    // yerel
    private Map<Long, Categorie> hCategories;
    private Map<Long, Produit> hProduits;

    @Before
    public void clean() {
        // tabloyu boşaltıyoruz [CATEGORIES]
        daoCategorie.deleteAllEntities();
        // sözlükler
        hCategories = new HashMap<Long, Categorie>();
        hProduits = new HashMap<Long, Produit>();
    }

    private List<Categorie> fill(int nbCategories, int nbProduits) {
        // tablolar dolduruluyor
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < nbCategories; i++) {
            Categorie categorie = new Categorie(null, 0L, String.format("categorie[%d]", i), null);
            for (int j = 0; j < nbProduits; j++) {
                Produit produit = new Produit(null, 0L, String.format("produit[%d,%d]", i, j), 0L,
                        100 * (1 + (double) (i * 10 + j) / 100), String.format("desc[%d,%d]", i, j), null);
                categorie.addProduit(produit);
            }
            categories.add(categorie);
        }
        // kategori ekleniyor - zincirleme olarak ürünler de eklenecek
        categories = daoCategorie.saveEntities(categories);
        // sözlükler
        for (Categorie categorie : categories) {
            hCategories.put(categorie.getId(), categorie);
            for (Produit produit : categorie.getProduits()) {
                hProduits.put(produit.getId(), produit);
            }
        }
        // sonuç
        return categories;
    }

....

    // -------------------- özel yöntemler
    private void checkLongProduit(Produit actual) {
        Long id = actual.getId();
        Produit expected = hProduits.get(id);
        hProduits.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertEquals(expected.getDescription(), actual.getDescription());
        Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
        Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
        Assert.assertNotNull(actual.getCategorie());
    }

    private void checkShortProduit(Produit actual) {
        Long id = actual.getId();
        Produit expected = hProduits.get(id);
        hProduits.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertEquals(expected.getDescription(), actual.getDescription());
        Assert.assertEquals(expected.getPrix(), actual.getPrix(), 1e-6);
        Assert.assertEquals(expected.getIdCategorie(), actual.getIdCategorie());
        boolean erreur = false;
        try {
            actual.getCategorie().getNom();
        } catch (Exception e) {
            erreur = true;
        }
        Assert.assertTrue(erreur);
    }

    private void checkShortCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = hCategories.get(actual.getId());
        hCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        boolean erreur = false;
        try {
            actual.getProduits().size();
        } catch (Exception e) {
            erreur = true;
        }
        Assert.assertTrue(erreur);
    }

    private void checkLongCategorie(Categorie actual) {
        Long id = actual.getId();
        Categorie expected = hCategories.get(actual.getId());
        hCategories.remove(id);
        Assert.assertEquals(expected.getNom(), actual.getNom());
        Assert.assertNotNull(actual.getProduits());
    }

}

Burada, [JUnitTestDao] sınıfının iskeletini görüyoruz. Bu yöntemlerin hepsiyle daha önce karşılaşmıştık. Test, her biri 2 ürün içeren 2500 kategoriden oluşan bir veritabanıyla çalışır (satır 32-33). Dolayısıyla [CATEGORIES] tablosu 2500 satır, [PRODUITS] tablosu ise 5000 satır içerecektir. Daha fazla satır ekleyebilirdik, ancak test zaten yaklaşık bir dakika sürüyor. Bu nedenle, testin bitmesini bekleyen kullanıcı için kabul edilebilir değerler seçtik.

Toplamda 18 test bulunmaktadır. Bu testler, [1] yürütme yapılandırmasıyla gerçekleştirilmektedir. Yürütme süreleri [2]'te gösterilmektedir:

4.11.3.2. doNothing [0,114]

[doNothing] yöntemi hiçbir işlem yapmaz. Bu yöntem, her testten önce çalıştırılan ve veritabanını boşaltan [clean] yönteminin süresini ölçmeye yarar. Yukarıda görüldüğü gibi, bu işlemin süresi diğerlerine kıyasla çok azdır.


    @Test
    public void doNothing() {
        // temizle
}

4.11.3.3. perf01 [4,179]

[perf01] testi, veritabanının doldurulma süresini ölçmeye yarar:


    @Test
    public void perf01() {
        // ekle
        fill(NB_CATEGORIES, NB_PRODUITS);
}

4.11.3.4. perf02 [7,624]

[perf02] yöntemi:

  • veritabanını doldurur;
  • ardından tüm kategorilerin adını ve tüm ürünlerin fiyatını değiştirir.

    @Test
    public void perf02() {
        // güncelleme
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        for (Categorie categorie : categories) {
            categorie.setNom(categorie.getNom() + "*");
            for (Produit produit : categorie.getProduits()) {
                produit.setPrix(produit.getPrix() * 1.1);
            }
        }
        // güncelleme
        daoCategorie.saveEntities(categories);
}

4.11.3.5. perf03[3,911]

[perf03] yöntemi:

  • veritabanını doldurur
  • ardından tüm kategorileri tek tek siler. [CATEGORIES] tablosu ile [PRODUITS] tablosu arasında var olan zincirleme ilişki nedeniyle ürünler de silinir.

Burada şaşırtıcı olan, bu işlemin, daha az işlem yapan [perf01] ve [4,179 s] işlemlerinden daha kısa sürmesidir.


    @Test
    public void perf03() {
        // kategorileri ve bunlara bağlı ürünleri sil
        daoCategorie.deleteEntitiesByEntity(fill(NB_CATEGORIES, NB_PRODUITS));
}

[daoCategorie.deleteEntitiesByEntity] yönteminin koduna bakıldığında, 2500 parametreli (kategori sayısı) bir [PreparedStatement]'in yürütüleceği görülür. İşte burada [maxPreparedStatementParameters] bean devreye girer ve SQL komutunu, kullanılan belirli SGBD tarafından desteklenebilecek sayıda parametreye sahip birkaç [PreparedStatement]'e böler.

4.11.3.6. perf04[2,426]

[perf04] yöntemi:

  • veritabanını doldurur;
  • ardından tüm kategorilerin uzun versiyonunu ister;

    @Test
    public void perf04() {
        // seç
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        List<Long> ids = new ArrayList<Long>();
        for (Categorie categorie : categories) {
            ids.add(categorie.getId());
        }
        daoCategorie.getLongEntitiesById(ids);
}

4.11.3.7. perf05 [3,507]

[perf05] yöntemi:

  • veritabanını doldurur;
  • ardından birincil anahtarları aracılığıyla 5000 ürünü siler (bu durumda potansiyel olarak 5000 parametre içeren bir [PreparedStatement] olabilir);
  • daha sonra ürün tablosunun boş olduğunu doğrular;

    @Test
    public void perf05() {
        // ürünleri sil
        List<Categorie> categories = fill(NB_CATEGORIES, NB_PRODUITS);
        List<Long> ids = new ArrayList<Long>();
        for (Categorie categorie : categories) {
            for (Produit p : categorie.getProduits()) {
                ids.add(p.getId());
            }
        }
        daoProduit.deleteEntitiesById(ids);
        // doğrulama
        List<Produit> produits = daoProduit.getAllShortEntities();
        Assert.assertEquals(0, produits.size());
}

4.11.3.8. Résultats

Farklı testleri tek tek sunmaya devam etmeyeceğiz. Sadece ne yaptıklarını ve sürelerini belirteceğiz. Bu süreler ancak birbirleriyle karşılaştırıldığında anlamlıdır. Değerleri, kullanılan test ortamına (donanım ve yazılım yapılandırması) bağlıdır. Ancak aynı ortamda elde edildiklerinde karşılaştırılabilirler.

Toplam test süresi: 59,995 saniye

test
rol
durée (s)
perf01
veritabanını 2500 kategori ve 5000 ürünle doldurur
4,179
perf02
veritabanını doldurur ve ardından değiştirir
7,624
perf03
veritabanını doldurur, ardından tüm kategorileri ve bunlara ait ürünleri siler
3,911
perf04
veritabanını doldurur ve tüm kategorilerin uzun versiyonlarını ister
2,426
perf05
veritabanını doldurur ve 5000 ürünü birincil anahtarları aracılığıyla tek tek siler
3,507
perf06
veritabanını doldurur ve 5000 ürünü adlarına göre tek tek siler
3,947
perf07
veritabanını doldurur ve 5000 ürünü referans numaralarına göre tek tek siler
3,633
perf08
veritabanını doldurur ve tüm ürünlerin adlarına göre kısa versiyonlarını ister
4,054
perf09
veritabanını doldurur ve tüm ürünlerin adları üzerinden uzun versiyonlarını ister
2,643
perf10
veritabanını doldurur ve tüm ürünlerin birincil anahtarları aracılığıyla kısa sürümlerini ister
3,463
perf11
veritabanını doldurur ve tüm ürünlerin birincil anahtarları aracılığıyla uzun sürümlerini ister
2,777
perf12
veritabanını doldurur, ardından tüm kategorileri (dolayısıyla ilgili ürünleri) adlarına göre tek tek siler
3,806
perf13
veritabanını doldurur, ardından tüm kategorileri (dolayısıyla ilgili ürünleri) referans numaralarına göre tek tek siler
2,828
perf14
veritabanını doldurur ve tüm kategorilerin adlarına göre kısa versiyonlarını ister
2,731
perf15
veritabanını doldurur ve tüm kategorilerin adları aracılığıyla uzun sürümlerini ister
2,603
perf16
veritabanını doldurur ve tüm kategorilerin birincil anahtarları aracılığıyla kısa versiyonlarını ister
2,462
perf17
veritabanını doldurur ve tüm kategorilerin birincil anahtarları aracılığıyla uzun versiyonlarını ister
3,287

Bu sonuçlar bazen şaşırtıcı olabilir:

  • ürünlerin uzun versiyonunu (perf09) elde etmek, kısa versiyonunu (perf08) elde etmekten daha hızlı oldu; oysa uzun versiyon, iki tablo arasında bir birleştirme işlemi gerektiriyordu;
  • ilk doldurma işleminin süresi (perf01), ardından gelecek diğer tüm doldurma işlemlerinin sürelerini açıkça aşıyor;
  • ürünlerin adları üzerinden kısa sürümünü istemek (perf08), birincil anahtarlar üzerinden istemekten (perf10) daha uzun sürüyor. Bu oldukça mantıklı görünüyor. Ancak uzun sürümler için durum tam tersi (perf09, perf11);

Bu nedenle bu sonuçlar üzerinde fazla durmayacağız. Ancak bu sonuçlar, [Spring JDBC] çözümünü şu çözümlerle karşılaştırmak için bize faydalı olacaktır:

  • [Spring JDBC] ve diğer beş çözüm olan SGBD;
  • [Spring JPA];