Skip to content

3. JPA çok katmanlı bir mimaride

API JPA'i incelemek için aşağıdaki test mimarisini kullandık:

Test programlarımız, JPA katmanını doğrudan sorgulayan konsol uygulamalarıydı. Bu süreçte, JPA katmanının temel yöntemlerini keşfettik. "Java SE" (Standard Edition) olarak adlandırılan bir ortamda çalışıyorduk. JPA, hem Java SE hem de Java EE5 (Enterprise Edition) ortamlarında çalışır.

Artık hem ilişkisel/nesne köprüsünün yapılandırmasını hem de JPA katmanındaki yöntemlerin kullanımını bir ölçüde kavradığımıza göre, daha klasik bir çok katmanlı mimariye geri dönüyoruz:

[JPA] katmanına, [metier] ve [dao]'ten oluşan 2 katmanlı bir mimari aracılığıyla erişilecektir. Bu katmanları birbirine bağlamak için Spring [7] çerçeve yapısı ve ardından JBoss ile [8]'in EJB3 konteyneri kullanılacaktır.

Yukarıda, JPA'in SE ve EE5 ortamlarında kullanılabilir olduğunu belirtmiştik. EE5 Java ortamı, kalıcı verilere erişim alanında bağlantı havuzları, işlem yöneticileri gibi birçok hizmet sunar. Bir geliştirici için bu hizmetlerden yararlanmak faydalı olabilir. EE5 Java ortamı henüz yaygın değildir (Mayıs 2007). Şu anda Sun Application Server 9.x (Glassfish) uygulama sunucusunda bulunur. Bir uygulama sunucusu, esasen bir web uygulaması sunucusudur. Swing türü bağımsız bir grafik uygulama geliştiriliyorsa, EE ortamından ve bu ortamın sunduğu hizmetlerden yararlanmak mümkün değildir. Bu bir sorundur. Artık "bağımsız" EE ortamları ve c.a.d ortamları görülmeye başlanmıştır. Uygulama sunucusu dışında kullanılabilen ortamlar. Bu belgede kullanacağımız JBoss ve EJB3 de bu türden ortamlardır.

Bir EE5 ortamında, katmanlar EJB (Enterprise Java Bean) adı verilen nesnelerle uygulanır. EE'in önceki sürümlerinde, EJB'lerin (EJB, 2.x) uygulanması ve test edilmesi zor olduğu ve bazen düşük performans gösterdiği bilinmektedir. EJB2.x "entity" ve EJB2.x "session" arasında bir ayrım yapılır. Kısaca, bir EJB2.x "entity", bir veritabanı tablosundaki satırın görüntüsüdür; EJB2.x "session" ise çok katmanlı bir mimaride [metier] katmanlarını uygulamak için kullanılan bir nesnedir, [dao] katmanlarını uygulamak için kullanılan bir nesnedir. EJB ile uygulanan katmanlara yöneltilen başlıca eleştirilerden biri, bunların yalnızca EJB kapsayıcıları içinde kullanılabilmesidir; bu kapsayıcılar, EE ortamı tarafından sağlanan bir hizmettir. Bu durum, birim testlerini zorlaştırmaktadır. Dolayısıyla yukarıdaki şemada, EJB ile oluşturulan [metier] ve [dao] katmanlarının birim testleri, bir uygulama sunucusunun kurulmasını gerektirecektir; bu oldukça zahmetli bir işlemdir ve geliştiricinin sık sık test yapmasını pek teşvik etmez.

Spring çerçevesi, EJB2'lerin karmaşıklığına bir tepki olarak ortaya çıkmıştır. Spring, bir SE ortamında, genellikle EE ortamları tarafından sağlanan hizmetlerin önemli bir kısmını sunar. Dolayısıyla, burada ilgilendiğimiz "Veri Kalıcılığı" bölümünde Spring, uygulamaların ihtiyaç duyduğu bağlantı havuzlarını ve işlem yöneticilerini sağlar. Spring'in ortaya çıkışı, bir anda uygulanması çok daha kolay hale gelen birim test kültürünü teşvik etmiştir. Spring, bir uygulamanın katmanlarının klasik Java nesneleri (POJO, Plain Old/Ordinary Java Object) ile uygulanmasına olanak tanıyarak, bunların başka bir bağlamda yeniden kullanılmasını sağlar. Son olarak, Hibernate, Ibatis gibi kalıcılık araçları başta olmak üzere birçok üçüncü taraf aracını oldukça şeffaf bir şekilde entegre eder.

Java EE5, önceki EE spesifikasyonundaki eksiklikleri gidermek üzere tasarlanmıştır. EJB ve 2.x, EJB3 haline gelmiştir. Bunlar, bir EJB3 konteyneri içindeyken onları özel nesneler haline getiren açıklamalarla etiketlenmiş POJOs'lerdir. Bu kapsayıcı içinde, EJB3, kapsayıcının hizmetlerinden (bağlantı havuzu, işlem yöneticisi vb.) yararlanabilecektir. EJB3 konteynerinin dışında, EJB3 normal bir Java nesnesi haline gelir. EJB anotasyonları göz ardı edilir.

Yukarıda, Spring ve JBoss ile EJB3'i, çok katmanlı mimarimizin olası bir altyapısı (çerçeve) olarak gösterdik. İhtiyacımız olan hizmetleri (bağlantı havuzu ve işlem yöneticisi) sağlayacak olan bu altyapıdır.

  • Spring ile katmanlar, POJOs'ler kullanılarak uygulanacaktır. Bu POJOs'ler, Spring hizmetlerine (bağlantı havuzu, işlem yöneticisi) bağımlılık enjeksiyonu yoluyla erişecektir: bu POJOs'ler oluşturulurken, Spring onlara ihtiyaç duyacakları hizmetlere ilişkin referansları enjekte eder.
  • JBoss EJB3, bir uygulama sunucusunun dışında çalışabilen bir EJB konteyneridir. Çalışma prensibi (geliştirici açısından) Spring için açıklananla benzerdir. Aralarında çok az fark vardır.

Bu belgeyi, basit ancak yine de temsil edici nitelikteki üç katmanlı bir web uygulaması örneğiyle sonlandıracağız:

3.1. Örnek 1: Spring / JPA ile Kişi varlığı

2.1. paragrafta incelenen Personne varlığını alıyoruz ve katmanların entegrasyonunun Spring ile yapıldığı, kalıcılık katmanının ise Hibernate tarafından uygulandığı çok katmanlı bir mimariye entegre ediyoruz.

Burada okuyucunun Spring hakkında temel bilgilere sahip olduğu varsayılmaktadır. Aksi takdirde, Spring’in temelini oluşturan bağımlılık enjeksiyonu kavramını açıklayan aşağıdaki belgeyi okuyabilirsiniz:

[ref3]: Spring IOC (Kontrolün Tersine Çevrilmesi) [http://tahe.developpez.com/java/springioc].

3.1.1. Eclipse / Spring / Hibernate projesi

Eclipse projesi şöyledir:

  • [1]'te: Eclipse projesi. Bu proje, [6]'te, [5] numaralı öğreticinin örnekleri arasında bulunabilir. Bu projeyi içe aktaracağız.
  • [2] dosyasında: paketler halinde sunulan katmanların Java kodları:
    • [entites]: JPA varlık paketini
    • [dao]: Veri erişim katmanı - JPA katmanına dayanır
    • [service]: iş mantığından çok hizmet odaklı bir katman. Burada konteynerlerin işlem hizmeti kullanılacaktır.
    • [tests]: Test programlarını bir araya getirir.
  • [3]: [jpa-spring] kütüphanesi, Spring için gerekli jar dosyalarını bir araya getirir (ayrıca bkz. [7] ve [8]).
  • [4]: [conf] klasörü, bu öğreticide kullanılan her bir SGBD için Spring yapılandırma dosyalarını bir araya getirir.

3.1.2. t JPA

Burada yönetilen tek bir varlık vardır: 2.1. paragrafta incelenen Personne varlığı. Bu varlığın yapılandırmasını aşağıda tekrar hatırlatıyoruz:


package entites;

...
@Entity
@Table(name="jpa01_hb_personne")
public class Personne {

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // oluşturucular
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
...
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // alıcı ve ayarlayıcılar
...
}

3.1.3. [dao] katmanı

[dao] katmanı, aşağıdaki IDao arayüzünü sunar:


package dao;

import java.util.List;

import entites.Personne;

public interface IDao {
    // kimlik numarasıyla bir kişiyi bulma
    public Personne getOne(Integer id);

    // tüm kişileri alma
    public List<Personne> getAll();

    // bir kişiyi kaydetme
    public Personne saveOne(Personne personne);

    // bir kişiyi güncellemek
    public Personne updateOne(Personne personne);

    // kimlik numarasıyla bir kişiyi silme
    public void deleteOne(Integer id);

    // adı belirli bir şablona uyan kişileri al
    public List<Personne> getAllLike(String modele);

}

Bu arayüzün [Dao] uygulaması şöyledir:


package dao;

import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

import entites.Personne;

public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // kimlik numarasıyla bir kişiyi sil
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    @SuppressWarnings("unchecked")
    // tüm kişileri almak
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    @SuppressWarnings("unchecked")
    // adları belirli bir şablona uyan kişileri almak
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // kimlik numarasıyla bir kişiyi bul
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // bir kişiyi kaydetmek
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // bir kişiyi güncelle
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Öncelikle, [Dao] uygulamasının basitliğine dikkat çekmek gerekir. Bu basitlik, veri erişim işinin büyük kısmını üstlenen JPA katmanının kullanılmasına bağlıdır.
  • 10. satır: [Dao] sınıfı, [IDao] arayüzünü uygular
  • 13. satır: JPA kalıcılık bağlamını işlemek için kullanılacak [EntityManager] türündeki nesne. Dil kullanımındaki bir yanlışlık nedeniyle, bunu bazen kalıcılık bağlamının kendisiyle karıştıracağız. Kalıcılık bağlamı, Personne varlıklarını içerecektir.
  • 12. satır: Kodun hiçbir yerinde [EntityManager em] alanı başlatılmamıştır. Bu alan, uygulamanın başlatılması sırasında Spring tarafından başlatılacaktır. 12. satırdaki JPA @PersistenceContext anotasyonu, Spring'den em'e bir kalıcılık bağlam yöneticisi enjekte etmesini ister.
  • 26-28. satırlar: Tüm kişilerin listesi, JPQL sorgusu ile alınır.
  • 32-35. satırlar: Adı belirli bir modele uyan tüm kişilerin listesi, JPQL sorgusu ile alınır.
  • satır 38-40: Belirli bir kimliğe sahip kişi, API JPA'in find yöntemi ile alınır. Kişi mevcut değilse, bir null işaretçisi döndürür.
  • satır 43-46: Bir kişi, API ve JPA'in persist yöntemiyle kalıcı hale getirilir. Yöntem, kişiyi kalıcı hale getirir.
  • satır 49-51: Bir kişinin güncellenmesi, API JPA'in merge yöntemi ile gerçekleştirilir. Bu yöntem, güncellenen kişinin daha önce ayrılmış olması durumunda anlamlıdır. Yöntem, bu şekilde oluşturulan kişiyi kalıcı hale getirir.
  • 16-22. satırlar: Parametre olarak verilen kimlik numarasına sahip kişinin silinmesi iki aşamada gerçekleşir:
    • 17. satır: Kişi, kalıcı veritabanı bağlamında aranır
    • 18-20. satırlar: Kişi bulunamazsa, hata kodu 2 ile bir istisna atılır
    • 21. satır: Kişi bulunursa, API JPA'in remove yöntemi kullanılarak kalıcılık bağlamından kaldırılır.
  • Şu anda görünmeyen şey, her yöntemin [service] katmanı tarafından başlatılan bir işlem içinde yürütüleceğidir.

Uygulamanın [DaoException] adlı kendine özgü bir istisna türü vardır:


package dao;

@SuppressWarnings("serial")
public class DaoException extends RuntimeException {

    // hata kodu
    private int code;

    public DaoException(int code) {
        super();
        this.code = code;
    }

    public DaoException(String message, int code) {
        super(message);
        this.code = code;
    }

    public DaoException(Throwable cause, int code) {
        super(cause);
        this.code = code;
    }

    public DaoException(String message, Throwable cause, int code) {
        super(message, cause);
        this.code = code;
    }

    // getter ve setter

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

}
  • 4. satır: [DaoException], [RuntimeException]'ten türetilmiştir. Dolayısıyla bu, derleyicinin bizden try/catch ile yönetmemizi veya yöntem imzalarına eklememizi zorunlu kılmadığı bir istisna türüdür. Bu nedenle, [DaoException], [IDao] arayüzünün [deleteOne] yönteminin imzasında yer almamaktadır. Bu, söz konusu arayüzün, aynı zamanda [RuntimeException] sınıfından türetilmiş olması koşuluyla, başka türde istisnalar atan bir sınıf tarafından uygulanmasına olanak tanır.
  • Oluşabilecek hataları ayırt etmek için 7. satırdaki hata kodu kullanılır. 14., 19. ve 24. satırlardaki üç yapıcı, üst sınıf [RuntimeException]’e ait olanlardır ve bunlara bir parametre eklenmiştir: istisnaya atanacak hata kodu.

3.1.4. [metier / service] katmanı

[service] katmanı, aşağıdaki [IService] arayüzünü sunar:


package service;

import java.util.List;

import entites.Personne;

public interface IService {
    // kimlik numarasıyla bir kişiyi almak
    public Personne getOne(Integer id);

    // tüm kişileri almak
    public List<Personne> getAll();

    // bir kişiyi kaydetmek
    public Personne saveOne(Personne personne);

    // bir kişiyi güncellemek
    public Personne updateOne(Personne personne);

    // bir kişiyi kimlik numarasıyla silmek
    public void deleteOne(Integer id);

    // adı belirli bir şablona uyan kişileri almak
    public List<Personne> getAllLike(String modele);

    // birden fazla kişiyi aynı anda sil
    public void deleteArray(Personne[] personnes);

    // birden fazla kişiyi aynı anda kaydetmek
    public Personne[] saveArray(Personne[] personnes);

    // birden fazla kişiyi aynı anda güncelleme
    public Personne[] updateArray(Personne[] personnes);

}
  • 8-24. satırlar: [IService] arayüzü, [IDao] arayüzünün yöntemlerini içerir
  • 27. satır: [deleteArray] yöntemi, bir işlem içindeki bir grup kişiyi silmeye olanak tanır: ya tüm kişiler silinir ya da hiçbiri silinmez.
  • 30. ve 33. satırlar: Bir işlem içindeki bir grup kişiyi kaydetmek (30. satır) veya güncellemek (33. satır) için [deleteArray]'e benzer yöntemler.

[IService] arayüzünün [Service] uygulaması şu şekildedir:


package service;

...

// Sınıfın tüm yöntemleri bir işlem içinde gerçekleştirilir
@Transactional
public class Service implements IService {

    // [dao] katmanı
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

    // birden fazla kişiyi aynı anda silme
    public void deleteArray(Personne[] personnes) {
        for (Personne p : personnes) {
            dao.deleteOne(p.getId());
        }
    }

    // bir kişiyi kimlik numarasıyla silme
    public void deleteOne(Integer id) {
        dao.deleteOne(id);
    }

    // tüm kişileri almak
    public List<Personne> getAll() {
        return dao.getAll();
    }

    // adı belirli bir şablona uyan kişileri almak
    public List<Personne> getAllLike(String modele) {
        return dao.getAllLike(modele);
    }

    // kimlik numarasıyla bir kişiyi almak
    public Personne getOne(Integer id) {
        return dao.getOne(id);
    }

    // birden fazla kişiyi aynı anda kaydetmek
    public Personne[] saveArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.saveOne(personnes[i]);
        }
        return personnes2;
    }

    // bir kişiyi kaydet
    public Personne saveOne(Personne personne) {
        return dao.saveOne(personne);
    }

    // birden fazla kişiyi aynı anda güncelle
    public Personne[] updateArray(Personne[] personnes) {
        Personne[] personnes2 = new Personne[personnes.length];
        for (int i = 0; i < personnes.length; i++) {
            personnes2[i] = dao.updateOne(personnes[i]);
        }
        return personnes2;
    }

    // bir kişiyi güncelle
    public Personne updateOne(Personne personne) {
        return dao.updateOne(personne);
    }

}
  • 6. satır: Spring @Transactional anotasyonu, sınıfın tüm yöntemlerinin bir işlem kapsamında yürütülmesi gerektiğini belirtir. Yöntemin yürütülmesine başlamadan önce bir işlem başlatılır ve yürütme tamamlandıktan sonra kapatılır. Yöntemin yürütülmesi sırasında [RuntimeException] türünde veya bunun türevlerinden bir istisna meydana gelirse, otomatik bir geri alma işlemi tüm işlemi iptal eder; aksi takdirde otomatik bir onaylama işlemi işlemi geçerli kılar. Java kodunun işlemlerle ilgilenmesine gerek olmadığı unutulmamalıdır. İşlemler Spring tarafından yönetilir.
  • 10. satır: [dao] katmanına bir referans. Daha sonra göreceğimiz üzere, bu referans uygulamanın başlatılması sırasında Spring tarafından başlatılır.
  • [Service] yöntemleri, yalnızca 10. satırdaki [IDao dao] arayüzünün yöntemlerini çağırır. Kodu incelemeyi okuyucuya bırakıyoruz. Özel bir zorluk yoktur.
  • Daha önce, [Service]'in her yönteminin bir işlem içinde yürütüldüğünü belirtmiştik. Bu işlem, yöntemin yürütme iş parçacığına bağlıdır. Bu iş parçacığında, [dao] katmanındaki yöntemler yürütülür. Bu yöntemler, yürütme iş parçacığının işlemine otomatik olarak eklenecektir. Örneğin, [deleteArray] yöntemi (21. satır), [dao] katmanındaki [deleteOne] yöntemini N kez çalıştırır. Bu N yürütme, [deleteArray] yönteminin yürütme iş parçacığı içinde, yani aynı işlem içinde gerçekleşecektir. Dolayısıyla, her şey yolunda giderse tümü onaylanacak (commit), ya da [dao] katmanındaki [deleteOne] yönteminin N yürütmesinden birinde bir istisna meydana gelirse tümü geri alınacaktır (rollback).

3.1.5. Katmanların yapılandırılması

[service], [dao] ve [JPA] katmanlarının yapılandırması, yukarıdaki iki dosya tarafından sağlanır: [META-INF/persistence.xml] ve [spring-config.xml]. Bu iki dosya, uygulamanın classpath klasöründe bulunmalıdır; bu nedenle Eclipse projesinin [src] klasöründedirler. [spring-config.xml] dosyasının adı serbesttir.

persistence.xml


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />
</persistence>
  • 4. satır: Dosya, jpa adlı bir kalıcılık birimini tanımlamaktadır; bu birim, EJB3 konteyneri tarafından sağlanmayan “yerel” işlemler olan c.a.d’i kullanır. Bu işlemler Spring tarafından oluşturulur ve yönetilir; yapılandırmaları ise [spring-config.xml] dosyasında bulunur.

spring-config.xml


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- uygulama katmanları -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <!-- kalıcılık katmanı JPA -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean
                class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform"
                    value="org.hibernate.dialect.MySQL5InnoDBDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean
                class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- veri kaynağı DBCP -->
    <bean id="dataSource"
        class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- işlem yöneticisi -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory"
            ref="entityManagerFactory" />
    </bean>

    <!-- istisna çevirisi -->
    <bean
        class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- kalıcılık açıklamaları -->
    <bean
        class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>
  • 2-5. satırlar: yapılandırma dosyasının kök <beans> etiketidir. Bu etiketin çeşitli öznitelikleri hakkında açıklama yapmayacağız. Bu özniteliklerden birinde hata yapılması, bazen anlaşılması zor hatalara yol açabileceğinden, kopyala/yapıştır işlemini dikkatli bir şekilde gerçekleştirin.
  • 8. satır: "dao" bean'i, [dao.Dao] sınıfının bir örneğine yapılan bir referanstır. Tek bir örnek oluşturulacak (singleton) ve uygulamanın [dao] katmanını uygulayacaktır.
  • 9-11. satırlar: [service] katmanının örneklenmesi. "service" bean'i, [service.Service] sınıfının bir örneğine bir referanstır. Tek bir örnek oluşturulacak (singleton) ve uygulamanın [service] katmanını uygulayacaktır. [service.Service] sınıfının [IDao dao] adlı özel bir alanı olduğunu görmüştük. Bu alan, 8. satırda tanımlanan "dao" bean'i tarafından 10. satırda başlatılır.
  • Sonuç olarak, 8-11. satırlar [dao] ve [service] katmanlarını yapılandırmıştır. Bunların ne zaman ve nasıl örnekleneceğini daha sonra göreceğiz.
  • 35-42. satırlar: Bir veri kaynağı tanımlanmıştır. Hibernate ile JPA varlıklarını incelerken veri kaynağı kavramıyla daha önce karşılaşmıştık:

Yukarıda, "bağlantı havuzu" olarak adlandırılan [c3p0], "veri kaynağı" olarak da adlandırılabilirdi. Bir veri kaynağı, "bağlantı havuzu" hizmetini sağlar. Spring ile [c3p0] dışında bir veri kaynağı kullanacağız. Bu, Apache Commons projesine ait DBCP ve [http://jakarta.apache.org/commons/dbcp/]'ten gelen [DBCP]'tir. [DBCP] arşivleri, [jpa-spring] kullanıcı kütüphanesine yerleştirilmiştir:

 
  • 38-41. satırlar: Hedef veritabanıyla bağlantı kurmak için veri kaynağının kullanılan JDBC sürücüsünü (38. satır), veritabanının URL'sini (39. satır), bağlantı kullanıcısını ve şifresini (40-41. satırlar) bilmesi gerekir.
  • 14-32. satırlar: JPA katmanını yapılandırır
  • 14-15. satırlar: Kalıcılık bağlamlarını yönetmek üzere [EntityManager] türünde nesneler oluşturabilen [EntityManagerFactory] türünde bir bean tanımlar. Oluşturulan [LocalContainerEntityManagerFactoryBean] sınıfı Spring tarafından sağlanır. Bu sınıfın oluşturulabilmesi için 16-31. satırlarda tanımlanan bir dizi parametreye ihtiyaç duyar.
  • 16. satır: SGBD'e bağlantı kurmak için kullanılacak veri kaynağı. Bu, 35-42. satırlarda tanımlanan [DBCP] kaynağıdır.
  • 17-27. satırlar: Kullanılacak JPA uygulaması
  • 18-26. satırlar: Hibernate'i (19. satır) kullanılacak JPA uygulaması olarak tanımlar
  • 23-24. satırlar: Hibernate'in hedef SGBD ile birlikte kullanması gereken SQL lehçesi; burada hedef MySQL5'tir.
  • 25. satır: Uygulamanın başlatılması sırasında veritabanının oluşturulmasını (drop ve create) talep eder.
  • 28-31. satırlar: Bir "sınıf yükleyici" tanımlar. JPA katmanındaki EntityManagerFactory tarafından kullanılan bu bean'in rolünü net bir şekilde açıklayamam. Her neyse, bu, uygulamayı çalıştıran JVM'e, içeriği uygulamanın başlatılması sırasında sınıfların yüklenmesini yönetecek bir arşivin adının aktarılmasını gerektirir. Burada bu arşiv, [jpa-spring] kullanıcı kütüphanesinde bulunan [spring-agent.jar]'tir (yukarıya bakınız). Hibernate'in bu ajana ihtiyacı olmadığını, ancak Toplink'in buna ihtiyaç duyduğunu göreceğiz.
  • 45-50. satırlar: kullanılacak işlem yöneticisini tanımlar
  • 45. satır: İşlemlerin Java anotasyonlarıyla yönetildiğini belirtir (bunlar spring-config.xml içinde de tanımlanabilirdi). Bu, özellikle [Service] sınıfında (6. satır) görülen @Transactional anotasyonudur.
  • 46-50. satırlar: işlem yöneticisi
  • 47. satır: İşlem yöneticisi, Spring tarafından sağlanan bir sınıftır
  • 48-49. satırlar: Spring işlem yöneticisi, JPA katmanını yöneten EntityManagerFactory sınıfını bilmelidir. Bu sınıf, 14-32. satırlarda tanımlanmıştır.
  • satır 57-58: Java kodunda bulunan Spring kalıcılık anotasyonlarını yöneten sınıfı tanımlar; örneğin, [dao.Dao] sınıfındaki @PersistenceContext anotasyonu (satır 12).
  • 53-54. satırlar: Özellikle @Repository anotasyonunu yöneten Spring sınıfını tanımlar; bu anotasyon, anotasyonu yapılmış bir sınıfın, SGBD Jdbc sürücüsünün yerel istisnalarının [DataAccessException] türündeki genel Spring istisnalarına dönüştürülmesine uygun hale getirir. Bu dönüştürme, yerel Jdbc istisnasını çeşitli alt sınıflara sahip [DataAccessException] türünde kapsüller:

Image

Bu çeviri, istemci programının hedef SGBD ne olursa olsun istisnaları genel bir şekilde yönetmesine olanak tanır. Java kodumuzda @Repository anotasyonunu kullanmadık. Dolayısıyla 53-54. satırlar gereksizdir. Bunları sadece bilgilendirme amacıyla bıraktık.

Spring yapılandırma dosyası ile işimiz bitti. Bu dosya karmaşıktır ve pek çok husus belirsizliğini korumaktadır. Dosya, Spring belgelerinden alınmıştır. Neyse ki, çeşitli durumlara uyarlanması genellikle iki değişiklikle sınırlıdır:

  • hedef veritabanı değişikliği: 38-41. satırlar. Bir Oracle örneği vereceğiz.
  • JPA uygulamasının ayarlanması: 14-32. satırlar. Toplink örneği vereceğiz.

3.1.6. [InitDB] istemci programı

Daha önce açıklanan mimarinin ilk istemcisinin yazımına geçiyoruz:

[InitDB] kodunun içeriği şöyledir:


package tests;

...
public class InitDB {

    // hizmet katmanı
    private static IService service;

    // oluşturucu
    public static void main(String[] args) throws ParseException {
        // uygulama yapılandırması
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // hizmet katmanı
        service = (IService) ctx.getBean("service");
        // veritabanını boşaltma
        clean();
        // veritabanını doldurma
        fill();
        // gözle kontrol ediliyor
        dumpPersonnes();
    }

    // tablo içeriğinin görüntülenmesi
    private static void dumpPersonnes() {
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // tablo doldurma
    public static void fill() throws ParseException {
        // kullanıcılar oluşturuluyor
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // kaydedilir
        service.saveArray(new Personne[] { p1, p2 });
    }

    // tablodaki öğeleri silme
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • 12. satır: [spring-config.xml] dosyası, dosyanın bellek görüntüsü olan bir [ApplicationContext ctx] nesnesi oluşturmak için kullanılır. Bu işlem sırasında [spring-config.xml] içinde tanımlanan bean'ler örneklenir.
  • 14. satır: ctx uygulama bağlamından [service] katmanına ilişkin bir referans istenir. Bu katmanın "service" adlı bir bean ile temsil edildiği bilinmektedir.
  • 16. satır: Veritabanı, clean yöntemi kullanılarak boşaltılır. 41-45. satırlar:
    • 42-44. satırlar: Kalıcılık bağlamından tüm kişilerin listesi istenir ve bunlar tek tek silinmek üzere bir döngü içinde işlenir. Hatırlanacağı üzere, [spring-config.xml] yöntemi, veritabanının uygulamanın başlatılması sırasında oluşturulmasını gerektirir. Dolayısıyla bizim durumumuzda, boş bir veritabanından başladığımız için clean yönteminin çağrılmasına gerek yoktur.
  • 18. satır: fill yöntemi veritabanını doldurur. Veritabanı 32-38. satırlarda tanımlanmıştır:
    • 34-35. satırlar: iki kişi oluşturulur
    • 37. satır: [service] katmanından bunların kalıcı hale getirilmesi istenir.
  • 20. satır: dumpPersonnes yöntemi, kalıcı kişileri görüntüler. Bu yöntem 24-29. satırlarda tanımlanmıştır
    • 26-28. satırlar: [service] katmanından tüm kalıcı kişilerin listesi istenir ve bunlar konsolda görüntülenir.

[InitDB]'in çalıştırılması şu sonucu verir:

1
2
3
[personnes]
[72,0,p1,Paul,31/01/2000,true,2]
[73,0,p2,Sylvie,05/07/2001,false,0]

3.1.7. [TestNG] birim testleri

[TestNG] eklentisinin kurulumu 5.2.4. paragrafında açıklanmaktadır. [TestNG] programının kodu şöyledir:


package tests;

....
public class TestNG {

    // hizmet katmanı
    private IService service;

    @BeforeClass
    public void init() {
        // günlük
        log("init");
        // uygulama yapılandırması
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // hizmet katmanı
        service = (IService) ctx.getBean("service");
    }

    @BeforeMethod
    public void setUp() throws ParseException {
        // veritabanını boşaltma
        clean();
        // veritabanını doldurma
        fill();
    }

    // günlükler
    private void log(String message) {
        System.out.println("----------- " + message);
    }

    // tablo içeriğinin görüntülenmesi
    private void dump() {
        log("dump");
        System.out.format("[personnes]%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // tablo doldurma
    public void fill() throws ParseException {
        log("fill");
        // kullanıcı oluşturma
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // kaydediliyor
        service.saveArray(new Personne[] { p1, p2 });
    }

    // tablodaki öğelerin silinmesi
    public void clean() {
        log("clean");
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    @Test()
    public void test01() {
...
    }
...
}
  • 9. satır: @BeforeClass yorumu, testler için gerekli yapılandırmayı başlatmak üzere çalıştırılacak yöntemi belirtir. Bu yöntem, ilk test çalıştırılmadan önce yürütülür. Burada kullanılmayan @AfterClass anotasyonu, tüm testler tamamlandıktan sonra çalıştırılacak yöntemi belirtir.
  • 10-17. satırlar: @BeforeClass anotasyonuyla işaretlenen init yöntemi, Spring yapılandırma dosyasını kullanarak uygulamanın farklı katmanlarını örneklendirir ve [service] katmanına bir referans oluşturur. Daha sonra tüm testler bu referansı kullanır.
  • 19. satır: @BeforeMethod anotasyonu, her testten önce çalıştırılacak yöntemi belirtir. Burada kullanılmayan @AfterMethod anotasyonu ise her testten sonra çalıştırılacak yöntemi belirtir.
  • 20-25. satırlar: @BeforeMethod ile işaretlenmiş setUp yöntemi, veritabanını boşaltır (clean satır 52-56) ve ardından iki kişi ile doldurur (fill satır 42-49).
  • 59. satır: @Test açıklaması, çalıştırılacak bir test yöntemini belirtir. Şimdi bu testleri açıklayacağız.

@Test()
    public void test01() {
        log("test1");
        dump();
        // kişi listesi
        List<Personne> personnes = service.getAll();
        assert 2 == personnes.size();
    }

    @Test()
    public void test02() {
        log("test2");
        // kişileri adlarına göre arama
        List<Personne> personnes = service.getAllLike("p1%");
        assert 1 == personnes.size();
        Personne p1 = personnes.get(0);
        assert "Paul".equals(p1.getPrenom());
    }

    @Test()
    public void test03() throws ParseException {
        log("test3");
        // yeni bir kişinin eklenmesi
        Personne p3 = new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // kayıtın kalıcı hale getirilmesi
        service.saveOne(p3);
        // kişiyi yeniden sorgulama
        Personne loadedp3 = service.getOne(p3.getId());
        // görüntüleme
        System.out.println(loadedp3);
        // doğrulama
        assert "p3".equals(loadedp3.getNom());
    }
  • satır 2-8: 01 numaralı test. Her testin başlangıcında, veritabanında sırasıyla p1 ve p2 adlı iki kişi bulunduğunu unutmamak gerekir.
  • 6. satır: Kişi listesi isteniyor
  • 7. satır: Elde edilen listede kişi sayısının 2 olup olmadığı kontrol edilir
  • 14. satır: adı p1 ile başlayan kişilerin listesi istenir
  • Elde edilen listenin tek bir öğe içerdiği (satır 15) ve elde edilen tek kişinin adının "Paul" olduğu (satır 17) kontrol edilir
  • 24. satır: p3 adlı bir kişi oluşturulur
  • 25. satır: bu kişi kalıcı hale getirilir
  • 28. satır: Doğrulama amacıyla bu kişiyi kalıcılık bağlamından yeniden istenir
  • satır 32: Alınan kişinin adının gerçekten p3 olup olmadığını kontrol ediyoruz.

@Test()
    public void test04() throws ParseException {
        log("test4");
        // p1 kişisini yükleme
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // görüntüleniyor
        System.out.println(p1);
        // doğrulama
        assert "p1".equals(p1.getNom());
        int version1 = p1.getVersion();
        // adını değiştiriliyor
        p1.setPrenom("x");
        // kaydediliyor
        service.updateOne(p1);
        // yeniden yükleniyor
        p1 = service.getOne(p1.getId());
        // görüntüleniyor
        System.out.println(p1);
        // sürüm numarasının artırıldığını kontrol ediliyor
        assert (version1 + 1) == p1.getVersion();

    }
  • 5. satır: p1 adlı kişiyi talep ediyoruz
  • satır 10: adını kontrol ediyoruz
  • 11. satır: Sürüm numarasını not ediyoruz
  • 13. satır: Adı değiştirilir
  • 15. satır: Değişiklik kaydedilir
  • 17. satır: p1 kişisini tekrar çağırıyoruz
  • 21. satır: sürüm numarasının 1 arttığını kontrol edin

@Test()
    public void test05() {
        log("test5");
        // p2 kişisini yüklenir
        List<Personne> personnes = service.getAllLike("p2%");
        Personne p2 = personnes.get(0);
        // görüntüleniyor
        System.out.println(p2);
        // kontrol ediliyor
        assert "p2".equals(p2.getNom());
        // p2 kişisini siliyoruz
        service.deleteOne(p2.getId());
        // yeniden yükleniyor
        p2 = service.getOne(p2.getId());
        // null işaretçisi elde edildiğini kontrol ediyoruz
        assert null == p2;
        // tablo görüntülenir
        dump();
    }
  • 5. satır: p2 kişisini çağırıyoruz
  • 10. satır: Adını kontrol ediyoruz
  • 12. satır: Siliniyor
  • 14. satır: Kişi yeniden aranır
  • 16. satır: Kişinin bulunamadığı kontrol ediliyor

@Test()
    public void test06() throws ParseException {
        log("test6");
        // aynı ada sahip 2 kişiden oluşan bir dizi oluşturulur (adın benzersizlik kuralı ihlal edilir)
        Personne[] personnes = { new Personne("p3", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2),
                new Personne("p4", "x", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2)};
        // bu tablo kaydediliyor - bir istisna ve geri alma işlemi gerçekleşmelidir
        boolean erreur = false;
        try {
            service.saveArray(personnes);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // döküm
        dump();
        // kontroller
        assert erreur;
        // p3 adlı kişiyi arama
        List<Personne> personnesp3 = service.getAllLike("p3%");
        assert 0 == personnesp3.size();
        // döküm
        dump();
    }
  • 5. satır: İki kişinin adı "p4" olan üç kişilik bir tablo oluşturulur. Bu, @Entity Personne'in adının benzersiz olması kuralını ihlal eder:

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
private String nom;
  • 11. satır: Üç kişiden oluşan dizi, kalıcılık bağlamına eklenir. İkinci kişi olan p4'ün eklenmesi başarısız olmalıdır. [saveArray] yöntemi bir işlem içinde gerçekleştirildiğinden, daha önce yapılmış olabilecek tüm eklemeler geri alınacaktır. Sonuç olarak, hiçbir ekleme yapılmayacaktır.
  • 18. satır: [saveArray] yönteminin bir istisna oluşturduğu doğrulanır
  • 20-21. satırlar: Eklenmiş olabilecek p3 kişisinin eklenmediğini kontrol ediyoruz.

@Test()
    public void test07() {
        log("test7");
        // iyimser kilitleme testi
        // p1 adlı kişiyi yüklüyor
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // görüntüleniyor
        System.out.println(p1);
        // çocuk sayısını artırıyoruz
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // p1'i kaydediyoruz
        Personne newp1 = service.updateOne(p1);
        assert (nbEnfants1 + 1) == newp1.getNbenfants();
        System.out.println(newp1);
        // ikinci kez kaydediliyor - p1 artık doğru sürüme sahip olmadığı için bir istisna oluşması gerekir
        // bunu newp1 aldı
        boolean erreur = false;
        try {
            service.updateOne(p1);
        } catch (RuntimeException e) {
            erreur = true;
        }
        // doğrulama
        assert erreur;
        // newp1'in çocuk sayısını artırıyoruz
        int nbEnfants2 = newp1.getNbenfants();
        newp1.setNbenfants(nbEnfants2 + 1);
        // newp1'e kaydediliyor
        service.updateOne(newp1);
        // yeniden yükleniyor
        p1 = service.getOne(p1.getId());
        // doğrulama
        assert (nbEnfants1 + 2) == p1.getNbenfants();
        System.out.println(p1);
    }
  • 6. satır: p1 kişisi istenir
  • 12. satır: Çocuk sayısını 1 artırıyoruz
  • 14. satır: p1 kişisi kalıcılık bağlamında güncellenir. [updateOne] yöntemi, p1’in yeni sürümü olan newp1’i kalıcı hale getirir. Bu sürüm, artırılmış olması gereken sürüm numarasıyla p1’den farklıdır.
  • 15. satır: newp1'in çocuk sayısını kontrol ediyoruz.
  • 21. satır: Eski sürüm p1'den yola çıkarak p1 kişisinin güncellenmesini yeniden talep ediyoruz. p1, p1 kişisinin en son sürümü olmadığı için bir istisna oluşması gerekir. En son sürüm newp1'dir.
  • 23. satır: Hatanın gerçekten meydana geldiği kontrol edilir.
  • 27-35. satırlar: En son sürüm olan newp1'den bir güncelleme yapıldığında her şeyin yolunda gittiği kontrol edilir.

@Test()
    public void test08() {
        log("test8");
        // updateArray üzerinde geri alma testi
        // p1 kişisi yükleniyor
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // görüntüleniyor
        System.out.println(p1);
        // çocuk sayısını artırılıyor
        int nbEnfants1 = p1.getNbenfants();
        p1.setNbenfants(nbEnfants1 + 1);
        // 2 değişiklik kaydediliyor; bunlardan ikincisi başarısız olmalı (kişi yanlış başlatılmış)
        // işlem nedeniyle her ikisi de iptal edilmelidir
        boolean erreur = false;
        try {
            service.updateArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // kontroller
        assert erreur;
        // p1 kişisi yeniden yükleniyor
        personnes = service.getAllLike("p1%");
        p1 = personnes.get(0);
        // çocuk sayısı değişmemiş olmalı
        assert nbEnfants1 == p1.getNbenfants();
    }
  • Test 8, test 6'ya benzerdir: İkinci kişinin doğru şekilde başlatılmadığı iki kişilik bir dizide çalışan updateArray üzerinde rollback'i kontrol eder. JPA açısından bakıldığında, henüz mevcut olmayan ikinci kişi üzerindeki birleştirme işlemiSQL insert emrini oluşturacak ve bu emir, nullable=false varlığının bazı alanlarında bulunan kısıtlamalar nedeniyle başarısız olacaktır.

@Test()
    public void test09() {
        log("test9");
        // deleteArray üzerinde geri alma testi
        // döküm
        dump();
        // p1 kişisi yükleniyor
        List<Personne> personnes = service.getAllLike("p1%");
        Personne p1 = personnes.get(0);
        // görüntüleniyor
        System.out.println(p1);
        // 2 silme işlemi yapılıyor; bunlardan ikincisi başarısız olmalı (bilinmeyen kişi)
        // işlem nedeniyle her ikisi de iptal edilmelidir
        boolean erreur = false;
        try {
            service.deleteArray(new Personne[] { p1, new Personne() });
        } catch (RuntimeException e) {
            erreur = true;
        }
        // kontroller
        assert erreur;
        // p1 kişisi yeniden yükleniyor
        personnes = service.getAllLike("p1%");
        // doğrulama
        assert 1 == personnes.size();
        // döküm
        dump();
    }
  • 9 numaralı test bir öncekine benzer: rollback'i, iki kişiden oluşan bir tabloda çalışan deleteArray üzerinde doğrular; bu tabloda ikinci kişi mevcut değildir. Ancak bu durumda, [dao] katmanındaki [deleteOne] yöntemi bir istisna oluşturur.

// iyimser kilitleme - çoklu iş parçacığı erişimi
    @Test()
    public void test10() throws Exception {
        // bir kişinin eklenmesi
        Personne p3 = new Personne("X", "X", new SimpleDateFormat("dd/MM/yyyy").parse("01/02/2006"), true, 0);
        service.saveOne(p3);
        int id3 = p3.getId();
        // çocuk sayısını güncellemek için N iş parçacığı oluşturma
        final int N = 20;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadMajEnfants("thread n° " + i, service, id3);
            taches[i].start();
        }
        // iş parçacıklarının bitmesini bekliyoruz
        for (int i = 0; i < taches.length; i++) {
            taches[i].join();
        }
        // kişiyi alıyoruz
        p3 = service.getOne(id3);
        // bu kişinin N çocuğu olması gerekir
        assert N == p3.getNbenfants();
        // p3 kişisinin silinmesi
        service.deleteOne(p3.getId());
        // doğrulama
        p3 = service.getOne(p3.getId());
        // null işaretçisi olmalı
        assert p3 == null;
    }
  • Test 10'un amacı, bir kişinin çocuk sayısını paralel olarak artırmak için N sayıda iş parçacığı (9. satır) başlatmaktır. Sürüm numarası sisteminin bu senaryoya karşı dayanıklı olduğunu doğrulamak istiyoruz. Sistem bu amaçla oluşturulmuştur.
  • 5-6. satırlar: p3 adlı bir kişi oluşturulur ve kalıcı hale getirilir. Başlangıçta 0 çocuğu vardır.
  • 7. satır: Kimliği kaydedilir.
  • 9-14. satırlar: p3'ün çocuk sayısını 1 artırmakla görevli N adet iş parçacığı paralel olarak başlatılır.
  • 16-18. satırlar: Tüm iş parçacıklarının tamamlanmasını bekliyoruz
  • 20. satır: p3 adlı kişiyi görüntülemek istenir
  • 22. satır: Artık N çocuğu olup olmadığı kontrol edilir.
  • 24. satır: p3 silinir.

[ThreadMajEnfants] iş parçacığı şöyledir:


package tests;

...
public class ThreadMajEnfants extends Thread {
    // iş parçacığının adı
    private String name;

    // katman referansı [service]
    private IService service;

    // üzerinde çalışılacak kişinin kimliği
    private int idPersonne;

    // oluşturucu
    public ThreadMajEnfants(String name, IService service, int idPersonne) {
        this.name = name;
        this.service = service;
        this.idPersonne = idPersonne;
    }

    // konu özü
    public void run() {
        // izleme
        suivi("lancé");
        // 1 artırmayı başaramayana kadar döngü devam eder
        // idPersonne kişisinin çocuk sayısı
        boolean fini = false;
        int nbEnfants = 0;
        while (!fini) {
            // idPersonne'ten kişinin bir kopyası alınır
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // takip
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // kişinin çocuk sayısını 1 artırır
            personne.setNbenfants(nbEnfants + 1);
            // işlemciyi serbest bırakmak için 10 ms bekler
            try {
                // devam
                suivi("début attente");
                // işlemciyi serbest bırakmak için kesintiye gidilir
                Thread.sleep(10);
                // takip
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
            }
            // bekleme sona erdi - kopyayı onaylamaya çalışılıyor
            // bu arada diğer iş parçacıkları orijinali değiştirmiş olabilir
            try {
                // orijinali değiştirmeye çalışılıyor
                service.updateOne(personne);
                // işlem tamamlandı - orijinal değiştirildi
                fini = true;
            } catch (javax.persistence.OptimisticLockException e) {
                // nesnenin sürümü yanlış: yeniden başlamak için istisna göz ardı ediliyor
            } catch (org.springframework.transaction.UnexpectedRollbackException e2) {
                // zaman zaman ortaya çıkan Spring istisnası
            } catch (RuntimeException e3) {
                // başka bir istisna türü - istisna üst katmana iletiliyor
                throw e3;
            }
        }
        // izleme
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }

    // takip
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : " + message);
    }
}
  • satır 15-19: Oluşturucu, çalışmak için ihtiyaç duyduğu bilgileri hafızaya alır: kendi adı (satır 16), kullanması gereken [service] katmanındaki referans (satır 17) ve çocuk sayısını artırması gereken p kişisinin kimliği (satır 18).
  • satır 22-66: Tüm iş parçacıkları tarafından paralel olarak yürütülen [run] yöntemi.
  • 29. satır: İş parçacığı, p kişisinin çocuk sayısını artırmayı tekrar tekrar dener. Ancak başarılı olduğunda durur.
  • satır 31: p kişisi sorgulanır
  • satır 36: bellekte çocuk sayısı artırılır
  • satır 38-47: 10 ms'lik bir duraklama yapılır. Bu, diğer iş parçacıklarının p kişisinin aynı sürümüne erişmesini sağlayacaktır. Böylece aynı anda, p kişisinin aynı sürümüne sahip olan ve onu değiştirmek isteyen birden fazla iş parçacığı olacaktır. İstenen de budur.
  • 52. satır: Bekleme süresi bittiğinde, iş parçacığı [service] katmanından değişikliği kalıcı hale getirmesini ister. Zaman zaman istisnalar olacağı bilindiğinden, işlem bir try / catch bloğu içine alınmıştır.
  • 55. satır: Testler, [javax.persistence.OptimisticLockException] türünde istisnalar oluştuğunu göstermektedir. Bu normaldir: Bir iş parçacığı, p kişisinin en son sürümüne sahip olmadan onu değiştirmeye çalıştığında JPA katmanı tarafından atılan istisnadır. Bu istisna, iş parçacığının işlemi başardığı ana kadar yeniden denemesine izin vermek için göz ardı edilir.
  • 57. satır: Testler, [org.springframework.transaction.UnexpectedRollbackException] türünde istisnalar da oluştuğunu gösteriyor. Bu can sıkıcı ve beklenmedik bir durum. Bunun için bir açıklamam yok. Böylece Spring'e bağımlı hale geldik, oysa bunu önlemek istemiştik. Bu, örneğin uygulamamızı JBoss Ejb3'te çalıştırırsak, iş parçacığının kodunun değiştirilmesi gerektiği anlamına gelir. Burada da Spring istisnası, iş parçacığının artırım işlemini yeniden denemesine olanak sağlamak için göz ardı edilir.
  • 59. satır: Diğer istisna türleri uygulamaya iletilir.

[TestNG] çalıştırıldığında şu sonuçlar elde edilir:

Image

10 testin tümü başarıyla geçilmiştir.

10 numaralı test, başarılı olması biraz sihirli bir yönü olduğu için ek açıklamayı hak ediyor. Öncelikle [dao] katmanının yapılandırmasına geri dönelim:


public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

  • 4. satır: [EntityManager] nesnesi, JPA @PersistenceContext anotasyonu aracılığıyla em alanına eklenir. [dao] katmanı tek bir kez örneklenir. Bu, JPA katmanını kullanan tüm iş parçacıkları tarafından kullanılan bir singleton'dur. Dolayısıyla, EntityManager em değeri tüm iş parçacıkları tarafından paylaşılır. Bunu, [ThreadMajEnfants] iş parçacıkları tarafından kullanılan [updateOne] yönteminde em değerini görüntüleyerek doğrulayabiliriz: tüm iş parçacıkları için aynı değer elde edilir.

Bu durumda, tüm iş parçacıkları için aynı olan EntityManager em tarafından yönetilen farklı iş parçacıklarının kalıcı nesnelerinin birbirine karışıp aralarında çakışmalara yol açıp açmayacağı sorusu akla gelebilir. Bunun ne anlama gelebileceğine dair bir örnek [ThreadMajEnfants]'te bulunabilir:


        while (!fini) {
            // idPersonne'ten kişinin bir kopyası alınır
            Personne personne = service.getOne(idPersonne);
            nbEnfants = personne.getNbenfants();
            // takip
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1) + " pour la version " + personne.getVersion());
            // kişinin çocuk sayısını 1 artırır
            personne.setNbenfants(nbEnfants + 1);
            // işlemciyi serbest bırakmak için 10 ms bekler
            try {
                // takip
                suivi("début attente");
                // işlemciyi serbest bırakmak için kesintiye gidilir
                Thread.sleep(10);
                // takip
                suivi("fin attente");
            } catch (Exception ex) {
                throw new RuntimeException(ex.toString());
}
  • 3. satır: Bir T1 iş parçacığı p kişisini alır
  • 8. satır: p'nin çocuk sayısını artırır
  • 14. satır: T1 iş parçacığı bir ara verir

T2 iş parçacığı devreye girer ve o da 3. satırı yürütür: T1 ile aynı p kişisini ister. Eğer iş parçacıklarının kalıcılık bağlamı aynı olsaydı, T1 sayesinde bağlamda zaten bulunan p kişisi, T2'e geri verilmeliydi. Nitekim, [getOne] yöntemi, [EntityManager] yöntemini kullanır.API, JPA'i bulur ve bu yöntem, istenen nesne kalıcılık bağlamının bir parçası değilse veritabanına erişir; aksi takdirde kalıcılık bağlamındaki nesneyi döndürür. Böyle bir durumda, T1 ve T2 aynı p nesnesine sahip olurdu. T2 ise p'nin çocuk sayısını tekrar 1 artırırdı (8. satır). Duraklamadan sonra iş parçacıklarından biri güncellemeyi başarıyla gerçekleştirirse, p'nin çocuk sayısı beklendiği gibi 1 değil, 2 artmış olur. Bu durumda N iş parçacığının çocuk sayısını N'ye değil, daha fazla bir sayıya yükselteceği beklenebilir. Ancak durum böyle değildir. Bundan, T1 ve T2'in aynı p referansına sahip olmadığı sonucuna varabiliriz. Bunu, iş parçacıklarının p'nin adresini görüntülemesini sağlayarak doğrulayabiliriz: her biri için adres farklıdır.

Dolayısıyla, iş parçacıkları:

  • aynı kalıcılık bağlam yöneticisini (EntityManager) paylaşıyor
  • ancak her birinin kendine özgü bir kalıcılık bağlamı var.

Bunlar sadece varsayımlardır ve bu konuda bir uzmanın görüşü faydalı olabilir.

3.1.8. SGBD'e geçmek

SGBD'i değiştirmek için, [src/spring-config.xml] [2] dosyasını, ilgili SGBD dosyasının [spring-config.xml] dosyasını [conf] [1] klasöründeki ilgili SGBD dosyasını [spring-config.xml] dosyasıyla değiştirmek yeterlidir.

Örneğin, Oracle'ın [spring-config.xml] dosyası şöyledir:


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver" />
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
...
</beans>

Daha önce MySQL5 için kullanılan aynı dosyaya kıyasla yalnızca bazı satırlar değişmiştir:

  • 14. satır: Hibernate'in kullanması gereken SQL lehçesi
  • 25-28. satırlar: SGBD ile JDBC bağlantısının özellikleri

Okuyucunun, MySQL5 için açıklanan testleri diğer SGBD dosyalarıyla da tekrarlaması önerilir.

3.1.9. JPA uygulamasını değiştirme

Önceki testlerin mimarisine geri dönelim:

JPA / Hibernate uygulamasını, JPA / Toplink uygulamasıyla değiştiriyoruz. Toplink, Hibernate ile aynı kütüphaneleri kullanmadığından, yeni bir Eclipse projesi kullanıyoruz:

  • [1]: Eclipse projesi. Bu proje, öncekiyle aynıdır. Değişen tek şey, [spring-config.xml] ve [2] yapılandırma dosyaları ile [jpa-hibernate] kütüphanesinin yerine geçen [jpa-toplink] kütüphanesidir.
  • [3]: Bu öğreticinin örnek dosyaları. [4]: İçe aktarılacak Eclipse projesi.

Toplink için [spring-config.xml] yapılandırma dosyası şu şekilde olur:


<?xml version="1.0" encoding="UTF-8"?>

<!-- JVM, -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:tx="http://www.springframework.org/schema/tx"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd">

    <!-- uygulama katmanları -->
    <bean id="dao" class="dao.Dao" />
    <bean id="service" class="service.Service">
        <property name="dao" ref="dao" />
    </bean>

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
    </bean>

    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="com.mysql.jdbc.Driver" />
        <property name="url" value="jdbc:mysql://localhost:3306/jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>

    <!-- işlem yöneticisi -->
    <tx:annotation-driven transaction-manager="txManager" />
    <bean id="txManager" class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>

    <!-- istisna çevirisi -->
    <bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />

    <!-- kalıcılık -->
    <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" />

</beans>

Hibernate'ten Toplink'e geçmek için birkaç satırın değiştirilmesi gerekir:

  • 19. satır: JPA uygulaması artık Toplink tarafından gerçekleştiriliyor
  • 23. satır: [databasePlatform] özelliğinin değeri, Hibernate'de olduğundan farklıdır: Toplink'e özgü bir sınıf adıdır. Bu adın nerede bulunacağı 2.1.15.2. paragrafında açıklanmıştır.

Hepsi bu kadar. Spring ile SGBD veya JPA uygulamasını ne kadar kolay değiştirebileceğimize dikkat edin.

Yine de henüz tam olarak bitmiş sayılmayız. Örneğin, [InitDB]'i çalıştırdığımızda, anlaşılması kolay olmayan bir istisna ile karşılaşırız:


Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory' defined in class path resource [spring-config.xml]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.
Caused by: java.lang.IllegalStateException: Must start with Java agent to use 

  1. satırdaki hata mesajı, Spring belgelerini okumaya yönlendiriyor. Burada, [spring-config.xml] dosyasındaki belirsiz bir bildirimin oynadığı rol hakkında biraz daha bilgi ediniyoruz:

    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
                <!-- 
                    <property name="showSql" value="true" />
                -->
                <property name="databasePlatform" value="org.hibernate.dialect.OracleDialect" />
                <property name="generateDdl" value="true" />
            </bean>
        </property>
        <property name="loadTimeWeaver">
            <bean class="org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
        </property>
</bean>

İstisnanın 1. satırı, [InstrumentationLoadTimeWeaver] adlı bir sınıfa atıfta bulunuyor; bu sınıf, Spring yapılandırma dosyasının 13. satırında yer alıyor. Spring belgelerinde, bu sınıfın bazı durumlarda uygulama sınıflarını yüklemek için gerekli olduğu ve bu sınıfın çalışabilmesi için JVM'in bir aracı ile başlatılması gerektiği açıklanmaktadır. Bu aracı Spring tarafından sağlanır ve adı [spring-agent]'tir:

  • [spring-agent.jar] dosyası, <exemples>/lib [1] klasöründe bulunur. Bu dosya, Spring dağıtımıyla birlikte sağlanır (bkz. paragraf 5.11).
  • [3] dosyasında, [Run/Run...] adlı bir yürütme yapılandırması oluşturulur
  • [4]'te bir Java yürütme yapılandırması oluşturulur (çeşitli türde yürütme yapılandırmaları vardır)
  • [5]'te, [Main] sekmesi seçilir
  • [6]'te yapılandırmaya bir ad verin
  • [7]'te, bu yapılandırmayla ilgili Eclipse projesine bir ad verin (Browse düğmesini kullanın)
  • [8]'te, [main] yöntemini içeren Java sınıfına bir ad verin (Browse düğmesini kullanın)
  • [9]'te, [Arguments] sekmesine geçin. Bu sekmede iki tür argüman belirtebilirsiniz:
    • [9]'te, [main] yöntemine geçirilenler
    • [10]'te, kodu çalıştıracak olan JVM'e geçirilenler. Spring ajanı, JVM'in -javaagent:değer parametresi kullanılarak tanımlanır. Değer, [spring-agent.jar] dosyasının yoludur.
  • [11]'te: yapılandırma onaylanır
  • [12]'te: yapılandırma oluşturulur
  • [13]'te: yapılandırma çalıştırılır

Bu işlem tamamlandıktan sonra, [InitDB] çalıştırılır ve Hibernate ile aynı sonuçları verir. [TestNG] için de aynı şekilde ilerlenmelidir:

  • [1]'te, [Run/Run...] adlı bir yürütme yapılandırması oluşturulur
  • [2]'te, TestNG adlı bir yürütme yapılandırması oluşturulur
  • [3]'te, [Test] sekmesi seçilir
  • [4]'te yapılandırmaya bir ad verilir
  • [5]'te, bu yapılandırmayla ilgili Eclipse projesinin adını girin (Browse düğmesini kullanın)
  • [6]'te, test sınıfına bir ad verin (Browse düğmesini kullanın)
  • [7]'te, [Arguments] sekmesine geçin.
  • [8]'te: JVM'in -javaagent argümanını ayarlayın.
  • [9]'te: yapılandırmayı onaylayın
  • [10]'te: yapılandırma oluşturulur
  • [11]'te: çalıştırılır

Bu işlem tamamlandıktan sonra, [TestNG] çalıştırılır ve Hibernate ile aynı sonuçları verir.

3.2. Örnek 2: JBoss EJB3 / JPA, Personne varlığı ile

Önceki örnekle aynı örneği kullanıyoruz, ancak bunu EJB3 konteynerinde, yani JBoss'in konteynerinde çalıştırıyoruz:

Bir Ejb3 konteyneri normalde bir uygulama sunucusuna entegre edilir. JBoss, uygulama sunucusu dışında kullanılabilen "bağımsız" bir Ejb3 konteyneri sunar. Bu konteynerin, Spring tarafından sunulanlara benzer hizmetler sağladığını göreceğiz. Bu konteynerlerden hangisinin daha kullanışlı olduğunu incelemeye çalışacağız.

JBoss Ejb3 konteynerinin kurulumu 5.12. paragrafta açıklanmaktadır.

3.2.1. Eclipse / JBoss EJB3 / Hibernate Projesi

Eclipse projesi şöyledir:

  • [1]: Eclipse projesi. Bu proje, [6] dosyasında, [5] numaralı öğreticinin örnekleri arasında bulunabilir. Bu projeyi içe aktaracağız.
  • [2] dosyasında: paketler halinde sunulan katmanların Java kodları:
    • [entites]: JPA varlık paketi
    • [dao]: Veri erişim katmanı - JPA katmanına dayanır
    • [service]: iş mantığından çok hizmet odaklı bir katman. Burada Ejb3 konteynerinin işlem hizmeti kullanılacaktır.
    • [tests]: Test programlarını bir araya getirir.
  • [3]: [jpa-jbossejb3] kütüphanesi, JBoss EJB3 için gerekli jar dosyalarını bir araya getirir (ayrıca bkz. [7] ve [8]).
  • [4]: [conf] klasörü, bu öğreticide kullanılan her bir SGBD için yapılandırma dosyalarını bir araya getirir. Her seferinde iki dosya bulunur: [persistence.xml], JPA katmanını yapılandırır; [jboss-config.xml] ise Ejb3 konteynerini yapılandırır.

3.2.2. JPA varlıkları

Burada yönetilen tek bir varlık vardır: daha önce 3.1.2. paragrafında incelenen Personne varlığı.

3.2.3. [dao] katmanı

[dao] katmanı, daha önce 3.1.3. paragrafında açıklanan [IDao] arayüzünü sunar.

Bu arayüzün [Dao] uygulaması şu şekildedir:


package dao;

...
@Stateless
public class Dao implements IDao {

    @PersistenceContext
    private EntityManager em;

    // kimlik numarasıyla bir kişiyi silme
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public void deleteOne(Integer id) {
        Personne personne = em.find(Personne.class, id);
        if (personne == null) {
            throw new DaoException(2);
        }
        em.remove(personne);
    }

    // tüm kişileri alma
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAll() {
        return em.createQuery("select p from Personne p").getResultList();
    }

    // adı belirli bir şablona uyan kişileri alma
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public List<Personne> getAllLike(String modele) {
        return em.createQuery("select p from Personne p where p.nom like :modele")
                .setParameter("modele", modele).getResultList();
    }

    // kimlik numarasıyla bir kişiyi alma
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne getOne(Integer id) {
        return em.find(Personne.class, id);
    }

    // bir kişiyi kaydetmek
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne saveOne(Personne personne) {
        em.persist(personne);
        return personne;
    }

    // bir kişiyi güncelle
    @TransactionAttribute(TransactionAttributeType.REQUIRED)
    public Personne updateOne(Personne personne) {
        return em.merge(personne);
    }

}
  • Bu kod, Spring ile kullandığımız kodla her açıdan aynıdır. Yalnızca Java anotasyonları değişmektedir ve biz de bu konuyu ele alıyoruz.
  • 4. satır: @Stateless anotasyonu, [Dao] sınıfını durumuz bir EJB yapar. @Stateful anotasyonu ise bir sınıfı durumlu bir EJB yapar. Durumlu bir EJB, zaman içinde değerlerinin korunması gereken özel alanlara sahiptir. Klasik bir örnek, bir uygulamanın web kullanıcısıyla ilgili bilgileri içeren bir sınıftır. Bu sınıfın bir örneği belirli bir kullanıcıyla ilişkilidir ve bu kullanıcının bir isteğinin yürütme iş parçacığı sona erdiğinde, aynı müşterinin bir sonraki isteğinde kullanılabilir olması için bu örnek korunmalıdır. Bir @Stateless EJB'nin durumu yoktur. Aynı örneği ele alırsak, bir kullanıcının isteğinin yürütme iş parçacığı sona erdiğinde, @Stateless EJB bir @Stateless EJB havuzuna eklenir ve başka bir kullanıcının isteğinin yürütme iş parçacığı için kullanılabilir hale gelir.
  • Geliştirici açısından, EJB3 @Stateless kavramı Spring’deki singleton kavramına yakındır. Geliştirici, bunu aynı durumlarda kullanacaktır.
  • 7. satır: @PersistenceContext anotasyonu, [dao] katmanının Spring sürümünde karşılaşılan anotasyonla aynıdır. Bu, EntityManager'i alacak olan alanı belirtir; bu da [dao] katmanının kalıcılık bağlamını yönetmesini sağlar.
  • 11. satır: Bir yönteme uygulanan @TransactionAttribute anotasyonu, yöntemin çalışacağı işlemi yapılandırmaya yarar. Bu anotasyonun olası değerlerinden bazıları şunlardır:
    • TransactionAttributeType.REQUIRED: Yöntem bir işlem içinde yürütülmelidir. Zaten başlatılmış bir işlem varsa, yöntemin kalıcılık işlemleri bu işlem içinde gerçekleştirilir. Aksi takdirde, bir işlem oluşturulur ve başlatılır.
    • TransactionAttributeType.REQUIRES_NEW: Yöntem, yeni bir işlem içinde yürütülmelidir. Bu işlem oluşturulur ve başlatılır.
    • TransactionAttributeType.MANDATORY: Yöntem, mevcut bir işlem içinde çalıştırılmalıdır. Eğer böyle bir işlem yoksa, bir istisna atılır.
    • TransactionAttributeType.NEVER: Yöntem hiçbir zaman bir işlem içinde çalıştırılmaz.
    • ...

Bu açıklama sınıfın kendisine de eklenebilirdi:


@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Dao implements IDao {

Bu durumda öznitelik, sınıfın tüm yöntemlerine uygulanır.

3.2.4. [metier / service] katmanı

[service] katmanı, daha önce 3.1.4 paragrafında incelenen [IService] arayüzünü sunar. [IService] arayüzünün [Service] uygulaması, 3.1.4. paragrafında daha önce incelenen uygulamayla üç ayrıntı dışında aynıdır:



@Stateless
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public class Service implements IService {

    // katman [dao]
    @EJB
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

    public void setDao(IDao dao) {
        this.dao = dao;
    }

  • 2. satır: [Service] sınıfı durum bilgisi içermeyen bir EJB'dir
  • 3. satır: [Service] sınıfındaki tüm yöntemler bir işlem içinde gerçekleştirilmelidir
  • 7-8. satırlar: [dao] katmanındaki EJB'ye ait bir referans, EJB konteyneri tarafından 8. satırdaki [IDao dao] alanına enjekte edilecektir. Bu enjeksiyonu talep eden, 7. satırdaki @EJB anotasyonudur. Enjekte edilen nesne bir EJB olmalıdır. Bu, herhangi bir nesne türünün başka bir nesneye enjekte edilebildiği Spring ile önemli bir fark oluşturur.

3.2.5. Katmanların yapılandırılması

[service], [dao] ve [JPA] katmanlarının yapılandırması aşağıdaki dosyalarla sağlanır:

  • [META-INF/persistence.xml] dosyası, JPA katmanını yapılandırır
  • [jboss-config.xml], Ejb3 konteynerini yapılandırır. Bu dosya, [default.persistence.properties, ejb3-interceptors-aop.xml, embedded-jboss-beans.xml, jndi.properties] dosyalarını kullanır. Bu dosyalar Jboss Ejb3 ile birlikte gelir ve normalde değiştirilmeyen bir varsayılan yapılandırma sağlar. Geliştirici yalnızca [jboss-config.xml] dosyasıyla ilgilenir

İki yapılandırma dosyasını inceleyelim:

persistence.xml


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- JPA sağlayıcısı Hibernate'dir -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- DataSource JTA, Java ortamı tarafından yönetilir EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- JBA katmanındaki varlıkların aranması -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- SQL Hibernate günlükleri
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- SGBD türü yönetiliyor -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />

            <!-- kalıcılık biriminin dağıtımı sırasında tüm tabloların yeniden oluşturulması (drop+create) -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Bu dosya, JPA varlıklarını incelerken daha önce karşılaştıklarımıza benziyor. Bir JPA Hibernate katmanını yapılandırıyor. Yeni özellikler şunlardır:

  • 5. satır: jpa kalıcılık birimi, şimdiye kadar her zaman mevcut olan transaction-type özniteliğine sahip değildir:

<persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL" />

Değer belirtilmediğinde, transaction-type özniteliğinin varsayılan değeri "JTA" (Java Transaction API için) olur; bu, işlem yöneticisinin bir EJB3 konteyneri tarafından sağlandığını gösterir. "JTA" işlem yöneticisi, "RESOURCE_LOCAL" işlem yöneticisinden daha fazla işlev görebilir: birden fazla bağlantıyı kapsayan işlemleri yönetebilir. JTA ile, SGBD 1 üzerindeki c1 bağlantısı üzerinden t1 işlem açılabilir, SGBD 1 üzerinde bir c1 bağlantısı üzerinden bir t2 işlemi açabilir ve (t1,t2) 'yi, ya tüm işlemlerin başarılı olduğu (commit) ya da hiçbirinin başarılı olmadığı (rollback) tek bir işlem olarak değerlendirebiliriz.

Burada, Jboss Ejb3 konteynerinin JTA yöneticisiyle çalışıyoruz.

  • 11. satır: JTA yöneticisinin kullanması gereken veri kaynağını bildirir. Bu, JNDI (Java Naming and Directory Interface) adı biçiminde verilir. Bu veri kaynağı, [jboss-config.xml] içinde tanımlanmıştır.

jboss-config.xml


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- DataSource fabrikası -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- DataSource'in adı JNDI -->
        <property name="jndiName">java:/datasource</property>

        <!-- yönetilen veritabanı -->
        <property name="driverClass">com.mysql.jdbc.Driver</property>
        <property name="connectionURL">jdbc:mysql://localhost:3306/jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- bağlantı havuzu özellikleri -->
        <property name="minSize">0</property>
        <property name="maxSize">10</property>
        <property name="blockingTimeout">1000</property>
        <property name="idleTimeout">100000</property>

        <!-- işlem yöneticisi, burada JTA -->
        <property name="transactionManager">
            <inject bean="TransactionManager" />
        </property>
        <!-- Hibernate önbellek yöneticisi -->
        <property name="cachedConnectionManager">
            <inject bean="CachedConnectionManager" />
        </property>
        <!-- JNDI örneklendirme özellikleri? -->
        <property name="initialContextProperties">
            <inject bean="InitialContextProperties" />
        </property>
    </bean>

    <!-- DataSource bir fabrikadan talep ediliyor -->
    <bean name="datasource" class="java.lang.Object">
        <constructor factoryMethod="getDatasource">
            <factory bean="datasourceFactory" />
        </constructor>
    </bean>

</deployment>
  • 3. satır: Dosyanın kök etiketi <deployment>'dir. Bu dağıtım dosyası, esas olarak persistence.xml dosyasında tanımlanan java:/datasource veri kaynağını yapılandırmayı amaçlamaktadır.
  • Veri kaynağı, 38. satırdaki "datasource" bean'i tarafından tanımlanmaktadır. Veri kaynağının (40. satır), 7. satırdaki "datasourceFactory" bean'i tarafından tanımlanan bir "factory"den alındığı görülmektedir. Uygulamanın veri kaynağını elde etmek için istemci, factory'in [getDatasource] yöntemini çağırmalıdır (39. satır).
  • 7. satır: Veri kaynağını sağlayan factory, bir Jboss sınıfıdır.
  • 9. satır: Veri kaynağının adı JNDI'tir. Bu ad, persistence.xml dosyasındaki <jta-data-source> etiketinde bildirilen adla aynı olmalıdır. Nitekim, JPA katmanı, veri kaynağını talep etmek için bu JNDI adını kullanacaktır.
  • 12-15. satırlar: Daha klasik bir örnek: SGBD bağlantısının JDBC özellikleri
  • 18-21. satırlar: JBoss EJB3 konteynerinin dahili bağlantı havuzunun yapılandırması.
  • 24-26. satırlar: JTA yöneticisi. 25. satırda enjekte edilen [TransactionManager] sınıfı, [embedded-jboss-beans.xml] dosyasında tanımlanmıştır.
  • 28-30. satırlar: Hibernate önbelleği; bu kavramı daha önce ele almamıştık. 29. satırda enjekte edilen [CachedConnectionManager] sınıfı, [embedded-jboss-beans.xml] dosyasında tanımlanmıştır. Yapılandırmanın artık Hibernate’e bağlı olduğu unutulmamalıdır; bu durum, Toplink’e geçiş yapmak istediğimizde sorun yaratacaktır.
  • 32-34. satırlar: JNDI hizmetinin yapılandırması.

Jboss Ejb3 yapılandırma dosyası ile işimiz bitti. Dosya karmaşıktır ve pek çok husus belirsizliğini korumaktadır. Bu dosya, [ref1] dosyasından alınmıştır. Ancak bunu başka bir SGBD'e uyarlayabileceğiz (jboss-config.xml'in 12-15. satırları, persistence.xml'in 24. satırı). Örneklerin eksikliği nedeniyle Toplink'e geçiş mümkün olmadı.

3.2.6. [InitDB] istemci programı

Daha önce açıklanan mimariye ait ilk istemciyi yazmaya başlıyoruz:

[InitDB] kodunun içeriği şöyledir:


package tests;

...
public class InitDB {

    // hizmet katmanı
    private static IService service;

    // oluşturucu
    public static void main(String[] args) throws ParseException, NamingException {
        // EJB3 JBoss konteyneri başlatılıyor
        // ejb3-interceptors-aop.xml ve embedded-jboss-beans.xml yapılandırma dosyaları kullanılıyor
        EJB3StandaloneBootstrap.boot(null);

        // Uygulamaya özgü bean'lerin oluşturulması
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Sınıf yolunda bulunan tüm EJBs'leri dağıt (yavaş, tümünü tarar)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Uygulamanın sınıf yolunda bulunan tüm EJB dosyaları dağıtılıyor
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // JNDI bağlamı başlatılıyor. jndi.properties dosyası kullanılıyor
        InitialContext initialContext = new InitialContext();

        // hizmet katmanının örneği oluşturuluyor
        service = (IService) initialContext.lookup("Service/local");
        // veritabanı boşaltılıyor
        clean();
        // veritabanı dolduruluyor
        fill();
        // görsel olarak kontrol ediliyor
        dumpPersonnes();
        // EJB konteyneri durduruluyor
        EJB3StandaloneBootstrap.shutdown();

    }

    // tablo içeriğinin görüntülenmesi
    private static void dumpPersonnes() {
        System.out.format("[personnes]-------------------------------------------------------------------%n");
        for (Personne p : service.getAll()) {
            System.out.println(p);
        }
    }

    // tablo dolduruluyor
    public static void fill() throws ParseException {
        // kullanıcılar oluşturulur
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // kaydediliyor
        service.saveArray(new Personne[] { p1, p2 });
    }

    // tablodaki öğeleri silme
    public static void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }
}
  • Jboss Ejb3 konteynerinin nasıl başlatılacağı, [ref1]'te bulunmuştur.
  • 13. satır: konteyner başlatılır. [EJB3StandaloneBootstrap], konteynerin bir sınıfıdır.
  • 16. satır: [jboss-config.xml] tarafından yapılandırılan dağıtım birimi konteynere dağıtılır: JTA yöneticisi, veri kaynağı, bağlantı havuzu, Hibernate önbelleği ve JNDI hizmeti kurulur.
  • 22. satır: Konteynerden, Eclipse projesinin bin klasörünü tarayarak EJB'leri bulması istenir. [service] ve [dao] katmanlarına ait EJB'ler bulunacak ve konteyner tarafından yönetilecektir.
  • 25. satır: Bir JNDI bağlamı başlatılır. Bu bağlam, EJB'leri bulmak için kullanılacaktır.
  • 28. satır: [service] katmanındaki [Service] sınıfına karşılık gelen EJB, JNDI hizmetinden istenir. Bir EJB'ye yerel olarak (local) veya ağ üzerinden (remote) erişilebilir. Burada, aranan EJB'nin "Service/local" adı, yerel erişim için [service] katmanındaki [Service] sınıfını belirtir.
  • Şimdi uygulama dağıtıldı ve [service] katmanına bir referansımız var. Spring sürümündeki [InitDB] kodunun aşağıdaki 11. satırından sonraki durumla aynı durumdayız. Dolayısıyla her iki sürümde de aynı kodla karşılaşıyoruz.

public class InitDB {

    // hizmet katmanı
    private static IService service;

    // oluşturucu
    public static void main(String[] args) throws ParseException {
        // uygulama yapılandırması
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // hizmet katmanı
        service = (IService) ctx.getBean("service");
        // veritabanını boşaltma
        clean();
        // veritabanını doldurma
        fill();
        // gözle kontrol ediliyor
        dumpPersonnes();
    }
...
  • 36. satır (Jboss Ejb3): Ejb3 konteynerini durduruyoruz.

[InitDB] kodunun çalıştırılması şu sonuçları verir:

16:07:00,781  INFO LocalTxDataSource:117 - Bound datasource to JNDI name 'java:/datasource'
...
16:07:01,171  INFO Version:94 - Hibernate EntityManager 3.2.0.CR1
...
16:07:01,296  INFO Ejb3Configuration:94 - Processing PersistenceUnitInfo [
    name: jpa
    ...]
16:07:01,312  INFO Ejb3Configuration:94 - found EJB3 Entity bean: entites.Personne
...
16:07:01,375  INFO Configuration:94 - Reading mappings from resource: META-INF/orm.xml
16:07:01,375  INFO Ejb3Configuration:94 - [PersistenceUnit: jpa] no META-INF/orm.xml found
16:07:01,421  INFO AnnotationBinder:94 - Binding entity from annotated class: entites.Personne
16:07:01,468  INFO EntityBinder:94 - Bind entity entites.Personne on table jpa01_hb_personne
...
16:07:01,859  INFO SettingsFactory:94 - RDBMS: MySQL, version: 5.0.41-community-nt
16:07:01,859  INFO SettingsFactory:94 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-5.0.5 ( $Date: 2007-03-01 00:01:06 +0100 (Thu, 01 Mar 2007) $, $Revision: 6329 $ )
16:07:01,890  INFO Dialect:94 - Using dialect: org.hibernate.dialect.MySQLInnoDBDialect
16:07:01,890  INFO TransactionFactoryFactory:94 - Transaction strategy: org.hibernate.ejb.transaction.JoinableCMTTransactionFactory
...
16:07:02,234  INFO SchemaExport:94 - Running hbm2ddl schema export
16:07:02,234  INFO SchemaExport:94 - exporting generated schema to database
16:07:02,343  INFO SchemaExport:94 - schema export complete
...
16:07:02,562  INFO EJBContainer:479 - STARTED EJB: dao.Dao ejbName: Dao
...
16:07:02,593  INFO EJBContainer:479 - STARTED EJB: service.Service ejbName: Service
...
[personnes]-------------------------------------------------------------------
[1,0,p1,Paul,31/01/2000,true,2]
[2,0,p2,Sylvie,05/07/2001,false,0]

Okuyucunun bu günlükleri incelemesi önerilir. Burada, Ejb3 konteynerinin ne yaptığına dair ilginç bilgiler bulunur.

3.2.7. [TestNG] birim testleri

[TestNG] programının kodu şöyledir:


package tests;

...
public class TestNG {

    // hizmet katmanı
    private IService service = null;

    @BeforeClass
    public void init() throws NamingException, ParseException {
        // günlük
        log("init");
        // konteyneri başlatıyoruz EJB3 JBoss
        // ejb3-interceptors-aop.xml ve embedded-jboss-beans.xml yapılandırma dosyaları kullanılıyor
        EJB3StandaloneBootstrap.boot(null);

        // Uygulamaya özgü bean'lerin oluşturulması
        EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

        // Sınıf yolunda bulunan tüm EJBs'leri dağıt (yavaş, tümünü tarar)
        // EJB3StandaloneBootstrap.scanClasspath();

        // Uygulamanın sınıf yolunda bulunan tüm EJB dosyaları dağıtılıyor
        EJB3StandaloneBootstrap.scanClasspath("bin".replace("/", File.separator));

        // JNDI bağlamı başlatılır. jndi.properties dosyası kullanılır
        InitialContext initialContext = new InitialContext();

        // hizmet katmanının örneği oluşturuluyor
        service = (IService) initialContext.lookup("Service/local");
        // veritabanı boşaltılıyor
        clean();
        // veritabanı dolduruluyor
        fill();
        // gözle kontrol ediliyor
        dumpPersonnes();
    }

    @AfterClass
    public void terminate() {
        // günlük
        log("terminate");
        // EJB konteynerini kapatma
        EJB3StandaloneBootstrap.shutdown();
    }

    @BeforeMethod
    public void setUp() throws ParseException {
...
    }

...
}
  • Testler için gerekli ortamı hazırlamak amacıyla kullanılan init yöntemi (satır 10-37), daha önce [InitDB]'te açıklanan kodu kullanır.
  • Testlerin sonunda yürütülen ( @AfterClass anotasyonunun varlığıyla belirlenen) terminate yöntemi (satır 40-45), Ejb3 konteynerini durdurur (satır 44).
  • Geri kalan her şey Spring sürümündekiyle aynıdır.

Testler başarılı:

Image

3.2.8. SGBD'e değiştir

SGBD'i değiştirmek için, [META-INF] [2] klasörünün içeriğini, [conf] [1] klasöründeki SGBD klasörünün içeriğiyle değiştirmeniz yeterlidir. SQL Sunucusu örneğini ele alalım:

[persistence.xml] dosyası şu şekildedir:


<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
    http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd" version="1.0">

    <persistence-unit name="jpa">

        <!-- JPA sağlayıcısı Hibernate'dir -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>

        <!-- DataSource JTA, Java ortamı tarafından yönetilir EE5 -->
        <jta-data-source>java:/datasource</jta-data-source>

        <properties>
            <!-- JBA katmanındaki varlıkların aranması -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />

            <!-- SQL Hibernate günlükleri
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->

            <!-- yönetilen SGBD türü -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />

            <!-- kalıcılık biriminin dağıtımı sırasında tüm tabloların yeniden oluşturulması (drop+create) -->
            <property name="hibernate.hbm2ddl.auto" value="create" />

        </properties>
    </persistence-unit>

</persistence>

Sadece bir satır değişmiştir:

  • 24. satır: Hibernate'in kullanması gereken SQL lehçesi

SQL Sunucusundaki [jboss-config.xml] dosyası ise şu şekildedir:


<?xml version="1.0" encoding="UTF-8"?>

<deployment xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:jboss:bean-deployer bean-deployer_1_0.xsd"
    xmlns="urn:jboss:bean-deployer:2.0">

    <!-- DataSource fabrikası -->
    <bean name="datasourceFactory" class="org.jboss.resource.adapter.jdbc.local.LocalTxDataSource">
        <!-- DataSource'in adı JNDI -->
        <property name="jndiName">java:/datasource</property>

        <!-- yönetilen veritabanı -->
        <property name="driverClass">com.microsoft.sqlserver.jdbc.SQLServerDriver</property>
        <property name="connectionURL">jdbc:sqlserver://localhost\\SQLEXPRESS:1246;databaseName=jpa</property>
        <property name="userName">jpa</property>
        <property name="password">jpa</property>

        <!-- bağlantı havuzu özellikleri -->
    ...
    </bean>

</deployment>

Sadece 12-15. satırlar değiştirilmiştir: bunlar yeni Jdbc bağlantısının özelliklerini belirtir.

Okuyucunun, MySQL5 için açıklanan testleri diğer SGBD dosyalarıyla da tekrarlaması önerilir.

3.2.9. JPA Uygulamasını Değiştirme

Yukarıda belirtildiği gibi, Jboss Ejb3 konteynerinin Toplink ile birlikte kullanıldığına dair bir örnek bulamadık. Bugüne kadar (Haziran 2007), bu yapılandırmanın mümkün olup olmadığını hâlâ bilmiyorum.

3.3. Diğer örnekler

Personne varlığıyla yapılan çalışmaları özetleyelim. Aynı testleri gerçekleştirmek için üç farklı mimari oluşturduk:

1 - Spring / Hibernate uygulaması

2 - Spring / Toplink uygulaması

3 - bir Jboss Ejb3 / Hibernate uygulaması

Öğreticideki örnekler, bu üç mimariyi, öğreticinin ilk bölümünde incelenen diğer varlıklarla birlikte ele almaktadır:

Kategori - Makale

  • [1]'te: Spring / Hibernate sürümü
  • [2]: Spring / Toplink sürümü
  • [3]: Jboss Ejb3 / Hibernate sürümü

Kişi - Adres - Faaliyet

  • [1]: Spring / Hibernate sürümü
  • [2]: Spring / Toplink sürümü
  • [3]: JBoss EJB3 / Hibernate sürümü

Bu örnekler mimari açısından yeni bir şey getirmiyor. Bunlar, aralarında bir-çok veya çok-çok ilişkileri olan yönetilmesi gereken birden fazla varlık bulunan bir bağlamda yer alıyor; Personne varlığı içeren örneklerde bu durum söz konusu değildi.

3.4. Örnek 3: Bir web uygulamasında Spring / JPA

3.4.1. Giriş

Burada, aşağıdaki belgede sunulan bir uygulamayı ele alıyoruz:

[ref4]: Java'da MVC Web Geliştirmenin Temelleri [http://tahe.developpez.com/java/baseswebmvc/].

Bu belge, Java'da MVC web geliştirmenin temellerini ele almaktadır. Aşağıdaki örneği anlayabilmek için okuyucunun bu temel bilgilere sahip olması gerekmektedir. Web uygulaması Tomcat sunucusunu kullanacaktır. Tomcat'in kurulumu ve Eclipse ortamında kullanımı 5.3. paragrafında anlatılmaktadır.

Uygulama, ilişkisel-nesne köprüsünü sağlayan Ibatis / SqlMap [http://ibatis.apache.org/] aracına dayanan bir [dao] katmanı ile geliştirilmişti. Biz sadece Ibatis'i JPA ile değiştiriyoruz. Uygulamanın mimarisi şu şekilde olacaktır:

Yazacağımız web uygulaması, dört işlemle bir grup kişiyi yönetmeye olanak sağlayacak:

  • grup üyelerinin listelenmesi
  • gruba kişi ekleme
  • gruptaki bir kişinin bilgilerinin değiştirilmesi
  • gruptan bir kişinin silinmesi

Bu dört temel işlem, bir veritabanı tablosundaki işlemlerle aynıdır. Aşağıdaki ekran görüntüler , uygulamanın kullanıcıyla karşılıklı olarak görüntülediği sayfaları göstermektedir.

 

3.4.2. Eclipse Projesi

Uygulamanın Eclipse projesi şu şekildedir:

  • [1]: web projesi. Bu, [Dynamic Web Project] [2] türünde bir Eclipse projesidir. Bu proje, öğreticinin örnekleri klasöründeki [3] klasöründe [4] adıyla bulunur. Bu projeyi içe aktaracağız.
  • [5] dosyasında: [service, dao, jpa] katmanlarının kaynakları ve yapılandırması. 3.1.1. paragrafında incelenen Eclipse projesi [hibernate-spring-personnes-metier-dao]'ten elde edilen [dao, entites, service] bilgilerini koruyoruz. Yalnızca burada [web] paketi ile temsil edilen [web] katmanını geliştiriyoruz. Ayrıca, bu projenin [persistence.xml, spring-config.xml] yapılandırma dosyalarını, SGBD Postgres'i kullanacağımız ayrıntısı dışında aynen koruyoruz; bu da [spring-config.xml] dosyasında aşağıdaki değişiklikleri gerektiriyor:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" 
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
...
                <property name="databasePlatform" value="org.hibernate.dialect.PostgreSQLDialect" />
...
        </property>
    ...
    </bean>

    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql:jpa" />
        <property name="username" value="jpa" />
        <property name="password" value="jpa" />
    </bean>
....
</beans>
  1. satır ile 16-19. satırlar Postgres'e uyarlanmıştır.
  • [6]'te: [WebContent] klasörü, projenin JSP sayfalarını ve gerekli kütüphaneleri içerir. Bu kütüphaneler, [8]'te gösterilmiştir
  • Uygulama, çeşitli SGBD sürümleriyle kullanılabilir. Bunun için [spring-config.xml] dosyasını değiştirmeniz yeterlidir. [conf] [7] klasörü, çeşitli SGBD'lere uyarlanmış [spring-config.xml] dosyasını içerir.

3.4.3. [web] katmanı

Uygulamamız aşağıdaki çok katmanlı mimariye sahiptir:

[web] katmanı, kullanıcının kişi grubunu yönetebilmesi için ekranlar sunacaktır:

  • grup üyelerinin listesi
  • gruba kişi ekleme
  • gruptaki bir kişiyi düzenleme
  • gruptan bir kişinin silinmesi

Bunun için, kendisi de [dao] katmanını kullanacak olan [service] katmanından yararlanacaktır. [web] katmanı tarafından yönetilen ekranları daha önce tanıtmıştık (3.4.1. paragraf). Web katmanını açıklamak için sırasıyla şunları ele alacağız:

  • yapılandırmasını
  • görünümleri
  • denetleyicisi
  • bazı testler

3.4.3.1. Web uygulamasının yapılandırması

Eclipse projesinin mimarisine tekrar bir göz atalım:

 
  • [web] paketinde, web uygulamasının denetleyicisi olan [Application] sınıfı bulunur.
  • Uygulamanın JSP / JSTL sayfaları, [WEB-INF/vues] içinde yer almaktadır.
  • [WEB-INF/lib] klasörü, uygulama için gerekli olan üçüncü taraf arşivlerini içerir. Bunlar, [Web App Libraries] klasöründe görülebilir.

[web.xml]


[web.xml] dosyası, web sunucusu tarafından uygulamayı yüklemek için kullanılan dosyadır. İçeriği şöyledir:


<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
    <display-name>spring-jpa-hibernate-personnes-crud</display-name>
    <!--  ServletPersonne -->
    <servlet>
        <servlet-name>personnes</servlet-name>
        <servlet-class>web.Application</servlet-class>
        <init-param>
            <param-name>urlEdit</param-name>
            <param-value>/WEB-INF/vues/edit.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlErreurs</param-name>
            <param-value>/WEB-INF/vues/erreurs.jsp</param-value>
        </init-param>
        <init-param>
            <param-name>urlList</param-name>
            <param-value>/WEB-INF/vues/list.jsp</param-value>
        </init-param>
    </servlet>
    <!--  Eşleme ServletPersonne-->
    <servlet-mapping>
        <servlet-name>personnes</servlet-name>
        <url-pattern>/do/*</url-pattern>
    </servlet-mapping>
    <!--  ana sayfalar -->
    <welcome-file-list>
        <welcome-file>index.jsp</welcome-file>
    </welcome-file-list>
    <!--  Beklenmedik hata sayfası -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/vues/exception.jsp</location>
    </error-page>
</web-app>
  • 23-26. satırlar: [/do/*] URL'leri, [personnes] servleti tarafından işlenecektir
  • 7-8. satırlar: [personnes] servleti, [Application] sınıfının bir örneğidir; bu sınıfı daha sonra oluşturacağız.
  • 9-20. satırlar: JSP sayfalarının URL'lerini tanımlayan üç [urlList, urlEdit, urlErreurs] parametresini tanımlar; bu sayfalar, [list, edit, erreurs] görünümlerine aittir.
  • 28-30. satırlar: Uygulamanın, web uygulaması klasörünün kök dizininde bulunan [index.jsp] adlı bir varsayılan giriş sayfası vardır.
  • 32-35. satırlar: Uygulamanın, web sunucusu tarafından uygulama tarafından yönetilmeyen bir istisna algılandığında görüntülenen varsayılan bir hata sayfası vardır.
    • 37. satır: <exception-type> etiketi, <error-page> yönergesi tarafından yönetilen istisna türünü belirtir; burada [java.lang.Exception] türü ve türevleri, yani tüm istisnalar.
    • 38. satır: <location> etiketi, <exception-type> ile tanımlanan türde bir istisna oluştuğunda görüntülenecek JSP sayfasını belirtir. Sayfada şu yönerge varsa, meydana gelen istisna bu sayfada exception adlı bir nesne içinde bulunur:

<%@ page isErrorPage="true" %>
  • (devam)
    • <exception-type> etiketinde T1 türü belirtilmişse ve T1'ten türetilmemiş T2 türünde bir istisna web sunucusuna ulaşırsa, sunucu müşteriye genellikle kullanıcı dostu olmayan özel bir istisna sayfası gönderir. İşte bu nedenle [web.xml] dosyasındaki <error-page> etiketinin önemi ortaya çıkmaktadır.

[index.jsp]


Bu sayfa, bir kullanıcı URL belirtmeden doğrudan uygulama bağlamını talep ettiğinde görüntülenir, c.a.d. Burada [/spring-jpa-hibernate-personnes-crud]. İçeriği şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<c:redirect url="/do/list"/>

[index.jsp], istemciyi (4. satır) [/do/list] URL'sine yönlendirir. Bu URL, gruptaki kişilerin listesini gösterir.

3.4.3.2. Uygulamanın JSP / JSTL sayfaları


[list.jsp] görünümü


Bu görünüm, kişi listesini görüntülemek için kullanılır:

Image

Kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
            <c:if test="${erreurs!=null}">
                <h3>Les erreurs suivantes se sont produites :</h3>
                <ul>
                    <c:forEach items="${erreurs}" var="erreur">
                        <li><c:out value="${erreur}"/></li>
                    </c:forEach>
                </ul>
            <hr>
        </c:if>
        <h2>Liste des personnes</h2>
        <table border="1">
            <tr>
                <th>Id</th>
                <th>Version</th>
                <th>Pr&eacute;nom</th>
                <th>Nom</th>
                <th>Date de naissance</th>
                <th>Mari&eacute;</th>
                <th>Nombre d'enfants</th>
                <th></th>
            </tr>
            <c:forEach var="personne" items="${personnes}">
                <tr>
                    <td><c:out value="${personne.id}"/></td>
                    <td><c:out value="${personne.version}"/></td>
                    <td><c:out value="${personne.prenom}"/></td>
                    <td><c:out value="${personne.nom}"/></td>
                    <td><dt:format pattern="dd/MM/yyyy">${personne.datenaissance.time}</dt:format></td>
                    <td><c:out value="${personne.marie}"/></td>
                    <td><c:out value="${personne.nbenfants}"/></td>
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  • Bu görünüm, modelinde iki öğe alır:
    • [personnes] öğesi, [List] türündeki bir nesneye ve bu nesne de [Personne] türündeki nesnelere bağlıdır: bir kişi listesi.
    • isteğe bağlı [erreurs] öğesi, [List] türündeki bir nesneye ve bu nesne de [String] türündeki nesnelere bağlıdır: bir hata mesajları listesi.
  • 31-43. satırlar: ${personnes} listesi taranarak, gruptaki kişileri içeren bir HTML tablosu görüntülenir.
  • 40. satır: [Modifier] bağlantısının işaret ettiği URL, mevcut kişinin [id] alanı tarafından ayarlanır; böylece [/do/edit] URL'siyle ilişkili denetleyici, hangi kişinin değiştirileceğini bilir.
  • 41. satır: Aynı işlem [Supprimer] bağlantısı için de yapılır.
  • 37. satır: Kişinin doğum tarihini JJ/MM/AAAA biçiminde görüntülemek için, Apache [Jakarta Taglibs] projesinin [DateTime] etiket kütüphanesindeki <dt> etiketi kullanılır:

Image

Bu etiket kütüphanesinin açıklama dosyası 3. satırda tanımlanmıştır.

  • 46. satır: Yeni bir kişi ekleme bağlantısı olan [Ajout], 40. satırdaki [Modifier] bağlantısı gibi [/do/edit] URL'sini hedeflemektedir. [id] parametresinin -1 değeri, burada bir düzenleme değil, bir ekleme işlemi yapıldığını gösterir.
  • 10-18. satırlar: ${hatalar} öğesi şablonda varsa, içindeki hata mesajları görüntülenir.

[edit.jsp] görünümü


Bu görünüm, yeni bir kişi ekleme veya mevcut bir kişiyi düzenleme formunu görüntülemek için kullanılır:

[edit.jsp] görünümünün kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ taglib uri="/WEB-INF/taglibs-datetime.tld" prefix="dt" %>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="../ressources/standard.jpg">
        <h2>Ajout/Modification d'une personne</h2>
        <c:if test="${erreurEdit!=''}">
            <h3>Echec de la mise à jour :</h3>
          L'erreur suivante s'est produite : ${erreurEdit}
            <hr>
        </c:if>
        <form method="post" action="<c:url value="/do/validate"/>">
            <table border="1">
                <tr>
                    <td>Id</td>
                    <td>${id}</td>
                </tr>
                <tr>
                    <td>Version</td>
                    <td>${version}</td>
                </tr>
                <tr>
                    <td>Pr&eacute;nom</td>
                    <td>
                        <input type="text" value="${prenom}" name="prenom" size="20">
                    </td>
                    <td>${erreurPrenom}</td>
                </tr>
                <tr>
                    <td>Nom</td>
                    <td>
                        <input type="text" value="${nom}" name="nom" size="20">
                    </td>
                    <td>${erreurNom}</td>
                </tr>
                <tr>
                <td>Date de naissance (JJ/MM/AAAA)</td>
                    <td>
                        <input type="text" value="${datenaissance}" name="datenaissance">
                    </td>
                    <td>${erreurDateNaissance}</td>
                </tr>
                <tr>
                    <td>Mari&eacute;</td>
                    <td>
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
                    </td>
                </tr>
                <tr>
                    <td>Nombre d'enfants</td>
                    <td>
                        <input type="text" value="${nbenfants}" name="nbenfants">
                    </td>
                    <td>${erreurNbEnfants}</td>
                </tr>
            </table>
            <br>
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>
    </body>
</html>

Bu görünüm, yeni bir kişi ekleme veya mevcut bir kişiyi güncelleme formunu gösterir. Bundan sonra, yazımı basitleştirmek amacıyla tek bir terim olan [mise à jour]'i kullanacağız. [Valider] düğmesi (73. satır), formun POST işlemini [/do/validate] URL'sinde (16. satır) tetikler. POST işlemi başarısız olursa, [edit.jsp] görünümü, meydana gelen hata veya hatalarla birlikte yeniden görüntülenir; aksi takdirde [list.jsp] görünümü görüntülenir.

  • Hem GET hem de başarısız olan POST üzerinde görüntülenen [edit.jsp] görünümü, şablonunda aşağıdaki öğeleri alır:
öznitelik
GET
POST
id
güncellenen kişinin
güncellenen
aynı
version
sürümü
aynı
prenom
ad
girilen adı
nom
soyadı
girdilen soyadı
datenaissance
doğum tarihi
girdilen doğum tarihi
marie
medeni durumu
girilen medeni durumu
nbenfants
çocuk sayısı
girilen çocuk sayısı
erreurEdit
boş
ekleme
veya değiştirme işleminin başarısız olduğunu belirten bir hata mesajı
boş. Hata yoksa boş.
erreurPrenom
boş
yanlış bir isim olduğunu bildirir – aksi takdirde boştur
erreurNom
boş
hatalı soyad belirtir – aksi takdirde boş
erreurDateNaissance
boş
hatalı doğum tarihi bildirir – aksi takdirde boş bırakılır
erreurNbEnfants
boş
hatalı çocuk sayısı bildirildi – aksi takdirde boş bırakılır
  • 11-15. satırlar: Formdaki POST işlemi başarısız olursa, [erreurEdit!=''] değeri alınır ve bir hata mesajı görüntülenir.
  • satır 16: form, [/do/validate] URL'sine gönderilecektir
  • 20. satır: Şablondaki [id] öğesi görüntülenir
  • 24. satır: Şablondaki [version] öğesi görüntülenir
  • 26-32. satırlar: Kişinin adının girilmesi:
    • formun ilk görüntülenişinde (GET), ${prenom}, güncellenen [Personne] nesnesinin [prenom] alanının geçerli değerini gösterir ve ${erreurPrenom} boştur.
    • POST'ten sonra bir hata oluşması durumunda, girilen ${prenom} değeri ve varsa hata mesajı ${erreurPrenom} yeniden görüntülenir
  • 33-39. satırlar: kişinin soyadının girilmesi
  • satır 40-46: kişinin doğum tarihinin girilmesi
  • satır 47-61: Kişinin medeni durumunun bir radyo düğmesi ile girilmesi. Hangi radyo düğmesinin işaretlenmesi gerektiğini belirlemek için [Personne] nesnesinin [marie] alanındaki değer kullanılır.
  • satır 62-68: kişinin çocuk sayısının girilmesi
  • satır 71: HTML adlı gizli bir alan; bu alanın adı [id]'tir ve değeri, güncellenmekte olan kişinin [id] alanının değeridir; ekleme için -1, değişiklik için başka bir değer alır.
  • satır 72: HTML adlı gizli bir alan; adı [version] olup, değeri güncellenmekte olan kişinin [id] alanının değeridir.
  • 73. satır: Formdaki [Submit] türündeki [Valider] düğmesi
  • 74. satır: Kişi listesine geri dönmeyi sağlayan bir bağlantı. Formu onaylamadan çıkmaya olanak tanıdığı için [Annuler] olarak adlandırılmıştır.

[exception.jsp] görünümü


Bu görünüm, uygulama tarafından yönetilmeyen ve web sunucusuna iletilen bir istisna oluştuğunu bildiren bir sayfayı görüntülemek için kullanılır.

Örneğin, grupta bulunmayan bir kişiyi silelim:

[exception.jsp] görünümünün kodu şöyledir:


<%@ page language="java" pageEncoding="ISO-8859-1" contentType="text/html;charset=ISO-8859-1"%>
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>
<%@ page isErrorPage="true" %>

<%
  response.setStatus(200);
%>

<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
        <h2>MVC - personnes</h2>
        L'exception suivante s'est produite :
        <%= exception.getMessage()%>
        <br><br>
        <a href="<c:url value="/do/list"/>">Retour &agrave; la liste</a>
    </body>
</html>
  • Bu görünüm, modelinde [exception] öğesini içeren bir anahtar alır; bu, web sunucusu tarafından yakalanan istisnadır. Bu öğenin web sunucusu tarafından JSP sayfasının modeline dahil edilebilmesi için, sayfanın 3. satırdaki etiketini tanımlamış olması gerekir.
  • 6. satır: Yanıtın durum kodu HTTP, 200 olarak belirlenir. Bu, yanıtın ilk başlığı olan HTTP'tir. 200 kodu, istemciye isteğinin yerine getirildiğini bildirir. Genellikle, sunucunun yanıtına bir HTML belgesi eklenir. Burada da durum böyledir. Yanıtın HTTP durum kodu 200 olarak ayarlanmazsa, burada 500 değerini alır ve bu da bir hata oluştuğunu gösterir. Nitekim, yönetilmeyen bir istisna yakalayan web sunucusu bu durumu anormal olarak değerlendirir ve 500 koduyla bildirir. HTTP 500 koduna verilen tepki, tarayıcılara göre farklılık gösterir: Firefox, bu yanıtla birlikte gelebilecek HTML belgesini görüntülerken, IE bu belgeyi yok sayar ve kendi sayfasını görüntüler. Bu nedenle 500 kodunu 200 koduyla değiştirdik.
  • 16. satır: istisna metni görüntülenir
  • 18. satır: Kullanıcıya kişi listesine geri dönmesi için bir bağlantı sunulur

[erreurs.jsp] görünümü


Bu görünüm, uygulamanın başlatılmasında meydana gelen hataları bildiren bir sayfayı (c.a.d) ve denetleyici servletinin [init] yönteminin yürütülmesi sırasında tespit edilen hataları görüntülemek için kullanılır. Bu, örneğin aşağıdaki örnekte gösterildiği gibi [web.xml] dosyasında bir parametrenin eksik olması olabilir:

Image

[erreurs.jsp] sayfasının kodu şöyledir:


<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<%@ taglib uri="/WEB-INF/c.tld" prefix="c" %>

<html>
    <head>
      <title>MVC - Personnes</title>
  </head>
  <body>
      <h2>Les erreurs suivantes se sont produites</h2>
    <ul>
            <c:forEach var="erreur" items="${erreurs}">
                <li>${erreur}</li>
            </c:forEach>
    </ul>
  </body>
</html>

Sayfa, şablonunda [erreurs] öğesini alır; bu öğe, [ArrayList] türünde bir nesnedir ve [String] nesnelerinden oluşur; bu nesneler ise hata mesajlarıdır. Bunlar, 13-15. satırlardaki döngü ile görüntülenir.

3.4.3.3. Uygulama denetleyicisi

[Application] denetleyicisi, [web] paketinde tanımlanmıştır:

Image


yapısı ve denetleyicinin başlatılması


[Application] denetleyicisinin iskeleti şu şekildedir:


package web;

...


@SuppressWarnings("serial")
public class Application extends HttpServlet {
    // örnek ayarları
    private String urlErreurs = null;
    private ArrayList erreursInitialisation = new ArrayList<String>();
    private String[] paramètres = { "urlList", "urlEdit", "urlErreurs" };
    private Map params = new HashMap<String, String>();

    // hizmet
    private IService service = null;

    // başlatma
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        // servletin başlatma parametreleri alınır
        ServletConfig config = getServletConfig();
        // diğer başlatma parametreleri işleniyor
        String valeur = null;
        for (int i = 0; i < paramètres.length; i++) {
            // parametre değeri
            valeur = config.getInitParameter(paramètres[i]);
            // parametre mevcut mu?
            if (valeur == null) {
                // hata kaydedilir
                erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
            } else {
                // parametrenin değeri kaydedilir
                params.put(paramètres[i], valeur);
            }
        }
        // [erreurs] görünümünün URL'si özel bir işleme tabi tutulur
        urlErreurs = config.getInitParameter("urlErreurs");
        if (urlErreurs == null)
            throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
        // uygulama yapılandırması
        ApplicationContext ctx = new ClassPathXmlApplicationContext("spring-config.xml");
        // hizmet katmanı
        service = (IService) ctx.getBean("service");
        // veritabanı boşaltılır
        clean();
        // veritabanını dolduruyoruz
        try {
            fill();
        } catch (ParseException e) {
            throw new ServletException(e);
        }
    }

    // tablo doldurma
    public void fill() throws ParseException {
        // kullanıcı oluşturma
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // kaydediliyor
        service.saveArray(new Personne[] { p1, p2 });
    }

    // tablodaki öğeleri silme
    public void clean() {
        for (Personne p : service.getAll()) {
            service.deleteOne(p.getId());
        }
    }

    // GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
...
    }

    // kişi listesini görüntüleme
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // kişi düzenleme / ekleme
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // bir kişinin silinmesi
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // kişi düzenleme / ekleme işleminin onaylanması
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
...
    }

    // önceden doldurulmuş formun görüntülenmesi
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
    ...
    }

    // gönderme
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // işlem GET'e devrediliyor
        doGet(request, response);
    }

}
  • 21-34. satırlar: [web.xml] dosyasında beklenen parametreler alınır.
  • 37-39. satırlar: [urlErreurs] parametresi mutlaka bulunmalıdır, çünkü bu parametre, olası başlatma hatalarını görüntüleyebilen [erreurs] görünümünün URL’sini belirtir. Bu parametre mevcut değilse, [ServletException] komutu çalıştırılarak uygulama durdurulur (satır 39). Bu istisna web sunucusuna iletilir ve [web.xml] dosyasındaki <error-page> etiketiyle yönetilir. Böylece [exception.jsp] görünümü görüntülenir:

Image

Yukarıdaki [Retour à la liste] bağlantısı çalışmaz. Uygulama değiştirilip yeniden yüklenene kadar bu bağlantı kullanıldığında aynı yanıt verilir. Daha önce gördüğümüz gibi, bu bağlantı diğer istisna türleri için de yararlıdır.

  • 40-43. satırlar: Spring yapılandırma dosyasını kullanarak [service] katmanına ilişkin bir referans alır. Denetleyicinin başlatılmasından sonra, denetleyicinin yöntemleri, [service] katmanında (satır 15) bir [service] referansına sahip olur ve bu referansı, kullanıcı tarafından talep edilen eylemleri yürütmek için kullanırlar. Bu eylemler, [doGet] yöntemi tarafından yakalanacak ve denetleyicinin özel bir yöntemi tarafından işlenecektir:
Url
HTTP yöntemi
denetleyici yöntemi
/do/list
GET
doListPersonnes
/do/edit
GET
doEditPersonne
/do/validate
POST
doValidatePersonne
/do/delete
GET
doDeletePersonne

[doGet] yöntemi


Bu yöntemin amacı, kullanıcı tarafından talep edilen işlemlerin doğru yönteme yönlendirilmesini sağlamaktır. Kod şöyledir:


// GET
    @SuppressWarnings("unchecked")
    public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {

        
// servletin başlatılmasının nasıl gerçekleştiği kontrol ediliyor
        if (erreursInitialisation.size() != 0) {
            // hata sayfasına yönlendiriliyor
            request.setAttribute("erreurs", erreursInitialisation);
            getServletContext().getRequestDispatcher(urlErreurs).forward(request, response);
            // son
            return;
        }
        // isteği gönderen yöntemi alınır
        String méthode = request.getMethod().toLowerCase();
        // yürütülecek eylemi alıyoruz
        String action = request.getPathInfo();
        // eylem?
        if (action == null) {
            action = "/list";
        }
        // eylemin yürütülmesi
        if (méthode.equals("get") && action.equals("/list")) {
            // kişi listesi
            doListPersonnes(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/delete")) {
            // bir kişinin silinmesi
            doDeletePersonne(request, response);
            return;
        }
        if (méthode.equals("get") && action.equals("/edit")) {
            // kişi ekleme/değiştirme formunun görüntülenmesi
            doEditPersonne(request, response);
            return;
        }
        if (méthode.equals("post") && action.equals("/validate")) {
            // kişi ekleme/değiştirme formunun onaylanması
            doValidatePersonne(request, response);
            return;
        }
        // diğer durumlar
        doListPersonnes(request, response);
    }
  • 7-13. satırlar: Başlatma hataları listesinin boş olup olmadığı kontrol edilir. Boş değilse, hatayı veya hataları bildirecek olan [erreurs(erreurs)] görünümü görüntülenir.
  • 15. satır: Müşterinin isteğini yapmak için kullandığı [get] veya [post] yöntemini alırız.
  • 17. satır: İsteğin [action] parametresinin değeri alınır.
  • 23-27. satırlar: Kişi listesini isteyen [GET /do/list] isteğinin işlenmesi.
  • 28-32. satırlar: Bir kişinin silinmesini talep eden [GET /do/delete] isteğinin işlenmesi.
  • satır 33-37: Bir kişinin güncelleme formunu talep eden [GET /do/edit] isteğinin işlenmesi.
  • satır 38-42: Güncellenen kişinin onaylanmasını talep eden [POST /do/validate] isteğinin işlenmesi.
  • 44. satır: İstenen eylem önceki beş eylemden biri değilse, o zaman [GET /do/list] gibi işlem yapılır.

[doListPersonnes] yöntemi


Bu yöntem, kişi listesini talep eden [GET /do/list] isteğini işler:

Image

Kodu şu şekildedir:


    // kişi listesinin görüntülenmesi
    private void doListPersonnes(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // [list] görünüm şablonu
        request.setAttribute("personnes", service.getAll());
        // [list] görünümünün görüntülenmesi
        getServletContext().getRequestDispatcher((String) params.get("urlList")).forward(request, response);
}
  • 4. satır: [service] katmanından gruptaki kişilerin listesi istenir ve bu liste, şablonda "kişiler" anahtarı altında yerleştirilir.
  • 6. satır: 3.4.3.2. paragrafında açıklanan [list.jsp] görünümünü görüntülenir.

[doDeletePersonne] yöntemi


Bu yöntem, id=XX olan kişinin silinmesini talep eden [GET /do/delete?id=XX] sorgusunu işler. [/do/delete?id=XX] URL'si, [list.jsp] görünümündeki [Supprimer] bağlantılarına aittir:

Image

ve kodu şöyledir:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  1. satırda, [Supprimer] bağlantısının [/do/delete?id=XX] URL'si görülmektedir. Bu URL'yi işlemekle görevli [doDeletePersonne] yöntemi, id=XX olan kişiyi silmeli ve ardından grubun yeni kişi listesini görüntülemelidir. Kod şu şekildedir:

// bir kişinin silinmesi
    private void doDeletePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // kişinin kimliğini alıyoruz
        int id = Integer.parseInt(request.getParameter("id"));
        // kişiyi siliyoruz
        service.deleteOne(id);
        // kişi listesine yönlendirme
        response.sendRedirect("list");
    }
  • 4. satır: İşlenen URL, [/do/delete?id=XX] biçimindedir. [id] parametresinden [XX] değeri alınır.
  • 6. satır: [service] katmanından, elde edilen kimliğe sahip kişinin silinmesi istenir. Herhangi bir doğrulama yapmıyoruz. Silinmesi istenen kişi mevcut değilse, [dao] katmanı bir istisna oluşturur ve bu istisna [service] katmanı tarafından üst katmana iletilir. Bunu denetleyicide de yönetmiyoruz. Dolayısıyla istisna, web sunucusuna kadar iletilir ve yapılandırma gereği, 3.4.3.2 numaralı paragrafta açıklanan [exception.jsp] sayfası görüntülenir:

Image

  • 9. satır: Silme işlemi gerçekleştiyse (istisna yoksa), istemciden [list] göreceli URL'sine yönlendirilmesi istenir. Az önce işlenen sayfa [/do/delete] olduğu için, yönlendirme URL'si [/do/list] olacaktır. Böylece tarayıcı, kişi listesinin görüntülenmesini sağlayacak olan [GET /do/list] sayfasına yönlendirilecektir.

[doEditPersonne] yöntemi


Bu yöntem, id=XX olan kişinin güncelleme formunu talep eden [GET /do/edit?id=XX] isteğini işler. [/do/edit?id=XX] URL'si, [Modifier] bağlantılarının ve [list.jsp] görünümündeki [Ajout] bağlantısının URL'sidir:

Image

ve kodu şöyledir:


...
<html>
    <head>
        <title>MVC - Personnes</title>
    </head>
    <body background="<c:url value="/ressources/standard.jpg"/>">
...
            <c:forEach var="personne" items="${personnes}">
                <tr>
...
                    <td><a href="<c:url value="/do/edit?id=${personne.id}"/>">Modifier</a></td>
                    <td><a href="<c:url value="/do/delete?id=${personne.id}"/>">Supprimer</a></td>
                </tr>
            </c:forEach>
        </table>
        <br>
        <a href="<c:url value="/do/edit?id=-1"/>">Ajout</a>
    </body>
</html>
  1. satırda, [Modifier] bağlantısının [/do/edit?id=XX] URL'si ve 17. satırda, [Ajout] bağlantısının [/do/edit?id=-1] URL'si görülmektedir. [doEditPersonne] yöntemi, id=XX olan kişinin düzenleme formunu görüntülemeli veya yeni bir giriş söz konusuysa boş bir form sunmalıdır.
  • Yukarıdaki [1], ekleme formunu; [2] ise düzenleme formunu gösterir.

[doEditPersonne] yönteminin kodu şöyledir:


// bir kişinin bilgilerinin değiştirilmesi / eklenmesi
    private void doEditPersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // kişinin kimliğini alır
        int id = Integer.parseInt(request.getParameter("id"));
        // Ekleme mi, düzenleme mi?
        Personne personne = null;
        if (id != -1) {
            // değişiklik - değiştirilecek kişi alınır
            personne = service.getOne(id);
            request.setAttribute("id", personne.getId());
            request.setAttribute("version", personne.getVersion());
        } else {
            // ekleme - boş bir kişi oluşturulur
            personne = new Personne();
            request.setAttribute("id", -1);
            request.setAttribute("version", -1);
        }
        // [Personne] nesnesini kullanıcının oturumuna eklenir
        request.getSession().setAttribute("personne", personne);
        // ve [edit] görünüm şablonuna
        request.setAttribute("erreurEdit", "");
        request.setAttribute("prenom", personne.getPrenom());
        request.setAttribute("nom", personne.getNom());
        Date dateNaissance = personne.getDatenaissance();
        if (dateNaissance != null) {
            request.setAttribute("datenaissance", new SimpleDateFormat("dd/MM/yyyy").format(dateNaissance));
        } else {
            request.setAttribute("datenaissance", "");
        }
        request.setAttribute("marie", personne.isMarie());
        request.setAttribute("nbenfants", personne.getNbenfants());
        // [edit] görünümünün görüntülenmesi
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • GET, [/do/edit?id=XX] türünde bir URL’yi hedefler. 4. satırda, [id] değerini alıyoruz. Ardından iki durum söz konusudur:
    1. id, -1'den farklıysa, bu bir düzenleme işlemidir ve düzenlenecek kişinin bilgileriyle önceden doldurulmuş bir form görüntülenmelidir. 9. satırda, bu kişi [service] katmanından istenir.
    2. id değeri -1'e eşittir. Bu durumda bir ekleme söz konusudur ve boş bir form görüntülenmelidir. Bunun için 14. satırda boş bir kişi oluşturulur.
    3. Her iki durumda da, 3.4.3.2 numaralı paragrafta açıklanan [edit.jsp] sayfa şablonundaki [id, version] öğeleri başlatılır.
  • Elde edilen [Personne] nesnesi, [edit.jsp] sayfa şablonuna yerleştirilir. Bu şablon, [erreurEdit, id, version, prenom, erreurPrenom, nom, erreurNom, datenaissance, erreurDateNaissance, marie, nbenfants, erreurNbEnfants] öğelerini içerir. Bu öğeler, değeri boş dize olan [erreurPrenom, erreurNom, erreurDateNaissance, erreurNbEnfants] hariç olmak üzere 19-31. satırlarda başlatılır. Şablonda bulunmadıkları takdirde, JSTL kütüphanesinin bunların değeri olarak boş bir dize göstereceği bilinmektedir. [erreurEdit] öğesinin değeri de boş bir dize olsa da, [edit.jsp] sayfasında değeri üzerinde bir test yapıldığı için yine de başlatılır.
  • Şablon hazır hale geldiğinde, kontrol [edit.jsp] sayfasının 33. satırına geçer ve bu satır [edit] görünümünü oluşturur.

[doValidatePersonne] yöntemi


Bu yöntem, güncelleme formunu doğrulayan [POST /do/validate] isteğini işler. Bu POST, [Valider] düğmesi tarafından tetiklenir:

Image

Yukarıdaki görünümdeki HTML formundaki giriş alanlarını hatırlayalım:


<form method="post" action="<c:url value="/do/validate"/>">
...
                        <input type="text" value="${nom}" name="nom" size="20">
...
                        <input type="text" value="${datenaissance}" name="datenaissance">
...
                        <c:choose>
                            <c:when test="${marie}">
                                <input type="radio" name="marie" value="true" checked>Oui
                                <input type="radio" name="marie" value="false">Non
                            </c:when>
                            <c:otherwise>
                                <input type="radio" name="marie" value="true">Oui
                                <input type="radio" name="marie" value="false" checked>Non
                            </c:otherwise>
                        </c:choose>
...
                        <input type="text" value="${nbenfants}" name="nbenfants">
...
            <input type="hidden" value="${id}" name="id">
      <input type="hidden" value="${version}" name="version">
            <input type="submit" value="Valider">
            <a href="<c:url value="/do/list"/>">Annuler</a>
        </form>

POST isteği, [prenom, nom, datenaissance, marie, nbenfants, id] parametrelerini içerir ve [/do/validate] URL'sine gönderilir (1. satır). Bu istek, aşağıdaki [doValidatePersonne] yöntemi tarafından işlenir:


// kişi düzenleme / ekleme onayı
    public void doValidatePersonne(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // gönderilen öğeleri alıyoruz
        boolean formulaireErroné = false;
        boolean erreur;
        // ad
        String prenom = request.getParameter("prenom").trim();
        // ad geçerli mi?
        if (prenom.length() == 0) {
            // hatayı not ediyoruz
            request.setAttribute("erreurPrenom", "Le prénom est obligatoire");
            formulaireErroné = true;
        }
        // soyadı
        String nom = request.getParameter("nom").trim();
        // İsim geçerli mi?
        if (nom.length() == 0) {
            // hata kaydediliyor
            request.setAttribute("erreurNom", "Le nom est obligatoire");
            formulaireErroné = true;
        }
        // doğum tarihi
        Date datenaissance = null;
        try {
            datenaissance = new SimpleDateFormat("dd/MM/yyyy").parse(request.getParameter("datenaissance").trim());
        } catch (ParseException e) {
            // hata kaydedildi
            request.setAttribute("erreurDateNaissance", "Date incorrecte");
            formulaireErroné = true;
        }
        // medeni durum
        boolean marie = Boolean.parseBoolean(request.getParameter("marie").trim());
        // çocuk sayısı
        int nbenfants = 0;
        erreur = false;
        try {
            nbenfants = Integer.parseInt(request.getParameter("nbenfants").trim());
            if (nbenfants < 0) {
                erreur = true;
            }
        } catch (NumberFormatException ex) {
            // hatayı not edin
            erreur = true;
        }
        // çocuk sayısı yanlış mı?
        if (erreur) {
            // hata bildiriliyor
            request.setAttribute("erreurNbEnfants", "Nombre d'enfants incorrect");
            formulaireErroné = true;
        }
        // kişinin kimlik numarası
        int id = Integer.parseInt(request.getParameter("id"));
        // Formda hata mı var?
        if (formulaireErroné) {
            // Form, hata mesajlarıyla birlikte yeniden görüntüleniyor
            showFormulaire(request, response, "");
            // işlem tamamlandı
            return;
        }
        // form doğru - oturumda bulunan kişinin bilgileri güncelleniyor
        // müşteri tarafından gönderilen bilgilerle
        Personne personne = (Personne)request.getSession().getAttribute("personne");
        personne.setDatenaissance(datenaissance);
        personne.setMarie(marie);
        personne.setNbenfants(nbenfants);
        personne.setNom(nom);
        personne.setPrenom(prenom);
        // kalıcılık
        try {
            if (id == -1) {
                // oluşturma
                service.saveOne(personne);
            } else {
                // güncelleme
                service.updateOne(personne);
            }
        } catch (DaoException ex) {
            // meydana gelen hata mesajıyla birlikte form yeniden görüntülenir
            showFormulaire(request, response, ex.getMessage());
            // bitti
            return;
        }
        // kişi listesine yönlendirme
        response.sendRedirect("list");
    }

    // önceden doldurulmuş form görüntülenir
    private void showFormulaire(HttpServletRequest request, HttpServletResponse response, String erreurEdit) throws ServletException, IOException {
        // [edit] görünüm şablonu hazırlanır
        request.setAttribute("erreurEdit", erreurEdit);
        request.setAttribute("id", request.getParameter("id"));
        request.setAttribute("version", request.getParameter("version"));
        request.setAttribute("prenom", request.getParameter("prenom").trim());
        request.setAttribute("nom", request.getParameter("nom").trim());
        request.setAttribute("datenaissance", request.getParameter("datenaissance").trim());
        request.setAttribute("marie", request.getParameter("marie"));
        request.setAttribute("nbenfants", request.getParameter("nbenfants").trim());
        // [edit] görünümünün görüntülenmesi
        getServletContext().getRequestDispatcher((String) params.get("urlEdit")).forward(request, response);
    }
  • 7-13. satırlar: POST isteğinin [prenom] parametresi alınır ve geçerliliği kontrol edilir. Yanlış olduğu tespit edilirse, [erreurPrenom] öğesi bir hata mesajıyla başlatılır ve isteğin özniteliklerine eklenir.
  • 15-21. satırlar: [nom] parametresi için de benzer şekilde işlem yapılır
  • 23-30. satırlar: [datenaissance] parametresi için de benzer şekilde işlem yapılır
  • 32. satır: [marie] parametresi alınır. Geçerliliği kontrol edilmez, çünkü ilk bakışta bir radyo düğmesinin değerinden geldiği varsayılır. Bununla birlikte, bir programın [POST /.../do/validate] parametresini, uydurma bir [marie] parametresiyle birlikte oluşturmasını engelleyen hiçbir şey yoktur. Bu nedenle bu parametrenin geçerliliğini test etmeliyiz. Burada, denetleyici bunları kendisi yönetemediği takdirde [exception.jsp] sayfasının görüntülenmesini sağlayan istisna yönetimine güveniyoruz. Dolayısıyla, 32. satırda [marie] parametresinin boole değerine dönüştürülmesi başarısız olursa, bir istisna ortaya çıkacak ve bu da [exception.jsp] sayfasının müşteriye gönderilmesine yol açacaktır. Bu işleyiş bizim için uygundur.
  • 34-50. satırlar: [nbenfants] parametresini alır ve değerini kontrol ederiz.
  • 52. satır: [id] parametresini, değerini kontrol etmeden alınır
  • 54-59. satırlar: Form hatalıysa, daha önce oluşturulan hata mesajlarıyla birlikte yeniden görüntülenir
  • 62-67. satırlar: Form geçerliyse, form öğeleriyle yeni bir [Personne] nesnesi oluşturulur
  • 69-82. satırlar: Kişi kaydedilir. Kaydetme işlemi başarısız olabilir. Çoklu kullanıcı ortamında, değiştirilecek kişi silinmiş veya başka biri tarafından zaten değiştirilmiş olabilir. Bu durumda, [dao] katmanı bir istisna oluşturur ve bu istisna burada yönetilir.
  • 84. satır: İstisna oluşmamışsa, müşteriyi grubun yeni durumunu göstermek üzere [/do/list] URL’sine yönlendiririz.
  • 79. satır: Kaydetme sırasında bir istisna oluştuysa, istisnaya ait hata mesajını (3. parametre) ileterek ilk formun yeniden görüntülenmesini talep ederiz.

[showFormulaire] yöntemi (satır 88-97), girilen değerlerle [edit.jsp] sayfası için gerekli şablonu oluşturur (request.getParameter(" ... ")). Hata mesajlarının daha önce [doValidatePersonne] yöntemi tarafından şablona yerleştirildiğini hatırlayalım. [edit.jsp] sayfası 99. satırda görüntülenir.

3.4.4. Web uygulamasının testleri

3.4.1 numaralı paragrafta bir dizi test sunulmuştur. Okuyucuyu bu testleri tekrar yapmaya davet ediyoruz. Burada, çoklu kullanıcı ortamında veri erişim çakışmaları durumlarını gösteren başka ekran görüntüleri sunuyoruz:

[Firefox], U1 kullanıcısının tarayıcısı olacaktır. Bu kullanıcı, [http://localhost:8080/spring-jpa-hibernate-personnes-crud/do/list] URL’sini talep eder:

Image

[IE7], U2 kullanıcısının tarayıcısı olacaktır. Bu kullanıcı aynı URL'yi ister:

Image

U1 kullanıcısı, [p2] kişisinin düzenleme sayfasına girer:

Image

U2 kullanıcısı da aynısını yapar:

Image

U1 kullanıcısı değişiklikleri yapar ve onaylar:

U2 kullanıcısı da aynısını yapıyor:

U2 kullanıcısı, formdaki [Retour à la liste] bağlantısını kullanarak kişi listesine geri döner:

Image

U1 tarafından değiştirilen haliyle (evli, 2 çocuklu) [Lemarchand] adlı kişiyi bulur. p2'nin sürüm numarası değişti. Artık U2, [p2]'i siliyor:

U1 hâlâ kendi listesine sahiptir ve [p2]'i yeniden değiştirmek istemektedir:

U1, ne olduğunu görmek için [Retour à la liste] bağlantısını kullanır:

Image

Gerçekten de [p2]'in artık listede olmadığını fark eder...

3.4.5. Sürüm 2

Önceki sürümü, [service, dao, jpa] katmanlarının kaynak kodlarını değil, arşivlerini kullanmak üzere biraz değiştiriyoruz:

  • [1]: yeni Eclipse projesi. [service, dao, entites] paketlerinin ortadan kalktığına dikkat edilmelidir. Bunlar, [service-dao-jpa-personne.jar] ve [2] arşivlerine kapsüllenmiş ve [WEB-INF/lib] içine yerleştirilmiştir.
  • Proje dosyası [4]'tedir. Bunu içe aktaracağız.

Yapılacak başka bir şey yok. Yeni web uygulaması başlatıldığında ve kişi listesi istendiğinde, şu yanıt alınır:

 

Hibernate, [Personne] varlığını bulamıyor. Bu sorunu çözmek için, [persistence.xml] içinde yönetilen varlıkları açıkça tanımlamak gerekiyor:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
    xmlns="http://java.sun.com/xml/ns/persistence"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <class>entites.Personne</class>
    </persistence-unit>
</persistence>
  • 7. satır: Personne varlığı tanımlanmıştır.

Bu yapıldıktan sonra istisna ortadan kalkar:

 

3.4.6. JPA uygulamasını değiştirin

  • [1] olarak değiştirin: yeni Eclipse projesi
  • [2]: Toplink kütüphaneleri, Hibernate kütüphanelerinin yerini aldı
  • proje klasörü [4]'tedir. Bunu içe aktaracağız.

JPA uygulamasını değiştirmek, yalnızca [spring-config.xml] dosyasında birkaç değişiklik gerektirir. Başka hiçbir şey değişmez. [spring-config.xml] dosyasında yapılan değişiklikler 3.1.9 numaralı paragrafta açıklanmıştır:


<?xml version="1.0" encoding="UTF-8"?>

<!-- JVM, -javaagent:C:\data\2006-2007\eclipse\dvp-jpa\lib\spring\spring-agent.jar 
    (à remplacer par le chemin exact de spring-agent.jar)-->

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
...
    <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter">
            <bean class="org.springframework.orm.jpa.vendor.TopLinkJpaVendorAdapter">
...    
            <property name="databasePlatform" value="oracle.toplink.essentials.platform.database.MySQL4Platform" />
...
    </bean>
...
</beans>

Hibernate'ten Toplink'e geçmek için birkaç satırın değiştirilmesi gerekiyor:

  • 11. satır: JPA uygulaması artık Toplink tarafından gerçekleştirilmektedir
  • 13. satır: [databasePlatform] özelliğinin değeri, Hibernate'tekinden farklıdır: Toplink'e özgü bir sınıf adıdır. Bu adın nerede bulunacağı, 2.1.15.2. paragrafında açıklanmıştır.

Hepsi bu kadar. Spring ile SGBD veya JPA uygulamasını ne kadar kolay değiştirebileceğimize dikkat edin. Yine de henüz tam olarak bitmiş sayılmayız. Uygulamayı çalıştırdığımızda bir istisna ile karşılaşıyoruz:

 

Bu, 3.1.9. paragrafında anlatılan ve daha önce karşılaştığımız bir sorundur. Bu sorun, JVM'i bir Spring ajanı ile çalıştırarak çözülür. Bunun için Tomcat'in başlatma yapılandırmasını şu şekilde değiştiririz:

  • [1] olarak değiştirilir: Tomcat yapılandırmasını değiştirmek için [Run / Run...] seçeneği seçilmiştir
  • [2] olarak: [Arguments] sekmesini seçtik
  • [3]'te: 3.1.9. paragrafında açıklandığı şekilde -javaagent parametresini ekledik.

Bu işlem tamamlandıktan sonra, kişi listesini isteyebiliriz:

Image

3.5. Diğer örnekler

Paragraf 3.2'de incelenen Jboss Ejb3 konteynerinin Spring konteynerinin yerine kullanıldığı bir web örneği göstermek isterdik:

  • [1]: Eclipse projesi
  • [3]: örnekler klasöründeki konumu. Bunu içe aktaracağız.

3.2. paragrafta açıklanan [jboss-config.xml, persistence.xml] yapılandırmasını kullandık, ardından [Application.java] denetleyicisinin [init] yöntemini şu şekilde değiştirdik:


// init
    @SuppressWarnings("unchecked")
    public void init() throws ServletException {
        try {
            // servletin başlatma parametreleri alınır
            ServletConfig config = getServletConfig();
            // diğer başlatma parametreleri işlenir
            String valeur = null;
            for (int i = 0; i < paramètres.length; i++) {
                // parametre değeri
                valeur = config.getInitParameter(paramètres[i]);
                // parametre mevcut mu?
                if (valeur == null) {
                    // hata kaydedilir
                    erreursInitialisation.add("Le paramètre [" + paramètres[i] + "] n'a pas été initialisé");
                } else {
                    // parametrenin değeri kaydedilir
                    params.put(paramètres[i], valeur);
                }
            }
            // [erreurs] görünümünün URL'si özel bir işleme tabi tutulur
            urlErreurs = config.getInitParameter("urlErreurs");
            if (urlErreurs == null)
                throw new ServletException("Le paramètre [urlErreurs] n'a pas été initialisé");
            // uygulama yapılandırması
            // EJB3 JBoss konteyneri başlatılır
            // ejb3-interceptors-aop.xml ve embedded-jboss-beans.xml yapılandırma dosyaları kullanılıyor
            EJB3StandaloneBootstrap.boot(null);

            // Uygulamaya özgü bean'lerin oluşturulması
            EJB3StandaloneBootstrap.deployXmlResource("META-INF/jboss-config.xml");

            // Uygulamanın sınıf yolunda bulunan tüm EJB dosyaları dağıtılıyor
            //EJB3StandaloneBootstrap.scanClasspath("WEB-INF/classes".replace("/", File.separator));
            EJB3StandaloneBootstrap.scanClasspath();

            // JNDI bağlamı başlatılıyor. jndi.properties dosyası kullanılıyor
            InitialContext initialContext = new InitialContext();

            // hizmet katmanının örneği oluşturuluyor
            service = (IService) initialContext.lookup("Service/local");
            // veritabanı boşaltılıyor
            clean();
            // veritabanı dolduruluyor
            fill();
        } catch (Exception e) {
            throw new ServletException(e);
        }
    }
  • 28-38. satırlar: Ejb3 konteyneri başlatılır. Bu konteyner, Spring konteynerinin yerini alır.
  • 41. satır: Uygulamanın [service] katmanından bir referans istenir.

İlk bakışta yapılması gereken değişiklikler bunlarla sınırlıdır. Çalıştırma sırasında şu hata ile karşılaşılır:

 

Sorunun tam olarak nerede olduğunu anlayamadım. Tomcat tarafından bildirilen istisna, "TransactionManager" adlı nesnenin JNDI hizmetinden talep edildiğini ve bu hizmetin söz konusu nesneyi tanımadığını gösteriyor gibi görünüyor. Bu soruna bir çözüm bulmayı okuyuculara bırakıyorum. Bir çözüm bulunursa, bu belgeye eklenecektir.