Skip to content

17. 3 katmanlı mimaride MVC web uygulaması – Örnek 3 – Firebird veritabanı

17.1. Firebird veritabanı

Bu yeni sürümde, kişi listesini bir Firebird veritabanı tablosuna kaydedeceğiz. [http://tahe.developpez.com/divers/sql-firebird/] belgesinde, bu SGBD'i kurmak ve yönetmek için gerekli bilgiler yer almaktadır. Aşağıdaki ekran görüntüleri, Interbase ve Firebird SGBD dosyalarını yönetmek için kullanılan bir istemci olan IBExpert'ten alınmıştır.

Veritabanının adı [dbpersonnes.gdb]'tir. Bu veritabanında [PERSONNES] adlı bir tablo bulunmaktadır:

Image

[PERSONNES] tablosu, web uygulaması tarafından yönetilen kişilerin listesini içerecektir. Bu tablo, aşağıdaki SQL komutlarıyla oluşturulmuştur:

CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);


ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);
  • 2-10. satırlar: [Personne] türündeki nesneleri kaydetmeye yönelik [PERSONNES] tablosunun yapısı, bu nesnenin yapısını yansıtmaktadır. Firebird'de boole türü bulunmadığından, [MARIE] alanı (satır 8) [SMALLINT] türü, yani bir tamsayı olarak tanımlanmıştır. Değeri 0 (bekar) veya 1 (evli) olacaktır.
  • 13-16. satırlar: [ValidatePersonne] veri doğrulayıcısındakileri yansıtan bütünlük kısıtlamaları.
  • 19. satır: ID alanı, [PERSONNES] tablosunun birincil anahtarıdır.

[PERSONNES] tablosu aşağıdaki içeriğe sahip olabilir:

Image

[dbpersonnes.gdb] veritabanında, [PERSONNES] tablosunun yanı sıra, [GEN_PERSONNES_ID] adlı bir "jeneratör" nesnesi bulunmaktadır. Bu üretici, [PERSONNES] sınıfının [ID] birincil anahtarına değer atamak için kullanacağımız ardışık tamsayılar üretir. Nasıl çalıştığını açıklamak için bir örnek verelim:

[GEN_PERSONNES_ID] üreteç değerinin değiştiğini görebiliriz (üzerine çift tıklayın + F5 yazarak yenileyin):

 

Sıra SQL

SELECT GEN_ID ( GEN_PERSONNES_ID,1 ) FROM RDB$DATABASE

Bu sıraya göre, [GEN_PERSONNES_ID] üreteç değeri elde edilir. GEN_ID, Firebird'ün dahili bir işlevidir ve [RDB$DATABASE], SGBD'in bir sistem tablosudur.

17.2. [dao] ve [service] katmanlarının Eclipse projesi

Veritabanlı uygulamamızın [dao] ve [service] katmanlarını geliştirmek için aşağıdaki [mvc-personnes-03] Eclipse projesini kullanacağız:

Image

Bu proje, bir Tomcat web projesi değil, basit bir Java projesidir. Uygulamamızın 2. sürümünün, 1. sürümün [web] katmanını kullanacağını hatırlayalım. Dolayısıyla bu katmanın yeniden yazılmasına gerek yoktur.


[src] klasörü


Bu klasör, [dao] ve [service] katmanlarının kaynak kodlarını içerir:

Image

Bu klasörde çeşitli paketler bulunmaktadır:

  • [istia.st.mvc.personnes.dao]: [dao] katmanını içerir
  • [istia.st.mvc.personnes.entites]: [Personne] sınıfını içerir
  • [istia.st.mvc.personnes.service]: [service] sınıfını içerir
  • [istia.st.mvc.personnes.tests]: [dao] ve [service] katmanlarının JUnit testlerini

ayrıca uygulamanın ClassPath klasöründe bulunması gereken yapılandırma dosyalarını içerir.


[database] klasörü


Bu klasör, kişilerin Firebird veritabanını içerir:

Image

  • [dbpersonnes.gdb] veritabanıdır.
  • [dbpersonnes.sql], veritabanını oluşturan SQL komut dosyasıdır:
/******************************************************************************/
/***           IBExpert tarafından oluşturuldu 07.03.2006 2         7/04/2006 10:27:11 ***/
/******************************************************************************/

SET SQL DIALECT 3;

SET NAMES NONE;

CREATE DATABASE 'C:\data\2005-2006\webjava\dvp-spring-mvc\mvc-38\database\DBPERSONNES.GDB'
USER 'SYSDBA' PASSWORD 'masterkey'
PAGE_SIZE 16384
DEFAULT CHARACTER SET NONE;



/******************************************************************************/
/***                                                               Oluşturucular ***/
/******************************************************************************/

CREATE GENERATOR GEN_PERSONNES_ID;
SET GENERATOR GEN_PERSONNES_ID TO 787;



/******************************************************************************/
/***                                                                   Tablolar ***/
/******************************************************************************/



CREATE TABLE PERSONNES (
    ID             INTEGER NOT NULL,
    "VERSION"      INTEGER NOT NULL,
    NOM            VARCHAR(30) NOT NULL,
    PRENOM         VARCHAR(30) NOT NULL,
    DATENAISSANCE  DATE NOT NULL,
    MARIE          SMALLINT NOT NULL,
    NBENFANTS      SMALLINT NOT NULL
);

INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (1, 1, 'Major', 'Joachim', '1984-11-13', 1, 2);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (2, 1, 'Humbort', 'Mélanie', '1985-02-12', 0, 1);
INSERT INTO PERSONNES (ID, "VERSION", NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) VALUES (3, 1, 'Lemarchand', 'Charles', '1986-03-01', 0, 0);

COMMIT WORK;



/* Kısıtlama tanımlarını kontrol et */

ALTER TABLE PERSONNES ADD CONSTRAINT CHK_PRENOM_PERSONNES check (PRENOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_NOM_PERSONNES check (NOM<>'');
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_MARIE_PERSONNES check (MARIE=0 OR MARIE=1);
ALTER TABLE PERSONNES ADD CONSTRAINT CHK_ENFANTS_PERSONNES check (NBENFANTS>=0);


/******************************************************************************/
/***                                                             Birincil Anahtarlar ***/
/******************************************************************************/

ALTER TABLE PERSONNES ADD CONSTRAINT PK_PERSONNES PRIMARY KEY (ID);

[lib] klasörü


Bu klasör, uygulama için gerekli dosyaları içerir:

JDBC, [firebirdsql-full.jar] ve SGBD Firebird sürücülerinin yanı sıra bir dizi [spring-*.jar] arşiv dosyasının da mevcut olduğu dikkat çekmektedir. Dağıtımın [dist] klasöründe bulunan ve Spring sınıflarının tamamını içeren tek [spring.jar] arşivini de kullanabilirdik. Ayrıca, proje için gerekli olan arşivleri de kullanabiliriz. Burada, Eclipse tarafından bildirilen eksik sınıf hatalarını ve Spring’in kısmi arşivlerinin adlarını kılavuz alarak bunu yaptık. [lib] klasöründeki tüm bu arşivler, projenin Classpath klasörüne yerleştirildi.


[dist] klasörü


Bu klasör, uygulamanın sınıflarının derlenmesi sonucu ortaya çıkan arşivleri içerecektir:

Image

  • [personnes-dao.jar]: [dao] katmanının arşivi
  • [personnes-service.jar]: [service] katmanının arşivi

17.3. [dao] katmanı

17.3.1. [dao] katmanının bileşenleri

[dao] katmanı aşağıdaki sınıf ve arayüzlerden oluşur:

Image

  • [IDao], [dao] katmanı tarafından sunulan arayüzdür
  • [DaoImplCommon], kişi grubunun bir veritabanı tablosunda bulunduğu bu arayüzün bir uygulamasıdır. [DaoImplCommon], SGBD'ten bağımsız işlevleri bir araya getirir.
  • [DaoImplFirebird], özellikle bir Firebird veritabanını yönetmek için [DaoImplCommon]'ten türetilmiş bir sınıftır.
  • [DaoException], [dao] katmanı tarafından tetiklenen kontrol edilemeyen istisnaların türüdür. Bu sınıf, sürüm 1’e aittir.

[IDao] arayüzü şu şekildedir:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IDao {
     // tüm kişilerin listesi
    Collection getAll();
     // belirli bir kişiyi bul
    Personne getOne(int id);
     // Kişi ekleme/düzenleme
    void saveOne(Personne personne);
     // bir kişiyi sil
    void deleteOne(int id);
}
  • Arayüz, önceki sürümdekiyle aynı dört yönteme sahiptir.

Bu arayüzü uygulayan [DaoImplCommon] sınıfı şu şekilde olacaktır:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // kişilerin listesi
    public Collection getAll() {
...
    }

     // belirli bir kişiyi bul
    public Personne getOne(int id) {
...
    }

     // bir kişiyi silme
    public void deleteOne(int id) {
...
    }

     // bir kişiyi eklemek veya düzenlemek
    public void saveOne(Personne personne) {
         // "person" parametresi geçerli mi?
        check(personne);
         // Ekleme mi, değişiklik mi?
        if (personne.getId() == -1) {
             // ekleme
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // Kişi ekle
    protected void insertPersonne(Personne personne) {
...
    }

     // bir kişiyi düzenle
    protected void updatePersonne(Personne personne) {
...
    }

     // bir kişinin geçerliliğini kontrol etme
    private void check(Personne p) {
...
    }

...
}
  • 8-9. satırlar: [DaoImpl] sınıfı, [IDao] arayüzünü ve dolayısıyla dört yöntemi ([getAll, getOne, saveOne, deleteOne]) uygular.
  • 27-37. satırlar: [saveOne] yöntemi, bir kişinin eklenmesi veya değiştirilmesi durumuna göre [insertPersonne] ve [updatePersonne] adlı iki dahili yöntemi kullanır.
  • 50. satır: Özel [check] yöntemi, önceki sürümden kalmadır. Bu yöntem üzerinde tekrar durmayacağız.
  • 8. satır: [IDao] arayüzünü uygulamak için, [DaoImpl] sınıfı Spring’in [SqlMapClientDaoSupport] sınıfından türetilmiştir.

17.3.2. [iBATIS] veri erişim katmanı

Spring sınıfı [SqlMapClientDaoSupport], [http://ibatis.apache.org/] adresinde bulunan üçüncü taraf bir çerçeve olan [Ibatis SqlMap]’i kullanır:

Image

[iBATIS], veritabanlarına dayalı katmanların oluşturulmasını kolaylaştıran bir Apache projesidir. [dao] ile veri erişim katmanının mimarisi şu şekildedir:

[iBATIS], uygulamanın [dao] katmanı ile veritabanının JDBC sürücüsü arasına yerleştirilir. [iBATIS]'e alternatifler mevcuttur; örneğin, [Hibernate] alternatifi:

Image

[iBATIS] çerçevesinin kullanılması için, her ikisi de projenin [lib] klasörüne yerleştirilmiş olan iki [ibatis-common, ibatis-sqlmap] arşivi gereklidir:

[SqlMapClientDaoSupport] sınıfı, [iBATIS] ve c.a.d çerçevelerinin kullanımının genel kısmını kapsar. [iBATIS] aracını kullanan tüm [dao] katmanlarında bulunan kod parçaları. Kodun genel olmayan kısmını, yani yazdığımız [dao] katmanına özgü kısmı yazmak için, [SqlMapClientDaoSupport] sınıfından türetmek yeterlidir. Burada yaptığımız da budur.

[SqlMapClientDaoSupport] sınıfı şu şekilde tanımlanmıştır:

Image

Bu sınıfın yöntemlerinden biri, veritabanını kullanacağımız [iBATIS] istemcisini yapılandırmamızı sağlar:

Image

[SqlMapClient sqlMapClient] nesnesi, bir veritabanına erişmek için kullanılan [IBATIS] nesnesidir. Tek başına, mimarimizin [iBATIS] katmanını uygular:

Bu nesneyle gerçekleştirilen tipik bir eylem dizisi şöyledir:

  1. bir bağlantı havuzuna bağlantı talebinde bulunmak
  2. bir işlem açmak
  3. bir yapılandırma dosyasında saklanan bir dizi SQL komutunu yürütmek
  4. işlemi kapatmak
  5. bağlantıyı havuza iade etmek

[DaoImplCommon] uygulamamız doğrudan [iBATIS] ile çalışıyor olsaydı, bu adımları tekrar tekrar gerçekleştirmek zorunda kalırdı. Yalnızca 3. işlem [dao] katmanına özgüdür; diğer işlemler ise genel niteliktedir. Spring sınıfı [SqlMapClientDaoSupport], 1, 2, 4 ve 5 numaralı işlemleri kendisi gerçekleştirecek ve 3 numaralı işlemi türetilmiş sınıfına, bu örnekte [DaoImplCommon] sınıfına devredecektir.

Çalışabilmesi için [SqlMapClientDaoSupport] sınıfının, veritabanıyla iletişimi sağlayacak iBATIS [SqlMapClient sqlMapClient] nesnesine bir referansa ihtiyacı vardır. Bu nesnenin çalışabilmesi için iki şeye ihtiyacı vardır:

  • veritabanına bağlı bir [DataSource] nesnesi; bu nesneden bağlantı talep edecektir
  • çalıştırılacak SQL komutlarının dışa aktarıldığı bir (veya birkaç) yapılandırma dosyası. Aslında bu komutlar Java kodunda yer almaz. Bir yapılandırma dosyasındaki kodla tanımlanırlar ve [SqlMapClient sqlMapClient] nesnesi, bu kodu kullanarak belirli bir SQL komutunu çalıştırır.

Yukarıdaki mimariyi yansıtacak [dao] katmanımızın bir yapılandırma taslağı şu şekilde olabilir:


    <!-- [dao] katmanına erişim sınıfı -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
</bean>

Burada, [DaoImplCommon] sınıfının (satır 2) [sqlMapClient] özelliği (satır 3) başlatılmaktadır. Bu, [DaoImpl] sınıfının [setSqlMapClient] yöntemi tarafından gerçekleştirilir. Bu sınıfta söz konusu yöntem bulunmamaktadır. Bu yöntem, üst sınıfı olan [SqlMapClientDaoSupport] sınıfında mevcuttur. Dolayısıyla burada aslında bu sınıf başlatılmaktadır.

Şimdi 4. satıra gelelim: Henüz oluşturulmamış olan "sqlMapClient" adlı bir nesneye atıfta bulunuluyor. Daha önce de belirtildiği gibi, bu nesne [SqlMapClient] türündedir; bir [iBATIS] türü:

Image

[SqlMapClient] bir arayüzdür. Spring, bu arayüzü uygulayan bir nesne elde etmek için [SqlMapClientFactoryBean] sınıfını sunar:

Image

Hatırlayalım ki, [SqlMapClient] arayüzünü uygulayan bir nesne oluşturmaya çalışıyoruz. Görünüşe göre [SqlMapClientFactoryBean] sınıfı bu duruma uymuyor. Bu sınıf, [FactoryBean] arayüzünü uyguluyor (yukarıya bakınız). Bu arayüzde şu [getObject()] yöntemi bulunmaktadır:

Image

Spring’den [FactoryBean] arayüzünü uygulayan bir nesnenin örneği istendiğinde, Spring:

  • [I] sınıfından bir örnek oluşturur — burada ise [SqlMapClientFactoryBean] türünde bir örnek oluşturur.
  • çağıran yönteme, [I].getObject() yönteminin sonucunu döndürür – [SqlMapClientFactoryBean] yöntemi.getObject() yöntemi burada [SqlMapClient] arayüzünü uygulayan bir nesne döndürür.

[SqlMapClient] arayüzünü uygulayan bir nesneyi döndürebilmek için, [SqlMapClientFactoryBean] sınıfının bu nesneye ilişkin iki bilgiye ihtiyacı vardır:

  • veritabanına bağlı bir [DataSource] nesnesi; bu nesneden bağlantı taleplerinde bulunacaktır
  • çalıştırılacak SQL komutlarının dışa aktarıldığı bir (veya birden fazla) yapılandırma dosyası

[SqlMapClientFactoryBean] sınıfı, bu iki özelliği başlatmak için set yöntemlerine sahiptir:

Image

İlerliyoruz... Yapılandırma dosyamız netleşiyor ve şu hale geliyor:


<!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- [dao] katmanına erişim sınıfı -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
  • 2-3. satırlar: "sqlMapClient" bean'i, [SqlMapClientFactoryBean] türündedir. Az önce açıklananlardan, Spring'den bu bean'in bir örneğini istediğimizde, iBATIS [SqlMapClient] arayüzünü uygulayan bir nesne elde ettiğimizi biliyoruz. Dolayısıyla 14. satırda elde edilecek olan nesne işte budur.
  • 7-9. satırlar: iBATIS [SqlMapClient] nesnesi için gerekli yapılandırma dosyasının adının "sql-map-config-firebird.xml" olduğunu ve bu dosyanın uygulamanın ClassPath dizininde aranması gerektiğini belirtiriz. Burada [SqlMapClientFactoryBean].setConfigLocation yöntemi kullanılmaktadır.
  • 4-6. satırlar: [SqlMapClientFactoryBean] nesnesinin [dataSource] özelliğini, [setDataSource] yöntemiyle başlatıyoruz.
  1. satırda, henüz oluşturulmamış olan "dataSource" adlı bir bean'e başvuruyoruz. [SqlMapClientFactoryBean]'in [setDataSource] yönteminin beklediği parametreye bakarsak, bunun [DataSource] türünde olduğunu görürüz:

Image

Yine, bir uygulama sınıfı bulmamız gereken bir arayüzle karşı karşıyayız. Bu tür bir sınıfın rolü, bir uygulamaya belirli bir veritabanına etkili bir şekilde bağlantı sağlamaktır. Bir SGBD, aynı anda çok sayıda bağlantıyı açık tutamaz. Belirli bir anda açık olan bağlantı sayısını azaltmak için, veritabanıyla her veri alışverişinde şunları yapmak zorundayız:

  • bir bağlantı açmak
  • bir işlem başlatmak
  • SQL komutlarını göndermek
  • işlemi kapatmak
  • bağlantıyı kapatmak

Bağlantıları tekrar tekrar açıp kapatmak zaman alıcıdır. Bu iki sorunu (hem belirli bir anda açık olan bağlantı sayısını sınırlamak hem de bağlantıların açılma/kapanma maliyetini sınırlamak) çözmek için, [DataSource] arayüzünü uygulayan sınıflar genellikle şu şekilde hareket eder:

  • oluşturulur oluşturulmaz, hedef veritabanıyla N adet bağlantı açarlar. N genellikle bir varsayılan değere sahiptir ve çoğu zaman bir yapılandırma dosyasında tanımlanabilir. Bu N bağlantı her zaman açık kalır ve uygulamanın iş parçacıkları için kullanılabilir bir bağlantı havuzu oluşturur.
  • Uygulamanın bir iş parçacığı bağlantı açılmasını talep ettiğinde, [DataSource] nesnesi, başlangıçta açılmış olan N bağlantıdan birini, eğer kullanılabilir bağlantılar kalmışsa, iş parçacığına verir. Uygulama bağlantıyı kapattığında, bağlantı aslında kapatılmaz, sadece kullanılabilir bağlantı havuzuna geri eklenir.

[DataSource] arayüzünün çeşitli uygulamaları ücretsiz olarak mevcuttur. Burada, [http://jakarta.apache.org/commons/dbcp/] adresinde bulunan [commons DBCP] uygulamasını kullanacağız:

Image

[commons DBCP] aracını kullanmak için, her ikisi de projenin [lib] klasörüne yerleştirilmiş olan iki [commons-dbcp, commons-pool] arşivi gereklidir:

[commons DBCP]'in [BasicDataSource] sınıfı, ihtiyacımız olan [DataSource] uygulamasını sağlar:

Image

Bu sınıf, uygulamamızın Firebird veritabanına erişmek için bir bağlantı havuzu sağlayacaktır. Bunun için, havuzdaki bağlantıları oluşturmak üzere ihtiyaç duyduğu bilgileri ona vermemiz gerekir:

  1. kullanılacak sürücünün adı JDBC – [setDriverClassName] ile başlatılır
  2. kullanılacak veritabanının URL'si – [setUrl] ile başlatılır
  3. bağlantının sahibi olan kullanıcının kimliği – [setUsername] ile başlatılır (beklenebileceğinin aksine setUserName değildir)
  4. şifresi - [setPassword] ile başlatılır

[dao] katmanımızın yapılandırma dosyası şu şekilde olabilir:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- dikkat: URL'deki iki <value> etiketi arasında boşluk bırakmayın -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • 7-9. satırlar: JDBC Firebird sürücüsünün adı
  • 11-13. satırlar: Firebird veritabanının URL'si [dbpersonnes.gdb]. Bu URL'nin yazımına özellikle dikkat edilmelidir. <value> etiketleri ile URL arasında boşluk olmamalıdır.
  • 14-16. satırlar: Bağlantının sahibi – burada, Firebird dağıtımlarının varsayılan yöneticisi olan [sysdba]
  • 17-19. satırlar: şifresi [masterkey] – bu da varsayılan değerdir

Çok ilerleme kaydettik ancak hala açıklığa kavuşturulması gereken yapılandırma noktaları var: 28. satır, [sql-map-config-firebird.xml] dosyasına atıfta bulunuyor; bu dosya, iBATIS'in [SqlMapClient] istemcisini yapılandırmalıdır. İçeriğini incelemeden önce, Eclipse projemizde bu yapılandırma dosyalarının yerlerini gösterelim:

Image

  • [spring-config-test-dao-firebird.xml], az önce incelediğimiz [dao] katmanının yapılandırma dosyasıdır
  • [sql-map-config-firebird.xml], [spring-config-test-dao-firebird.xml] tarafından referans alınmaktadır. Bunu inceleyeceğiz.
  • [personnes-firebird.xml], [sql-map-config-firebird.xml] tarafından referans alınmaktadır. Bunu inceleyeceğiz.

Yukarıdaki üç dosya, [src] klasöründe bulunmaktadır. Eclipse'te bu, çalıştırma sırasında bu dosyaların projenin [bin] klasöründe (yukarıda gösterilmemiştir) bulunacağı anlamına gelir. Bu klasör, uygulamanın ClassPath dosyasının bir parçasıdır. Sonuç olarak, yukarıdaki üç dosya uygulamanın ClassPath klasöründe yer alacaktır. Bu gereklidir.

[sql-map-config-firebird.xml] dosyası şöyledir:


<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMapConfig
    PUBLIC "-//iBATIS.com//DTD SQL Map Config 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-config-2.dtd">

<sqlMapConfig>
    <sqlMap resource="personnes-firebird.xml"/>
</sqlMapConfig>
  • Bu dosyanın kök etiketi <sqlMapConfig> olmalıdır (6. ve 8. satırlar)
  • 7. satır: <sqlMap> etiketi, yürütülmesi gereken SQL komutlarını içeren dosyaları belirtmek için kullanılır. Genellikle her tablo için bir dosya bulunur, ancak bu zorunlu değildir. Bu, belirli bir tabloya ait SQL komutlarının tek bir dosyada toplanmasını sağlar. Ancak, sıklıkla birden fazla tabloyu ilgilendiren SQL komutlarına rastlanır. Bu durumda, yukarıdaki ayrıştırma geçerli olmaz. Sadece, <sqlMap> etiketleriyle belirtilen tüm dosyaların birleştirileceğini unutmamak gerekir. Bu dosyalar, uygulamanın ClassPath dosyasında aranır.

[personnes-firebird.xml] dosyası, [dbpersonnes.gdb] adlı Firebird veritabanındaki [PERSONNES] tablosuna gönderilecek olan SQL komutlarını tanımlar. İçeriği şöyledir:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- takma ad sınıfı [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- eşleme tablosu [PERSONNES] - nesne [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- tüm kişilerin listesi -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
    <!-- belirli bir kişiyi bulma -->
        <select id="Personne.getOne" resultMap="Personne.map" >select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES WHERE ID=#değer#</select>
    <!-- bir kişi ekle -->
    <insert id="Personne.insertOne" parameterClass="Personne.classe">
        <selectKey keyProperty="id">
            SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
        </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #sürüm#, #soyadı#, #adı#, #dateNaissance#, #evli#, 
        #nbEnfants#) </insert>
    <!-- bir kişiyi güncelle -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#sürüm#+1, NOM=#soyadı#, PRENOM=#adı#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#eş#, NBENFANTS=#nbEnfants# WHERE ID=#id# ve 
        VERSION=#sürüm#</update>
    <!-- bir kişiyi sil -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#değer# </sil>
</sqlMap>
  • dosyanın kök etiketi <sqlMap> olmalıdır (7. ve 45. satırlar)
  • 9-10. satırlar: Dosyanın yazılmasını kolaylaştırmak için [istia.st.springmvc.personnes.entites.Personne] sınıfına [Personne.classe] takma adı (eşanlamlı) verilir.
  • 12-21. satırlar: [PERSONNES] tablosunun sütunları ile [Personne] nesnesinin alanları arasındaki eşleşmeleri belirler.
  • 23-24. satırlar: [PERSONNES] tablosundaki tüm kişileri almak için SQL [select] sorgusu
  • 26-27. satırlar: SQL ve [select] komutları, [PERSONNES] tablosundan belirli bir kişiyi almak için kullanılır
  • satır 29-36: SQL [insert] komutu, [PERSONNES] tablosuna bir kişiyi ekler
  • satır 38-41: SQL ve [update] komutları, [PERSONNES] tablosundaki bir kişiyi günceller
  • satır 42-44: SQL ve [delete] komutları, [PERSONNES] tablosundan bir kişiyi siler

[personnes-firebird.xml] dosyasının içeriğinin rolü ve anlamı, [dao] katmanını uygulayan [DaoImplCommon] sınıfının incelenmesi yoluyla açıklanacaktır.

17.3.3. [DaoImplCommon] sınıfı

Veri erişim mimarisine tekrar dönelim:

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

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

import java.util.Collection;

public class DaoImplCommon extends SqlMapClientDaoSupport implements
        IDao {

     // kişi listesi
    public Collection getAll() {
...
    }

     // belirli bir kişiyi bul
    public Personne getOne(int id) {
...
    }

     // bir kişiyi silme
    public void deleteOne(int id) {
...
    }

     // bir kişiyi ekleme veya değiştirme
    public void saveOne(Personne personne) {
         // "kişi" parametresi geçerli mi?
        check(personne);
         // Ekleme mi, düzenleme mi?
        if (personne.getId() == -1) {
             // ekleme
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }

     // kişi ekleme
    protected void insertPersonne(Personne personne) {
...
    }

     // bir kişiyi düzenle
    protected void updatePersonne(Personne personne) {
...
    }

     // bir kişinin geçerliliğini kontrol etme
    private void check(Personne p) {
...
    }

...
}

Yöntemleri sırayla inceleyeceğiz.


getAll


Bu yöntem, listedeki tüm kişileri almayı sağlar. Kodu şöyledir:

1
2
3
4
     // kişi listesi
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}

Öncelikle, [DaoImplCommon] sınıfının Spring [SqlMapClientDaoSupport] sınıfından türediğini hatırlayalım. Yukarıdaki 3. satırda kullanılan [getSqlMapClientTemplate()] yöntemine sahip olan sınıf budur. Bu yöntemin imzası şöyledir:

Image

[SqlMapClientTemplate] türü, [iBATIS] katmanındaki [SqlMapClient] nesnesini kapsüller. Veritabanına erişim bu tür aracılığıyla sağlanacaktır. [iBATIS] ve SqlMapClient türleri, [SqlMapClientDaoSupport] sınıfının bunlara erişimi olduğu için doğrudan kullanılabilir:

Image

[iBATIS] ve SqlMapClient sınıflarının dezavantajı, [SQLException] türünde istisnalar atmasıdır; bu, c.a.d türünde bir kontrollü istisnadır. Bu istisnalar, bir try/catch bloğu ile yönetilmeli veya istisnayı tetikleyen yöntemlerin imzalarında beyan edilmelidir. Ancak, [dao] katmanının, yöntemlerinin imzalarında istisna içermeyen bir [IDao] arayüzünü uyguladığını hatırlayalım. Dolayısıyla, [IDao] arayüzünü uygulayan sınıfların yöntemlerinin imzalarında da istisna bulunamaz. Bu nedenle, [iBATIS] katmanı tarafından atılan her [SQLException] istisnasını yakalamalı ve bunu kontrol edilemeyen bir istisna içinde kapsüllemeliyiz. Projemizdeki [DaoException] türü, bu kapsülleme için uygun olacaktır.

Bu istisnaları kendimiz yönetmek yerine, bunları [iBATIS] katmanındaki [SqlMapClient] nesnesini kapsülleyen Spring [SqlMapClientTemplate] türüne devredeceğiz. Nitekim [SqlMapClientTemplate], [SqlMapClient] katmanı tarafından atılan [SQLException] istisnalarını yakalamak ve bunları kontrol edilemeyen bir [DataAccessException] türünde kapsüllemek üzere oluşturulmuştur. Bu davranış bizim için uygundur. Sadece şunu unutmamak gerekir ki, [dao] katmanı artık iki tür kontrol edilmeyen istisna atabilir:

  • özel türümüz [DaoException]
  • Spring türü [DataAccessException]

[SqlMapClientTemplate] türü şu şekilde tanımlanmıştır:

Image

Aşağıdaki [SqlMapClientOperations] arayüzünü uygular:

Image

Bu arayüz, [personnes-firebird.xml] dosyasının içeriğini işleyebilen yöntemleri tanımlar:

[queryForList]

Image

Bu yöntem, bir [SELECT] emri gönderilmesini ve sonucun bir nesne listesi olarak alınmasını sağlar:

  • [statementName]: yapılandırma dosyasındaki [select] emrinin kimliği (id)
  • [parameterObject]: parametreleri ayarlanmış bir [select] için "parametre" nesnesi. "Parametre" nesnesi iki şekilde olabilir:
    • Javabean standardına uygun bir nesne: bu durumda [select] emrinin parametreleri, Javabean'ın alan adlarıdır. [select] emri yürütüldüğünde, bunlar söz konusu alanların değerleriyle değiştirilir.
    • bir sözlük: bu durumda [select] emrinin parametreleri, sözlüğün anahtarlarıdır. [select] emri yürütüldüğünde, bu anahtarlar sözlükte bunlara eşlenen değerlerle değiştirilir.
  • [SELECT] komutu hiçbir satır döndürmezse, [List] sonucu öğe içermeyen boş bir nesne olur, ancak null için durum böyle değildir (doğrulanmalıdır).

[queryForObject]

Image

Bu yöntem, temel olarak öncekiyle aynıdır ancak yalnızca tek bir nesne döndürür. [SELECT] hiçbir satır döndürmezse, sonuç null işaretçisidir.

[insert]

Image

Bu yöntem, ikinci parametre ile yapılandırılan SQL [insert] komutunu çalıştırmaya olanak tanır. Döndürülen nesne, eklenen satırın birincil anahtarıdır. Bu sonucu kullanmak zorunlu değildir.

[update]

Image

Bu yöntem, ikinci parametre ile ayarlanan SQL [update] komutunu çalıştırmaya olanak tanır. Sonuç, SQL [update] komutu tarafından değiştirilen satır sayısıdır.

[delete]

Image

Bu yöntem, ikinci parametre ile ayarlanan SQL [delete] komutunu çalıştırmaya olanak tanır. Sonuç, SQL [delete] komutu tarafından silinen satır sayısıdır.

[DaoImplCommon] sınıfındaki [getAll] yöntemine geri dönelim:

1
2
3
4
     // kişi listesi
    public Collection getAll() {
        return getSqlMapClientTemplate().queryForList("Personne.getAll", null);
}
  • 4. satır: "Personne.getAll" adlı [select] emri yürütülür. Bu emir parametrelenmemiştir ve bu nedenle "parametre" nesnesi null'tir.

[personnes-firebird.xml]'te, "Personne.getAll" olarak adlandırılan [select] komutu şöyledir:


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

<!DOCTYPE sqlMap
    PUBLIC "-//iBATIS.com//DTD SQL Map 2.0//EN"
    "http://www.ibatis.com/dtd/sql-map-2.dtd">

<sqlMap>
    <!-- takma ad sınıfı [Personne] -->
    <typeAlias alias="Personne.classe" 
        type="istia.st.mvc.personnes.entites.Personne"/>
    <!-- eşleme tablosu [PERSONNES] - nesne [Personne] -->
    <resultMap id="Personne.map" 
        class="Personne.classe">
        <result property="id" column="ID" />
        <result property="version" column="VERSION" />
        <result property="nom" column="NOM"/>
        <result property="prenom" column="PRENOM"/>
        <result property="dateNaissance" column="DATENAISSANCE"/>
        <result property="marie" column="MARIE"/>
        <result property="nbEnfants" column="NBENFANTS"/>
    </resultMap>
    <!-- tüm kişilerin listesi -->
    <select id="Personne.getAll" resultMap="Personne.map" > select ID, VERSION, NOM, 
        PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM PERSONNES</select>
...
</sqlMap>
  • 23. satır: "Personne.getAll" adlı SQL komutunda parametre yoktur (istek metninde parametre bulunmamaktadır).
  • [getAll] yönteminin 3. satırı, "Personne.getAll" adlı [select] sorgusunun yürütülmesini talep eder. Bu sorgu yürütülecektir. [iBATIS], JDBC'e dayanmaktadır. Dolayısıyla, sorgunun sonucunun bir [ResultSet] nesnesi biçiminde elde edileceği bilinmektedir. 23. satırda, <select> etiketinin [resultMap] özniteliği, [iBATIS]'e, elde edilen [ResultSet]'in her satırını bir nesneye dönüştürmek için hangi "resultMap " kullanması gerektiğini belirtir. resultMap, yani 12-21. satırlarda tanımlanan [Personne.map], [PERSONNES] tablosundaki bir satırdan [Personne] türünde bir nesneye nasıl geçileceğini belirtir. [iBATIS], bu eşleşmeleri kullanarak [ResultSet] nesnesinin satırlarından bir [Personne] nesneleri listesi sağlayacaktır.
  • [getAll] yönteminin 3. satırı, bir [Personne] nesnesi koleksiyonunu döndürür
  • [queryForList] yöntemi, bir Spring [DataAccessException] istisnası atabilir. Bu istisnayı yukarıya iletiyoruz.

[AbstractDaoImpl] sınıfındaki diğer yöntemleri daha kısaca açıklayacağız; zira [iBATIS]'in kullanımıyla ilgili temel bilgiler, [getAll] yönteminin incelenmesi sırasında verilmiştir.


getOne


Bu yöntem, [id] kodu ile tanımlanan bir kişiyi bulmaya olanak tanır. Kod şu şekildedir:

         // belirli bir kişiyi bulma
    public Personne getOne(int id) {
         // BD'ten alınır
        Personne personne = (Personne) getSqlMapClientTemplate()
                .queryForObject("Personne.getOne", new Integer(id));
         // bir şey alındı mı?
        if (personne == null) {
             // bir istisna oluşturulur
            throw new DaoException(
                    "La personne d'id [" + id + "] n'existe pas", 2);
        }
         // kişiyi geri veriyoruz
        return personne;
    }
  • 4. satır: "Personne.getOne" adlı [select] komutunun yürütülmesini ister. Bu komut, [personnes-firebird.xml] dosyasında şu şekildedir:

<!-- belirli bir kişiyi alıyoruz -->
        <select id="Personne.getOne" resultMap="Personne.map" parameterClass="int">
            select ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS FROM 
            PERSONNES WHERE ID=#değer#</select>

SQL emri, #value# parametresiyle ayarlanır (4. satır). #value# özniteliği, SQL komutuna aktarılan parametrenin değerini belirtir; bu parametre basit türde olduğunda (Integer, Double, String, ...). <select> etiketinin özniteliklerinde, [parameterClass] özniteliği, parametrenin tamsayı türü olduğunu gösterir (2. satır). [getOne]'in 5. satırında, bu parametrenin aranan kişinin kimliği olduğunu ve bir Integer nesnesi biçiminde olduğunu görüyoruz. Bu tür değişikliği zorunludur, çünkü [queryForList]'in ikinci parametresi [Object] türünde olmalıdır.

[select] sorgusunun sonucu, [resultMap="Personne.map"] özniteliği (2. satır) aracılığıyla bir nesneye dönüştürülecektir. Böylece [Personne] türü elde edilecektir.

  • 7-11. satırlar: [select] sorgusu hiçbir satır döndürmediyse, 4. satırdaki null işaretçisi alınır. Bu, aranan kişinin bulunamadığı anlamına gelir. Bu durumda, kod 2 olan bir [DaoException] sorgusu başlatılır (9-10. satırlar).
  • 13. satır: Herhangi bir istisna oluşmadıysa, istenen [Personne] nesnesi döndürülür.

deleteOne


Bu yöntem, [id] ile tanımlanan bir kişiyi silmeye yarar. Kodu şöyledir:

     // bir kişiyi silme
    public void deleteOne(int id) {
         // kişiyi siliyoruz
        int n = getSqlMapClientTemplate().delete("Personne.deleteOne",
                new Integer(id));
         // işlem başarılı oldu mu
        if (n == 0) {
            throw new DaoException("Personne d'id [" + id + "] inconnue", 2);
        }
    }
  • 4-5. satırlar: "Personne.deleteOne" adlı [delete] komutunun yürütülmesini ister. Bu komut, [personnes-firebird.xml] dosyasında şu şekildedir:

<!-- bir kişiyi silmek -->
    <delete id="Personne.deleteOne" parameterClass="int"> delete FROM PERSONNES WHERE 
        ID=#değer# </delete>

SQL emri, [parameterClass="int"] (2. satır) türündeki #value# parametresiyle (3. satır) ayarlanır. Bu, aranan kişinin kimlik numarası olacaktır (deleteOne dosyasının 5. satırı)

  • 4. satır: [SqlMapClientTemplate].delete yönteminin sonucu, silinen satır sayısıdır.
  • 7-8. satırlar: [delete] sorgusu hiçbir satırı silmediyse, bu kişinin mevcut olmadığı anlamına gelir. Kod 2 olan bir [DaoException] sorgusu başlatılır (8. satır).

saveOne


Bu yöntem, yeni bir kişi eklemeye veya mevcut bir kişiyi değiştirmeye olanak tanır. Kodu şöyledir:

         // bir kişi eklemek veya değiştirmek
    public void saveOne(Personne personne) {
         // "kişi" parametresi geçerli mi?
        check(personne);
         // Ekleme mi, düzenleme mi?
        if (personne.getId() == -1) {
             // ekleme
            insertPersonne(personne);
        } else {
            updatePersonne(personne);
        }
    }
...
  • 4. satır: [check] yöntemiyle kişinin geçerliliği kontrol edilir. Bu yöntem önceki sürümde de mevcuttu ve o zaman yorum satırına alınmıştı. Kişi geçersizse bir [DaoException] çağrısı yapılır. Bu kodun yukarıya aktarılmasına izin verilir.
  • 6. satır: Buraya gelindiğine göre, herhangi bir istisna oluşmamıştır. Dolayısıyla kişi geçerlidir.
  • 6-11. satırlar: Kişinin kimliğine göre, bir ekleme (id = -1) veya bir güncelleme (id <> -1) söz konusudur. Her iki durumda da, sınıfın iki dahili yöntemi çağrılır:
    • insertPersonne: ekleme için
    • updatePersonne: güncelleme için

insertPersonne


Bu yöntem, yeni bir kişi eklemeye olanak tanır. Kod şöyledir:

// kişi ekle
    protected void insertPersonne(Personne personne) {
         // 1. sürüm
        personne.setVersion(1);
         // 10 ms bekleniyor - testler için false yerine true girin
        if (true)
            wait(10);
         // yeni kişi BD tablosuna ekleniyor
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • 4. satır: Oluşturulmakta olan kişinin sürüm numarası 1 olarak ayarlanır
  • 9. satır: "Personne.insertOne" adlı sorgu aracılığıyla ekleme yapılır; sorgu şöyledir:

        <insert id="Personne.insertOne" parameterClass="Personne.classe">
            <selectKey keyProperty="id">
                SELECT GEN_ID(GEN_PERSONNES_ID,1) as "value" FROM RDB$$DATABASE
            </selectKey>         
        insert into 
        PERSONNES(ID, VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
        VALUES(#id#, #sürüm#, #soyadı#, #adı#, #dateNaissance#, #marie#, 
    #nbEnfants#) </insert>

Bu, parametreli bir sorgudur ve parametre türü [Personne]'tir (parameterClass="Personne.classe", satır 1). Parametre olarak geçirilen [Personne] nesnesinin alanları (insertPersonne'in 9. satırı), [PERSONNES] tablosuna eklenecek satırın sütunlarını doldurmak için kullanılır (5-8. satırlar). Çözülmesi gereken bir sorun var. Ekleme işlemi sırasında, eklenecek [Personne] nesnesinin kimliği -1'dir. Bu değerin geçerli bir birincil anahtarla değiştirilmesi gerekir. Bunun için yukarıdaki <selectKey> etiketinin 2-4. satırları kullanılır. Bu satırlar şunu belirtir:

  • (devamı)
    • birincil anahtar değeri elde etmek için çalıştırılacak SQL sorgusu. Burada belirtilen sorgu, 17.1. paragrafında sunduğumuz sorgudur. İki noktaya dikkat edilmelidir:
      • "as" ve "value" zorunludur. "as value" şeklinde de yazılabilir, ancak value, Firebird'ün bir anahtar sözcüğü olduğundan tırnak işaretleriyle korunması gerekmiştir.
      • Firebird tablosunun gerçek adı [RDB$DATABASE]’tir. Ancak $ karakteri, [iBATIS] olarak yorumlanmaktadır. Bu karakter, iki kez yazılarak korunmuştur.
    • [Personne] nesnesinin alanı, [SELECT] komutuyla alınan değerle başlatılmalıdır; burada bu değer [id] alanından alınır. Bu alanı belirten, 2. satırdaki [keyProperty] özniteliğidir.
  • 6-7. satırlar: Testler için, ekleme işlemini gerçekleştirmeden önce 10 ms beklememiz gerekecek; bunun amacı, aynı anda ekleme yapmak isteyen iş parçacıkları arasında çakışma olup olmadığını görmek.

updatePersonne


Bu yöntem, [PERSONNES] tablosunda zaten mevcut olan bir kişiyi düzenlemeye olanak tanır. Kod şu şekildedir:

// bir kişiyi düzenle
    protected void updatePersonne(Personne personne) {
         // 10 ms bekleniyor - testler için false yerine true yazın
        if (true)
            wait(10);
         // değişiklik
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • Bir güncelleme en az iki nedenden dolayı başarısız olabilir:
    1. güncellenecek kişi mevcut değil
    2. güncellenecek kişi mevcut, ancak onu değiştirmek isteyen iş parçacığı doğru sürüme sahip değil
  • 7-8. satırlar: "Personne.updateOne" adlı SQL [update] sorgusu yürütülür. Bu sorgu şöyledir:

    <!-- bir kişiyi güncelle -->
    <update id="Personne.updateOne" parameterClass="Personne.classe"> update 
        PERSONNES set VERSION=#sürüm#+1, NOM=#soyadı#, PRENOM=#adı#, DATENAISSANCE=#dateNaissance#, 
        MARIE=#eş#, NBENFANTS=#nbEnfants# WHERE ID=#id# ve 
VERSION=#sürüm#</update>
  • (devamı)
    • 2. satır: sorgu yapılandırılmıştır ve parametre olarak [Personne] türünü kabul eder (parameterClass="Personne.classe"). Bu, değiştirilecek kişidir (satır 8 – updatePersonne).
    • Yalnızca, parametreyle aynı [id] numarasına ve aynı [version] sürümüne sahip olan [PERSONNES] tablosundaki kişiyi değiştirmek istiyoruz. Bu nedenle, [WHERE ID=#id# and VERSION=#version#] kısıtlaması vardır. Bu kişi bulunursa, parametre ile güncellenir ve sürümü 1 artırılır (yukarıdaki 3. satır).
  • 9. satır: Güncellenen satır sayısı alınır.
  • 10-11. satırlar: Bu sayı sıfırsa, kod 2 olan bir [DaoException] komutu çalıştırılır; bu, güncellenecek kişinin ya mevcut olmadığını ya da bu arada sürümünün değiştiğini gösterir.

17.4. [dao] katmanının testleri

17.4.1. [DaoImplCommon] uygulamasının testleri

[dao] katmanını yazdığımıza göre, şimdi bunu JUnit testleriyle test etmeyi planlıyoruz:

Image

Yoğun testlere geçmeden önce, [PERSONNES] tablosunun içeriğini görüntüleyecek basit bir [main] türü programla başlayabiliriz. Bu, [MainTestDaoFirebird] sınıfıdır:

package istia.st.mvc.personnes.tests;

import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;
import java.util.Iterator;

import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

public class MainTestDaoFirebird {
    public static void main(String[] args) {
        IDao dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
         // güncel liste
        Collection personnes = dao.getAll();
         // konsol ekranı
        Iterator iter = personnes.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
        }
    }
}

[dao] katmanına ait, 13-14. satırlarda kullanılan [spring-config-test-dao-firebird.xml] yapılandırma dosyası şu şekildedir:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- dikkat: iki <value> etiketi arasında boşluk bırakmayın -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplCommon">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>

Bu dosya, 17.3.2. paragrafında incelenen dosyadır.

Test için SGBD Firebird başlatılır. [PERSONNES] tablosunun içeriği şöyledir:

Image

[MainTestDaoFirebird] programının çalıştırılması sonucunda ekranda aşağıdaki sonuçlar görüntülenir:

Image

Kişi listesi başarıyla elde edilmiştir. JUnit testine geçilebilir.

JUnit ve [TestDaoFirebird] testleri şu şekildedir:

package istia.st.mvc.personnes.tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.Iterator;
import org.springframework.beans.factory.xml.XmlBeanFactory;
import org.springframework.core.io.ClassPathResource;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;
import junit.framework.TestCase;

public class TestDaoFirebird extends TestCase {

     // [dao] katmanı
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // üretici
    public void setUp() {
        dao = (IDao) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-dao-firebird.xml"))).getBean("dao");
    }

     // kişi listesi
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // mevcut olmayan bir öğenin değiştirilmesi-silinmesi
    public void test2() throws ParseException {
..
    }

     // kullanıcı sürüm yönetimi
    public void test3() throws ParseException, InterruptedException {
...
    }

     // iyimser kilitleme - çoklu iş parçacığı erişimi
    public void test4() throws Exception {
...
    }

     // saveOne geçerlilik testleri
    public void test5() throws ParseException {
....
    }

     // çoklu iş parçacığı ile ekleme
    public void test6() throws ParseException, InterruptedException{
...
}
  • [test1] ile [test5] arasındaki testler, biraz değiştirilen [test4] hariç, sürüm 1'dekilerle aynıdır. [test6] testi ise yenidir. Yalnızca bu iki test hakkında yorum yapacağız.

[test4]


[test4], [updatePersonne - DaoImplCommon] yöntemini test etmeyi amaçlamaktadır. Bu yöntemin kodunu hatırlatmak gerekirse:

// bir kişiyi düzenleme
    protected void updatePersonne(Personne personne) {
         // 10 ms bekleme - testler için false yerine true ayarlayın
        if (true)
            wait(10);
         // değişiklik
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }
  • 4-5. satırlar: 10 ms beklenir. Böylece, [updatePersonne]'i çalıştıran iş parçacığının işlemciyi kaybetmesi sağlanır; bu da, birbiriyle rekabet halindeki iş parçacıkları arasında erişim çakışmaları görme olasılığımızı artırabilir.

[test4], aynı kişinin çocuk sayısını aynı anda 1 artırmakla görevli N=100 iş parçacığı başlatır. Versiyon çatışmalarının ve erişim çatışmalarının nasıl yönetildiğini görmek istiyoruz.

    public void test4() throws Exception {
         // bir kişinin eklenmesi
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
        dao.saveOne(p1);
        int id1 = p1.getId();
         // çocuk sayısını güncellemek için N sayıda iş parçacığı oluşturma
        final int N = 100;
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoMajEnfants("thread n° " + i, dao, id1);
            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
        p1 = dao.getOne(id1);
         // bu kişinin N çocuğu olması gerekiyor
        assertEquals(N, p1.getNbEnfants());
         // p1 kişisinin silinmesi
        dao.deleteOne(p1.getId());
         // doğrulama
        boolean erreur = false;
        int codeErreur = 0;
        try {
            p1 = dao.getOne(p1.getId());
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // kod 2 hatası olmalı
        assertTrue(erreur);
        assertEquals(2, codeErreur);
    }

İş parçacıkları 8-13. satırlarda oluşturulur. Her biri, 3-5. satırlarda oluşturulan kişinin çocuk sayısını 1 artırır. Güncelleme iş parçacıkları [ThreadDaoMajEnfants ] şunlardır:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.DaoException;
import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

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

     // [dao] katmanında referans
    private IDao dao;

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

     // oluşturucu
    public ThreadDaoMajEnfants(String name, IDao dao, int idPersonne) {
        this.name = name;
        this.dao = dao;
        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 = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
             // takip
            suivi("" + nbEnfants + " -> " + (nbEnfants + 1)
                    + " pour la version " + personne.getVersion());
             // işlemciyi serbest bırakmak için 10 ms bekleme
            try {
                 // takip
                suivi("début attente");
                 // işlemciyi serbest bırakmak için duraklama
                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
            int codeErreur = 0;
            try {
                 // bu kopyanın alt öğe sayısını 1 artır
                personne.setNbEnfants(nbEnfants + 1);
                 // orijinali değiştirmeye çalışılıyor
                dao.saveOne(personne);
                 // işlem tamamlandı - orijinal değiştirildi
                fini = true;
            } catch (DaoException ex) {
                 // hata kodunu alıyoruz
                codeErreur = ex.getCode();
                 // ID hatası veya hata kodu 2 sürüm hatası varsa, güncelleme işlemi yeniden deneniyor
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // yönetilemeyen istisna - üst düzeye iletilir
                    throw ex;
                }
            }
        }
         // 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);
    }
}

Bir kişi güncellemesi, değiştirilmek istenen kişinin mevcut olmaması veya daha önce başka bir iş parçacığı tarafından güncellenmiş olması nedeniyle başarısız olabilir. Bu iki durum, burada 67-69. satırlarda yönetilmektedir. Nitekim bu iki durumda da [updatePersonne] yöntemi, kod 2 değerine sahip bir [DaoException] iş parçacığı başlatır. İş parçacığı daha sonra güncelleme prosedürünü başından itibaren yeniden başlatmak üzere geri döndürülür (while döngüsü, satır 34).


[test6]


[test6], [insertPersonne - DaoImplCommon] yöntemini test etmeyi amaçlamaktadır. Bu yöntemin kodunu tekrar hatırlatalım:

// bir kişi ekleyin
    protected void insertPersonne(Personne personne) {
         // 1. sürüm
        personne.setVersion(1);
         // 10 ms bekleniyor - testler için false yerine true girin
        if (true)
            wait(10);
         // yeni kişi BD tablosuna ekleniyor
        getSqlMapClientTemplate().insert("Personne.insertOne", personne);
    }
  • 6-7. satırlar: [insertPersonne]'i çalıştıran iş parçacığının işlemciyi kaybetmesini sağlamak için 10 ms bekliyoruz; böylece aynı anda ekleme yapan iş parçacıkları nedeniyle çakışmaların ortaya çıkma olasılığını artırıyoruz.

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

     // çoklu iş parçacıklı eklemeler
    public void test6() throws ParseException, InterruptedException{
         // bir kişinin oluşturulması
        Personne p = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 0);
         // bir dizide N kez kopyalanır
        final int N = 100;
        Personne[] personnes=new Personne[N];
        for(int i=0;i<personnes.length;i++){
            personnes[i]=new Personne(p);
        }
         // N adet ekleme iş parçacığı oluşturulur - her iş parçacığı 1 kişi ekler
        Thread[] taches = new Thread[N];
        for (int i = 0; i < taches.length; i++) {
            taches[i] = new ThreadDaoInsertPersonne("thread n° " + i, dao, personnes[i]);
            taches[i].start();
        }
         // iş parçacıklarının bitmesini bekliyoruz
        for (int i = 0; i < taches.length; i++) {
             // i numaralı iş parçacığı
            taches[i].join();
             // kişinin silinmesi
            dao.deleteOne(personnes[i].getId());
        }
}

Aynı anda 100 farklı kişiyi ekleyecek 100 iş parçacığı oluşturulur. Bu 100 iş parçacığının her biri, eklemesi gereken kişi için bir birincil anahtar alır ve ardından ekleme işlemini gerçekleştirebilmek için 10 ms süreyle kesintiye uğrar (10. satır – insertPersonne). Her şeyin yolunda gittiğini ve özellikle de her iş parçacığının farklı birincil anahtar değeri aldığını doğrulamak istiyoruz.

  • 7-11. satırlar: 100 kişiden oluşan bir dizi oluşturulur. Bu kişiler, 4-5. satırlarda oluşturulan p kişisinin kopyalarıdır.
  • satır 14-17: 100 ekleme iş parçacığı başlatılır. Her biri, daha önce oluşturulan 100 kişiden birini eklemekle görevlidir.
  • 19-23. satırlar: [test6], başlattığı 100 iş parçacığının her birinin bitmesini bekler. i numaralı iş parçacığının bittiğini algıladığında, bu iş parçacığının az önce eklediği kişiyi siler.

Ekleme iş parçacığı [ThreadDaoInsertPersonne] şu şekildedir:

package istia.st.mvc.personnes.tests;

import java.util.Date;

import istia.st.mvc.personnes.dao.IDao;
import istia.st.mvc.personnes.entites.Personne;

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

     // [dao] katmanına referans
    private IDao dao;

     // üzerinde çalışılacak kişinin kimliği
    private Personne personne;

     // oluşturucu
    public ThreadDaoInsertPersonne(String name, IDao dao, Personne personne) {
        this.name = name;
        this.dao = dao;
        this.personne = personne;
    }

     // konu özü
    public void run() {
         // takip
        suivi("lancé");
         // ekleme
        dao.saveOne(personne);
         // takip
        suivi("a terminé");
    }

     // takip
    private void suivi(String message) {
        System.out.println(name + " [" + new Date().getTime() + "] : "
                + message);
    }
}
  • 19-22. satırlar: İş parçacığı oluşturucu, eklemesi gereken kişiyi ve bu eklemeyi yapmak için kullanması gereken [dao] katmanını hafızaya alır.
  • 30. satır: Kişi eklenir. Bir istisna meydana gelirse, bu istisna [test6]'e iletilir.

Testler


Testler sonucunda şu sonuçlar elde edilir:

Dolayısıyla [test4] testi başarısız oluyor. Çocuk sayısı beklenen 100 yerine 69'a düşmüş. Ne oldu? Ekran günlüklerini inceleyelim. Bunlar, Firebird tarafından atılan istisnaların varlığını gösteriyor:


Exception in thread "Thread-62" org.springframework.jdbc.UncategorizedSQLException: SqlMapClient operation; uncategorized SQLException for SQL []; SQL state [HY000]; error code [335544336];   
--- Hata, personnes-firebird.xml'te meydana geldi.  
--- Hata, bir parametre eşlemesi uygulanırken meydana geldi.  
--- Personne.updateOne-InlineParameterMap dosyasını kontrol edin.  
--- Değiştirme komutunu kontrol edin (güncelleme başarısız).  
--- Neden: org.firebirdsql.jdbc.FBSQLException: GDS İstisnası. 335544336. kilitlenme
update conflicts with concurrent update; nested exception is com.ibatis.common.jdbc.exception.NestedSQLException:   
--- Hata, personnes-firebird.xml'te meydana geldi.  
--- Hata, bir parametre eşlemesi uygulanırken meydana geldi.  
  • 1. satır – Spring [org.springframework.jdbc.UncategorizedSQLException] istisnası oluştu. Bu, 6. satırda açıklanan Firebird’ün JDBC sürücüsü tarafından atılan bir istisnayı sarmalamak için kullanılan, denetlenmeyen bir istisnadır.
  • 6. satır – Firebird'ün JDBC sürücüsü, [org.firebirdsql.jdbc.FBSQLException] türünde ve hata kodu 335544336 olan bir istisna attı.
  • 7. satır: [PERSONNES] tablosunun aynı satırını aynı anda güncellemek isteyen iki iş parçacığı arasında bir erişim çakışması yaşandığını gösterir.

Bu, düzeltilemez bir hata değildir. Bu istisnayı yakalayan iş parçacığı, güncellemeyi yeniden deneyebilir. Bunun için [ThreadDaoMajEnfants] kodunda değişiklik yapılması gerekir:

            try {
                 // bu kopyanın alt öğe sayısını 1 artırır
                personne.setNbEnfants(nbEnfants + 1);
                 // orijinali değiştirmeye çalışılıyor
                dao.saveOne(personne);
                 // işlem tamamlandı - orijinal değiştirildi
                fini = true;
            } catch (DaoException ex) {
                 // hata kodu alınmaktadır
                codeErreur = ex.getCode();
                 // ID hatası veya kod sürümü hatası 2 varsa, güncelleme işlemi yeniden denenir
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                default:
                     // yönetilemeyen istisna - yukarıya iletilir
                    throw ex;
                }
  • 8. satır: [DaoException] türünde bir istisna işleniyor. Yukarıda belirtildiği gibi, testlerde ortaya çıkan [org.springframework.jdbc.UncategorizedSQLException] türündeki istisnayı işlememiz gerekiyor. Ancak, Spring'in tanımadığı istisnaları kapsüllemek için tasarlanmış genel bir tür olan bu türü yönetmekle yetinemeyiz. Spring, Oracle, MySQL, Postgres, DB2, SQL Server gibi bir dizi sürücünün JDBC sürücülerinden kaynaklanan istisnaları tanır; ancak Firebird'ü tanımaz. Ayrıca, Firebird’ün JDBC sürücüsü tarafından atılan tüm istisnalar, Spring’in [org.springframework.jdbc.UncategorizedSQLException] türünde kapsüllenmiştir:

Image

Yukarıda görüldüğü gibi, [UncategorizedSQLException] sınıfı, 17.3.3. paragrafında bahsettiğimiz [DataAccessException] sınıfından türetilmiştir. [UncategorizedSQLException] sınıfında kapsüllenmiş olan istisnayı, bu sınıfın [getSQLException] yönteminden öğrenmek mümkündür:

Image

[SQLException] türündeki bu istisna, [iBATIS] katmanı tarafından tetiklenen istisnadır; bu katman ise veritabanı sürücüsü JDBC tarafından tetiklenen istisnayı kapsüllemektedir. [SQLException] türündeki istisnanın kesin nedeni şu yöntemle elde edilebilir:

Image

Böylece, JDBC sürücüsü tarafından tetiklenen [Throwable] türündeki nesne elde edilir:

Image

[Throwable] türü, [Exception]'in üst sınıfıdır.

Burada, Firebird'ün JDBC sürücüsü tarafından başlatılan ve[iBATIS] katmanı tarafından tetiklenen [SQLException] istisnasının, gerçekten de [org.firebirdsql.gds.GDSException] türünde ve hata kodu 335544336 olan bir istisna olup olmadığını kontrol etmemiz gerekecektir. Hata kodunu almak için, [org.firebirdsql.gds.GDSException] sınıfının [getErrorCode()] yöntemini kullanabiliriz.

[ThreadDaoMajEnfants] kodunda [org.firebirdsql.gds.GDSException] istisnasını kullanırsak, bu iş parçacığı yalnızca SGBD Firebird ile çalışabilir. Bu iş parçacığını kullanan [test4] testi için de durum aynı olacaktır. Bunu önlemek istiyoruz. Aslında, JUnit testlerimizin, kullanılan SGBD ne olursa olsun geçerli kalmasını istiyoruz. Bu sonuca ulaşmak için, "güncelleme çakışması" türünde bir istisna tespit edildiğinde, altta yatan SGBD ne olursa olsun, [dao] katmanının kod 4'lü bir [DaoException] başlatmasına karar verdik. Böylece, [ThreadDaoMajEnfants] iş parçacığı şu şekilde yeniden yazılabilir:

package istia.st.mvc.personnes.tests;
...

public class ThreadDaoMajEnfants extends Thread {
...

     // iş parçacığının çekirdeği
    public void run() {
...
        while (!fini) {
             // idPersonne'ten bir kopya alınır
            Personne personne = dao.getOne(idPersonne);
            nbEnfants = personne.getNbEnfants();
...
             // bekleme tamamlandı - kopyayı doğrulamaya çalışılıyor
             // bu sırada diğer iş parçacıkları orijinali değiştirmiş olabilir
            int codeErreur = 0;
            try {
                 // bu kopyanın alt öğe sayısını 1 artırıyor
                personne.setNbEnfants(nbEnfants + 1);
                 // orijinali değiştirmeye çalışılıyor
                dao.saveOne(personne);
                 // değiştirildi - orijinal değiştirildi
                fini = true;
            } catch (DaoException ex) {
                 // hata kodunu alıyoruz
                codeErreur = ex.getCode();
                 // ID hatası, sürüm 2 hatası veya kilitlenme 4 hatası varsa,
                 // güncelleme işlemi yeniden denenir
                switch (codeErreur) {
                case 2:
                    suivi("version corrompue ou personne inexistante");
                    break;
                case 4:
                    suivi("conflit de mise à jour");
                    break;
                default:
                     // yönetilmeyen istisna - üst katmana iletilir
                    throw ex;
                }
            }
        }
         // izleme
        suivi("a terminé et passé le nombre d'enfants à " + (nbEnfants + 1));
    }
...
}
  • 34-36. satırlar: kod 4'e sahip [DaoException] türündeki istisna yakalanır. [ThreadDaoMajEnfants] iş parçacığı, güncelleme prosedürünü başından (10. satır) yeniden başlatmaya zorlanacaktır

Dolayısıyla, [dao] katmanımız, "güncelleme çakışması" türündeki bir istisnayı tanıyabilmelidir. Bu istisna, JDBC sürücüsü tarafından üretilir ve bu sürücüye özgüdür. Bu istisna, [DaoImplCommon] sınıfının [updatePersonne] yönteminde işlenmelidir:

// bir kişiyi düzenle
    protected void updatePersonne(Personne personne) {
         // 10 ms bekleniyor - testler için false yerine true girin
        if (true)
            wait(10);
         // değişiklik
        int n = getSqlMapClientTemplate()
                .update("Personne.updateOne", personne);
        if (n == 0)
            throw new DaoException("La personne d'Id [" + personne.getId()
                    + "] n'existe pas ou bien a été modifiée", 2);
    }

7-11. satırlar bir try / catch bloğu içine alınmalıdır. SGBD Firebird için, güncellemenin başarısız olmasına neden olan istisnanın [org.firebirdsql.gds.GDSException] türü olduğunu ve hata kodunun 335544336 olduğunu doğrulamamız gerekir. Bu tür bir testi [DaoImplCommon]'e eklersek, bu sınıfı SGBD Firebird ile ilişkilendirmiş oluruz ki bu elbette istenmeyen bir durumdur. [DaoImplCommon] sınıfının genel niteliğini korumak istiyorsak, bu sınıftan bir alt sınıf türetmeli ve istisnayı Firebird'e özgü bir sınıfta yönetmeliyiz. Şimdi bunu yapacağız.

17.4.2. [DaoImplFirebird] sınıfı

Kodu şu şekildedir:

package istia.st.mvc.personnes.dao;

import istia.st.mvc.personnes.entites.Personne;

public class DaoImplFirebird extends DaoImplCommon {

     // bir kişiyi düzenle
    protected void updatePersonne(Personne personne) {
         // 10 ms bekleniyor - testler için false yerine true girin
        if (true)
            wait(10);
         // değişiklik
        try {
             // doğru sürüme sahip kişiyi değiştiriyoruz
            int n = getSqlMapClientTemplate().update("Personne.updateOne",
                    personne);
            if (n == 0)
                throw new DaoException("La personne d'Id [" + personne.getId()
                        + "] n'existe pas ou bien a été modifiée", 2);
        } catch (org.springframework.jdbc.UncategorizedSQLException ex) {
            if (ex.getSQLException().getCause().getClass().isAssignableFrom(
                    org.firebirdsql.jdbc.FBSQLException.class)) {
                org.firebirdsql.jdbc.FBSQLException cause = (org.firebirdsql.jdbc.FBSQLException) ex
                        .getSQLException().getCause();
                if (cause.getErrorCode() == 335544336) {
                    throw new DaoException(
                            "Conflit d'accès au même enregistrement", 4);
                }
            } else {
                throw ex;
            }
        }
    }

     // bekleme
    private void wait(int N) {
         // N ms bekleniyor
        try {
            Thread.sleep(N);
        } catch (InterruptedException e) {
             // istisna izini görüntülenir
            e.printStackTrace();
            return;
        }
    }

}
  • 5. satır: [DaoImplFirebird] sınıfı, az önce incelediğimiz [DaoImplCommon] sınıfından türetilmiştir. Bu sınıf, 8-33. satırlarda bize sorun çıkaran [updatePersonne] yöntemini yeniden tanımlamaktadır.
  • 20. satır: [UncategorizedSQLException] türündeki Spring istisnasını yakalıyoruz
  • 21-22. satırlar: [SQLException] türündeki ve [iBATIS] katmanı tarafından atılan altta yatan istisnanın nedeninin [org.firebirdsql.jdbc.FBSQLException] türündeki bir istisna olduğunu kontrol ediyoruz
  • 25. satır: Ayrıca, bu Firebird istisnasının hata kodunun "deadlock" hatası kodu olan 335544336 olup olmadığı kontrol edilir.
  • 26-27. satırlar: Tüm bu koşullar karşılanırsa, kodu 4 olan bir [DaoException] başlatılır.
  • 36-44. satırlar: [wait] yöntemi, mevcut iş parçacığını N milisaniye süreyle durdurmaya yarar. Bu yöntem yalnızca test amaçlıdır.

Yeni [dao] katmanını test etmeye hazırız.

17.4.3. [DaoImplFirebird] uygulamasının testleri

[spring-config-test-dao-firebird.xml] test yapılandırma dosyası, [DaoImplFirebird] uygulamasını kullanacak şekilde değiştirildi:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <!-- dikkat: iki <value> etiketi arasında boşluk bırakmayın -->
        <property name="url">
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
</beans>
  • 32. satır: [dao] katmanının yeni uygulaması [DaoImplFirebird].

Daha önce başarısız olan [test4] testinin sonuçları şunlardır:

Image

[test4] başarılı oldu. Ekran günlüklerinin son satırları şunlardır:

1
2
3
4
5
6
7
thread n° 36 [1145977145984] : fin attente
thread n° 75 [1145977145984] : a terminé et passé le nombre d'enfants à 99
thread n° 36 [1145977146000] : version corrompue ou personne inexistante
thread n° 36 [1145977146000] : 99 -> 100 pour la version 100
thread n° 36 [1145977146000] : début attente
thread n° 36 [1145977146015] : fin attente
thread n° 36 [1145977146031] : a terminé et passé le nombre d'enfants à 100

Son satır, en son bitirenin 36 numaralı iş parçacığı olduğunu göstermektedir. 3. satır, 36 numaralı iş parçacığını kişinin güncelleme prosedürünü yeniden başlatmaya zorlayan bir sürüm çakışmasını göstermektedir (4. satır). Diğer günlük kayıtları, güncellemeler sırasında erişim çakışmalarını göstermektedir:

1
2
3
thread n° 52 [1145977145765] : version corrompue ou personne inexistante
thread n° 75 [1145977145765] : conflit de mise à jour
thread n° 36 [1145977145765] : version corrompue ou personne inexistante
  1. satır, 75 numaralı iş parçacığının bir güncelleme çakışması nedeniyle güncelleme işlemi sırasında başarısız olduğunu göstermektedir: SQL [update] komutu [PERSONNES] tablosuna gönderildiğinde, güncellenmesi gereken satır başka bir iş parçacığı tarafından kilitlenmişti. Bu erişim çakışması, 75 numaralı iş parçacığını güncellemeyi yeniden denemeye zorlayacaktır.

Son olarak, [test4] ile ilgili olarak, senkronizasyon sorunları nedeniyle başarısız olan sürüm 1’deki aynı testin sonuçlarıyla karşılaştırıldığında belirgin bir fark göze çarpmaktadır. Sürüm 1’deki [dao] katmanının yöntemleri senkronize olmadığından erişim çakışmaları ortaya çıkıyordu. Burada ise [dao] katmanını senkronize etmemize gerek kalmadı. Firebird tarafından bildirilen erişim çakışmalarını basitçe yönettik.

Şimdi [dao] katmanındaki JUnit testinin tamamını çalıştıralım:

Image

Görünüşe göre geçerli bir [dao] katmanımız var. Bunu yüksek bir olasılıkla geçerli olarak ilan edebilmek için daha fazla test yapmamız gerekir. Yine de bu katmanı çalışır durumda kabul edeceğiz.

17.5. [service] katmanı

17.5.1. [service] katmanının bileşenleri

[service] katmanı aşağıdaki sınıf ve arayüzlerden oluşur:

Image

  • [IService], [service] katmanı tarafından sunulan arayüzdür
  • [ServiceImpl], bunun bir uygulamasıdır

[IService] arayüzü şöyledir:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;

import java.util.Collection;

public interface IService {
     // tüm kişilerin listesi
    Collection getAll();

     // belirli bir kişiyi bulma
    Personne getOne(int id);

     // bir kişiyi ekleme/düzenleme
    void saveOne(Personne personne);

     // bir kişiyi sil
    void deleteOne(int id);

     // birden fazla kişiyi kaydet
    void saveMany(Personne[] personnes);

     // birden fazla kişiyi sil
    void deleteMany(int ids[]);
}
  • Arayüz, sürüm 1'dekiyle aynı dört yönteme sahiptir, ancak iki yöntem daha eklemiştir:
    • saveMany: Birden fazla kişiyi aynı anda atomik bir şekilde kaydetmeye olanak tanır. Ya hepsi kaydedilir ya da hiçbiri kaydedilmez.
    • deleteMany: Birden fazla kişiyi aynı anda atomik bir şekilde silmeye olanak tanır. Ya hepsi silinir ya da hiçbiri silinmez.

Bu iki yöntem web uygulaması tarafından kullanılmayacaktır. Bunları, veritabanındaki işlem kavramını açıklamak amacıyla ekledik. İstenen atomikliği sağlamak için bu iki yöntemin bir işlem içinde yürütülmesi gerekir.

Bu arayüzü uygulayan [ServiceImpl] sınıfı şu şekilde olacaktır:

package istia.st.mvc.personnes.service;

import istia.st.mvc.personnes.entites.Personne;
import istia.st.mvc.personnes.dao.IDao;

import java.util.Collection;

public class ServiceImpl implements IService {

     // [dao] katmanı
    private IDao dao;

    public IDao getDao() {
        return dao;
    }

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

     // kişi listesi
    public Collection getAll() {
        return dao.getAll();
    }

     // belirli bir kişiyi bul
    public Personne getOne(int id) {
        return dao.getOne(id);
    }

     // bir kişiyi ekleme veya düzenleme
    public void saveOne(Personne personne) {
        dao.saveOne(personne);
    }

     // bir kişiyi silme
    public void deleteOne(int id) {
        dao.deleteOne(id);
    }

     // kişi koleksiyonunu kaydetme
    public void saveMany(Personne[] personnes) {
         // kişi tablosunu döngüye sokma
        for (int i = 0; i < personnes.length; i++) {
            dao.saveOne(personnes[i]);
        }
    }

     // kişi koleksiyonunu silme
    public void deleteMany(int[] ids) {
         // ids: silinecek kişilerin kimlik numaraları
        for (int i = 0; i < ids.length; i++) {
            dao.deleteOne(ids[i]);
        }
    }
}
  • [getAll, getOne, insertOne, saveOne] yöntemleri, aynı isimdeki [dao] katmanındaki yöntemleri çağırır.
  • 42-47. satırlar: [saveMany] yöntemi, parametre olarak geçirilen dizideki kişileri tek tek kaydeder.
  • 50-55. satırlar: [deleteMany] yöntemi, id yönteminden parametre olarak aktarılan dizideki kişileri tek tek siler.

[saveMany] ve [deleteMany] yöntemlerinin, bu yöntemlerin "ya hep ya hiç" özelliğini sağlamak için bir işlem içinde gerçekleştirilmesi gerektiğini belirtmiştik. Yukarıdaki kodun bu işlem kavramını tamamen göz ardı ettiğini görebiliriz. Bu kavram, yalnızca [service] katmanının yapılandırma dosyasında yer alacaktır.

17.5.2. [service] katmanının yapılandırması

Yukarıda, 11. satırda, [ServiceImpl] uygulamasının [dao] katmanına bir referans içerdiği görülmektedir. Bu katman, sürüm 1’de olduğu gibi, [service - ServiceImpl] katmanının örneklendirilmesi sırasında Spring tarafından başlatılacaktır. [service] katmanının örneklendirilmesini sağlayacak yapılandırma dosyası şu şekilde olacaktır:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- dikkat: iki <value> etiketi arasında boşluk bırakmayın -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- işlem yöneticisi -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • 1-36. satırlar: [dao] katmanının yapılandırması. Bu yapılandırma, 17.3.2. paragrafında [dao] katmanı incelenirken açıklanmıştı.
  • 38-64. satırlar: [service] katmanını yapılandırır
  1. satırda, [service] katmanının uygulamasının [TransactionProxyFactoryBean] türü tarafından yapıldığı görülebilir. Burada [ServiceImpl] türünü bulmayı bekliyorduk. [TransactionProxyFactoryBean], Spring'in önceden tanımlanmış bir türüdür. Önceden tanımlanmış bir tür, uygulamamıza özgü olan [IService] arayüzünü nasıl uygulayabilir?

Öncelikle [TransactionProxyFactoryBean] sınıfını inceleyelim:

Image

Bu sınıfın [FactoryBean] arayüzünü uyguladığını görüyoruz. Bu arayüzle daha önce karşılaşmıştık. Bir uygulama Spring’den [FactoryBean]’i uygulayan bir türün örneğini istediğinde, Spring, bu türden bir [I] örneği değil, [I].getObject() yöntemi tarafından döndürülen nesneyi döndürür:

Image

Bizim durumumuzda, [service] katmanı, [TransactionProxyFactoryBean].getObject() tarafından döndürülen nesne tarafından uygulanacaktır. Bu nesnenin niteliği nedir? Ayrıntılara girmeyeceğiz çünkü bunlar karmaşıktır. Bunlar, Spring AOP (Aspect Oriented Programming) olarak adlandırılan alana girer. Basit şemalarla konuyu açıklığa kavuşturmaya çalışacağız. AOP şu imkanı sağlar:

  • C1 ve C2 adlı iki sınıfımız var; C1, C2 tarafından sunulan [I2] arayüzünü kullanıyor:
  • AOP sayesinde, her iki sınıf için de şeffaf bir şekilde, C1 ve C2 sınıfları arasına bir ara sınıf yerleştirilebilir:

[C1] sınıfı, [C2] tarafından uygulanan [I2] arayüzüyle çalışacak şekilde derlenmiştir. Çalışma sırasında, AOP, [intercepteur] sınıfını [C1] ile [C2] arasına yerleştirir. Bunun mümkün olabilmesi için, elbette [intercepteur] sınıfının, [C1]'e, [C2] ile aynı [I2] arayüzünü sunması gerekir.

Bu ne işe yarayabilir? Spring belgeleri birkaç örnek vermektedir. Örneğin, [C2] sınıfındaki belirli bir M yöntemine yapılan çağrılar sırasında, bu yöntemin denetimini yapmak amacıyla günlük kaydı tutmak isteyebilirsiniz. Bu durumda, [intercepteur] içinde bu günlükleri oluşturan bir [M] yöntemi yazılır. [C1]'in [C2].M yöntemini çağırması şu şekilde gerçekleşir (yukarıdaki şemaya bakınız):

  1. [C1], [C2]’in M yöntemini çağırır. Aslında çağrılacak olan, [intercepteur]'in M yöntemidir. Bu, [C1]'in [I2]'in belirli bir uygulamasına değil, [I2] arayüzüne başvurması durumunda mümkündür. Bu durumda, [intercepteur]'in [I2]'i uygulaması yeterlidir.
  2. [intercepteur]'in M yöntemi günlükleri oluşturur ve başlangıçta [C1] tarafından hedeflenen [C2]'in M yöntemini çağırır.
  3. [C2]'in M yöntemi çalışır ve sonucunu [intercepteur]'in M yöntemine döndürür; bu yöntem, 2. adımda yapılanlara gerekirse bir şeyler ekleyebilir.
  4. [intercepteur]'in M yöntemi, sonucu [C1]'in çağıran yöntemine döndürür

Görüldüğü gibi, [intercepteur]'in M yöntemi, [C2]'in M yönteminin çağrılmasından önce ve sonra bir işlem yapabilir. [C1] açısından bakıldığında, bu yöntem [C2]'in M yöntemini zenginleştirir. Dolayısıyla, AOP teknolojisini, bir sınıfın sunduğu arayüzü zenginleştirmenin bir yolu olarak görebiliriz.

Bu kavram, [service] katmanımıza nasıl uygulanır? [service] katmanını doğrudan bir [ServiceImpl] örneği ile uygularsak, web uygulamamız aşağıdaki mimariye sahip olacaktır:

[service] katmanını bir [TransactionProxyFactoryBean] örneği ile uygularsak, aşağıdaki mimariye sahip oluruz:

[service] katmanının iki nesneyle örneklendiğini söyleyebiliriz:

  • yukarıda [proxy transactionnel] olarak adlandırdığımız ve aslında [TransactionProxyFactoryBean]'in [getObject] yöntemiyle döndürdüğü nesne. [service] katmanının [web] katmanıyla arayüzünü sağlayacak olan bu nesnedir. Bu nesne, yapısı gereği [IService] arayüzünü uygular.
  • [ServiceImpl] örneği de [IService] arayüzünü uygular. Yalnızca bu örnek, [dao] katmanıyla nasıl çalışılacağını bilir; bu nedenle gereklidir.

[web] katmanının, [IService] arayüzündeki [saveMany] yöntemini çağırdığını varsayalım. İşlevsel olarak, bu yöntem tarafından yapılan ekleme ve güncelleme işlemlerinin bir işlem içinde gerçekleştirilmesi gerektiğini biliyoruz. Ya hepsi başarılı olur ya da hiçbiri yapılmaz. Daha önce [ServiceImpl] sınıfına ait [saveMany] yöntemini ele almış ve bu yöntemin işlem kavramını içermediğine dikkat çekmiştik. [proxy transactionnel] sınıfındaki [saveMany] yöntemi, [ServiceImpl] sınıfındaki [saveMany] yöntemini bu işlem kavramıyla zenginleştirecektir. Yukarıdaki şemayı takip edelim:

  1. [web] katmanı, [IService] arayüzünün [saveMany] yöntemini çağırır.
  2. [proxy transactionnel]'in [saveMany] yöntemi yürütülür. Bu yöntem bir işlem başlatır. Bunu yapabilmesi için yeterli bilgiye sahip olması gerekir; özellikle SGBD'e bağlantı kurmak için bir [DataSource] nesnesine sahip olması gerekir. Ardından, [ServiceImpl]'in [saveMany] yöntemini çağırır.
  3. Bu yöntem yürütülür. Ekleme veya güncelleme işlemlerini gerçekleştirmek için [dao] katmanını tekrar tekrar çağırır. Bu sırada yürütülen SQL komutları, 2. adımda başlatılan işlem kapsamında gerçekleştirilir.
  4. Bu işlemlerden birinin başarısız olduğunu varsayalım. [dao] katmanı, bir istisnayı [service] katmanına, bu durumda [ServiceImpl] örneğinin [saveMany] yöntemine iletecektir.
  5. Bu yöntem hiçbir işlem yapmaz ve istisnayı [proxy transactionnel]'in [saveMany] yöntemine iletir.
  6. İstisna alındığında, işlemin sahibi olan [proxy transactionnel]'in [saveMany] yöntemi, tüm güncellemeleri iptal etmek için bu işlem için bir [rollback] işlemi gerçekleştirir, ardından istisnanın, onu yönetmekle görevli olan [web] katmanına kadar iletilmesine izin verir.
  1. adımda, ekleme veya güncelleme işlemlerinden birinin başarısız olduğunu varsaydık. Aksi takdirde, [5]'te herhangi bir istisna iletilmez. Aynı durum [6] için de geçerlidir. Bu durumda, [proxy transactionnel]’in [saveMany] yöntemi, tüm güncellemeleri onaylamak için işlemin [commit]’ini gerçekleştirir.

Artık [TransactionProxyFactoryBean] bean'i tarafından kurulan mimari hakkında daha net bir fikrimiz var. Bu bean'in yapılandırmasına tekrar göz atalım:


    <!-- işlem yöneticisi -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- [service] katmanına erişim sınıfı -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_REQUIRED,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>

Yapılandırılan mimari ışığında bu yapılandırmayı inceleyelim:

  • [proxy transactionnel], işlemleri yönetecektir. Spring, işlem yönetimi için çeşitli stratejiler sunar. [proxy transactionnel], seçilen işlem yöneticisine bir referansa ihtiyaç duyar.
  • 11–13. satırlar: [TransactionProxyFactoryBean] bean'inin [transactionManager] özniteliğini, bir işlem yöneticisine referans vererek tanımlar. Bu işlem yöneticisi 2–7. satırlarda tanımlanmıştır.
  • 2–7. satırlar: İşlem yöneticisi, [DataSourceTransactionManager] türündedir:

Image

[DataSourceTransactionManager], bir [DataSource] nesnesi aracılığıyla erişilen SGBD'lere uyarlanmış bir işlem yöneticisidir. Bu işlem yöneticisi, yalnızca tek bir SGBD üzerindeki işlemleri yönetebilir. Birden fazla SGBD'e dağıtılmış işlemleri yönetemez. Burada ise tek bir SGBD bulunmaktadır. Dolayısıyla bu işlem yöneticisi uygundur. [proxy transactionnel] bir işlem başlatacağı zaman, bunu iş parçacığına bağlı bir bağlantı üzerinden yapacaktır. Veritabanına giden tüm katmanlarda kullanılacak olan bağlantı budur: [ServiceImpl, DaoImplCommon, SqlMapClientTemplate, JDBC].

[DataSourceTransactionManager] sınıfı, iş parçacığına bağlamak üzere bağlantı talebinde bulunacağı veri kaynağını bilmelidir. Bu, 4-6. satırlarda tanımlanmıştır: bu, [dao] katmanı tarafından kullanılan veri kaynağıyla aynıdır (bkz. 17.5.2. paragraf).

  • 14-19. satırlar: "target" özniteliği, yakalanması gereken sınıfı belirtir; burada bu, [ServiceImpl] sınıfıdır. Bu bilgi iki nedenden dolayı gereklidir:
    • [ServiceImpl] sınıfı, [dao] katmanıyla iletişimi sağlayan sınıf olduğu için örneklenmelidir
    • [TransactionProxyFactoryBean] sınıfı, [web] katmanına [ServiceImpl] ile aynı arayüzü sunan bir proxy oluşturmalıdır.
  • 21-27. satırlar: Proxy'nin [ServiceImpl]'in hangi yöntemlerini yakalaması gerektiğini belirtir. 21. satırdaki [transactionAttributes] özniteliği, [ServiceImpl]'in hangi yöntemlerinin bir işlem gerektirdiğini ve bu işlemin özniteliklerinin neler olduğunu belirtir:
  • 23. satır: Adı get [getOne, getAll] ile başlayan yöntemler, [PROPAGATION_REQUIRED,readOnly] özniteliğine sahip bir işlem içinde yürütülür:
    • PROPAGATION_REQUIRED: Yöntem, iş parçacığına zaten bir işlem bağlıysa bu işlem içinde yürütülür; aksi takdirde yeni bir işlem oluşturulur ve yöntem bu işlem içinde yürütülür.
    • readOnly: salt okunur işlem

Burada, [ServiceImpl]'in [getOne] ve [getAll] yöntemleri bir işlem içinde çalışacaktır, oysa aslında buna gerek yoktur. Her seferinde tek bir SELECT emrinden oluşan bir işlem söz konusudur. Bu SELECT'i bir işlem içine almanın yararını göremiyoruz.

  • 24. satır: Adı "save" ile başlayan yöntemler, [saveOne, saveMany], [PROPAGATION_REQUIRED] özniteliği işleminde yürütülmektedir.
  • 25. satır: [ServiceImpl]'in [deleteOne] ve [deleteMany] yöntemleri, [saveOne, saveMany] yöntemleriyle aynı şekilde yapılandırılmıştır.

[service] katmanımızda, yalnızca [saveMany] ve [deleteMany] yöntemlerinin bir işlem içinde yürütülmesi gerekmektedir. Yapılandırma aşağıdaki satırlarla sınırlandırılabilirdi:


        <property name="transactionAttributes">
            <props>
                <prop key="saveMany">PROPAGATION_REQUIRED</prop>
                <prop key="deleteMany">PROPAGATION_REQUIRED</prop>
            </props>
</property>

17.6. [service] katmanının testleri

[service] katmanını yazıp yapılandırdığımıza göre, şimdi JUnit testleriyle bu katmanı test etmeyi planlıyoruz:

Image

[service] katmanının [spring-config-test-service-firebird.xml] yapılandırma dosyası, 17.5.2. paragrafında açıklanan dosyadır.

JUnit [TestServiceFirebird] testi şu şekildedir:

package istia.st.mvc.personnes.tests;

...

public class TestServiceFirebird extends TestCase {

     // [service] katmanı
    private IService service;

    public IService getService() {
        return service;
    }

    public void setService(IService service) {
        this.service = service;
    }

     // kurulum
    public void setUp() {
        service = (IService) (new XmlBeanFactory(new ClassPathResource(
                "spring-config-test-service-firebird.xml"))).getBean("service");
    }

     // kişi listesi
    private void doListe(Collection personnes) {
...
    }

     // test1
    public void test1() throws ParseException {
...
    }

     // mevcut olmayan bir öğenin değiştirilmesi-silinmesi
    public void test2() throws ParseException {
...
    }

     // kişi sürümlerinin yönetimi
    public void test3() throws ParseException, InterruptedException {
...
    }

     // iyimser kilitleme - çoklu iş parçacığı erişimi
    public void test4() throws Exception {
...
    }

     // saveOne geçerlilik testleri
    public void test5() throws ParseException {
...
    }

         // çoklu iş parçacığı ile ekleme
    public void test6() throws ParseException, InterruptedException{
...
    }

     // deleteMany yönteminin testleri
    public void test7() throws ParseException {
         // güncel liste
        Collection personnes = service.getAll();
        int nbPersonnes1 = personnes.size();
         // görüntüleme
        doListe(personnes);
         // üç kişinin eklenmesi
        Personne p1 = new Personne(-1, "X", "X", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/02/2006"), true, 1);
        Personne p2 = new Personne(-1, "Y", "Y", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/03/2006"), false, 0);
        Personne p3 = new Personne(-2, "Z", "Z", new SimpleDateFormat(
                "dd/MM/yyyy").parse("01/04/2006"), true, 2);
         // 3 kişinin eklenmesi - p3 numaralı kişi, -2 kimliği ne sahip şunu tetikleyecektir
         // bir istisna
        boolean erreur = false;
        try {
            service.saveMany(new Personne[] { p1, p2, p3 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // doğrulama
        assertTrue(erreur);
         // yeni liste - öğe sayısı değişmemelidir
         // işlemin otomatik geri alınması nedeniyle
        int nbPersonnes2 = service.getAll().size();
        assertEquals(nbPersonnes1, nbPersonnes2);
         // iki geçerli kişinin eklenmesi
         // kimlikleri -1 olarak sıfırlandı
        p1.setId(-1);
        p2.setId(-1);
        service.saveMany(new Personne[] { p1, p2 });
         // kimlikleri geri alınır
        int id1 = p1.getId();
        int id2 = p2.getId();
         // doğrulamalar
        p1 = service.getOne(id1);
        assertEquals(p1.getNom(), "X");
        p2 = service.getOne(id2);
        assertEquals(p2.getNom(), "Y");
         // yeni liste - + işaretli 2 öğe olmalı
        int nbPersonnes3 = service.getAll().size();
        assertEquals(nbPersonnes1 + 2, nbPersonnes3);
         // p1 ve p2 ile var olmayan bir kişinin silinmesi
         // bir istisna oluşmalıdır
        erreur = false;
        try {
            service.deleteMany(new int[] { id1, id2, -1 });
        } catch (Exception ex) {
            erreur = true;
            System.out.println(ex.toString());
        }
         // doğrulama
        assertTrue(erreur);
         // yeni liste
        personnes = service.getAll();
        int nbPersonnes4 = personnes.size();
         // hiçbir kişi silinmemeliydi (işlemin
         // işlemin otomatik geri alınması)
        assertEquals(nbPersonnes4, nbPersonnes3);
         // iki geçerli kişi siliniyor
        service.deleteMany(new int[] { id1, id2 });
         // doğrulamalar
         // p1 kişisi
        erreur = false;
        int codeErreur = 0;
        try {
            p1 = service.getOne(id1);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // kod 2 hatası olmalı
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // p2 kişisi
        erreur = false;
        codeErreur = 0;
        try {
            p1 = service.getOne(id2);
        } catch (DaoException ex) {
            erreur = true;
            codeErreur = ex.getCode();
        }
         // kod 2 hatası olmalı
        assertTrue(erreur);
        assertEquals(2, codeErreur);
         // yeni liste
        personnes = service.getAll();
        int nbPersonnes5 = personnes.size();
         // doğrulama - başlangıç noktasına geri dönmüş olmalıyız
        assertEquals(nbPersonnes5, nbPersonnes1);
         // görüntüleme
        doListe(personnes);
    }

}
  • 19-22. satırlar: Program, bir önceki bölümde incelenen [spring-config-test-service-firebird.xml] dosyası tarafından yapılandırılan [dao] ve [service] katmanlarını test eder.
  • [test1] ile [test6] arasındaki testler, [dao] katmanındaki [TestDaoFirebird] test sınıfındaki aynı isimli testlerle temelde aynıdır. Tek fark, yapılandırma gereği [saveOne] ve [deleteOne] yöntemlerinin artık bir işlem içinde yürütülmesidir.
  • [test7] yöntemi, [saveMany] ve [deleteMany] yöntemlerini test etmeyi amaçlamaktadır. Bu yöntemlerin bir işlem içinde düzgün bir şekilde yürütüldüğünü doğrulamak istiyoruz. Bu yöntemin kodunu açıklayalım:
  • 62-63. satırlar: Şu anda listede bulunan [nbPersonnes1] kişilerinin sayısı sayılır
  • 67-72. satırlar: Üç kişi oluşturulur
  • 73-83. satırlar: Bu üç kişi, [saveMany] yöntemi ile kaydedilir – 77. satır. ID'si -1 olan ilk iki kişi p1 ve p2, [PERSONNES] tablosuna eklenecektir. p3 kişisinin ise id değeri -2’dir. Dolayısıyla bu bir ekleme değil, bir güncellemedir. [PERSONNES] tablosunda id değeri -2 olan hiçbir kişi bulunmadığından bu işlem başarısız olacaktır. Dolayısıyla [dao] katmanı bir istisna oluşturacak ve bu istisna [service] katmanına kadar iletilecektir. Bu istisnanın varlığı 83. satırda kontrol edilmektedir.
  • Önceki istisna nedeniyle, [service] katmanı, [saveMany] yönteminin yürütülmesi sırasında verilen tüm SQL emirleri için bir [rollback] işlemi gerçekleştirmelidir; çünkü bu yöntem bir işlem içinde yürütülmektedir. 86-87. satırlarda, listedeki kişi sayısının değişmediği ve dolayısıyla p1 ile p2'nin eklenmesinin gerçekleşmediği kontrol edilir.
  • 88-103. satırlar: Yalnızca p1 ve p2 kişileri eklenir ve ardından listede iki kişi daha olduğu doğrulanır.
  • 106-114. satırlar: Az önce eklediğimiz p1 ve p2 ile var olmayan bir kişi (id= -1)den oluşan bir kişi grubu silinir. Bunun için 108. satırdaki [deleteMany] yöntemi kullanılır. [PERSONNES] tablosunda id değeri –1 olan hiçbir kişi bulunmadığından bu yöntem başarısız olacaktır. Dolayısıyla [dao] katmanı bir istisna oluşturacak ve bu istisna [service] katmanına kadar yayılacaktır. Bu istisnanın varlığı 114. satırda test edilmektedir.
  • Önceki istisna nedeniyle, [service] katmanı, [deleteMany] yönteminin yürütülmesi sırasında gönderilen tüm SQL emirlerini [rollback]'e dönüştürmelidir; çünkü bu yöntem bir işlem içinde yürütülmektedir. 116-117. satırlarda, listedeki kişi sayısında bir değişiklik olmadığı ve dolayısıyla p1 ile p2'nin silinmediği doğrulanır.
  • 122. satır: Yalnızca p1 ve p2 kişilerinden oluşan bir grup silinir. Bu işlem başarılı olmalıdır. Yöntemin geri kalan kısmı, durumun gerçekten böyle olup olmadığını doğrular.

Testlerin yürütülmesi sonucunda şu sonuçlar elde edilir:

Image

Yedi testin tümü başarılı olmuştur. [service] katmanımızı çalışır durumda kabul edeceğiz.

17.7. [web] katmanı

Oluşturulacak web uygulamasının genel mimarisini hatırlayalım:

Firebird veritabanıyla çalışmayı sağlayan [dao] ve [service] katmanlarını az önce oluşturduk. Bu uygulamanın 1. sürümünü yazdık; bu sürümde [dao] ve [service] katmanları, bellekteki bir kişi listesiyle çalışıyordu. Bu sırada yazılan [web] katmanı hâlâ geçerlidir. Zira bu katman, [IService] arayüzünü uygulayan [service] katmanına yönelikti. Aynı arayüzü uygulayan yeni [service] katmanı olduğu için, [web] katmanının değiştirilmesine gerek yoktur.

Önceki makalede, uygulamanın 1. sürümü, [mvc-personnes-02B] adlı Eclipse projesiyle test edilmişti; bu projede [web, service, dao, entites] katmanları .jar dosyalarına yerleştirilmişti:

[src] klasörü boştu. Katman sınıfları, [personnes-*.jar ] arşivlerinde bulunuyordu:

Sürüm 2'yi test etmek için Eclipse'te [mvc-personnes-02B] klasörünü [mvc-personnes-03B] olarak kopyalayıp yapıştırıyoruz:

Image

[mvc-personnes-03] projesinde, [File / Export / Jar file] katmanlarını sırasıyla [dao] ve [service] adıyla, projenin [dist] klasöründeki [personnes-dao.jar] ve [personnes-service.jar] arşivlerine sırasıyla aktarıyoruz:

Image

Bu iki dosyayı kopyalayıp Eclipse'te [WEB-INF/lib] projesinin [mvc-personnes-03B] klasörüne yapıştırıyoruz; burada önceki sürümdeki aynı isimli arşivlerin yerini alacaklar.

Ayrıca, [mvc-personnes-03] projesinin [lib] klasöründeki [commons-dbcp-*.jar, commons-pool-*.jar, firebirdsql-full.jar, ibatis-common-2.jar, ibatis-sqlmap-2.jar] dosyalarını da [mvc-personnes-03B] projesinin [WEB-INF/lib] klasörüne kopyalayıp yapıştırıyoruz. Bu arşivler, yeni [dao] ve [service] katmanları için gereklidir.

Bu işlem tamamlandıktan sonra, yeni arşivleri [clic droit sur projet -> Properties -> Java Build Path -> Add Jars] projesinin Classpath'ine ekliyoruz.

[src] klasörü, [dao] ve [service] katmanlarının yapılandırma dosyalarını içerir:

Image

[spring-config.xml] dosyası, web uygulamasının [dao] ve [service] katmanlarını yapılandırır. Yeni sürümde, bu dosya, [mvc-personnes-03] projesinde hizmet katmanının testini yapılandırmak için kullanılan [spring-config-test-service-firebird.xml] dosyasıyla aynıdır. Dolayısıyla, bir dosyadan diğerine kopyala/yapıştır işlemi gerçekleştirilir:


<?xml version="1.0" encoding="ISO_8859-1"?>
<!DOCTYPE beans SYSTEM "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <!-- veri kaynağı DBCP -->
    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" 
        destroy-method="close">
        <property name="driverClassName">
            <value>org.firebirdsql.jdbc.FBDriver</value>
        </property>
        <property name="url">
            <!-- dikkat: iki <value> etiketi arasına boşluk bırakmayın -->
            <value>jdbc:firebirdsql:localhost/3050:C:/data/2005-2006/eclipse/dvp-eclipse-tomcat/mvc-personnes-03/database/dbpersonnes.gdb</value>
        </property>
        <property name="username">
            <value>sysdba</value>
        </property>
        <property name="password">
            <value>masterkey</value>
        </property>
    </bean>
    <!-- SqlMapCllient -->
    <bean id="sqlMapClient" 
        class="org.springframework.orm.ibatis.SqlMapClientFactoryBean">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
        <property name="configLocation">
            <value>classpath:sql-map-config-firebird.xml</value>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [dao] -->
    <bean id="dao" class="istia.st.mvc.personnes.dao.DaoImplFirebird">
        <property name="sqlMapClient">
            <ref local="sqlMapClient"/>
        </property>
    </bean>
    <!-- işlem yöneticisi -->
    <bean id="transactionManager" 
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    <!-- katmana erişim sınıfı [service] -->
    <bean id="service" 
        class="org.springframework.transaction.interceptor.TransactionProxyFactoryBean">
        <property name="transactionManager">
            <ref local="transactionManager"/>
        </property>
        <property name="target">
            <bean class="istia.st.mvc.personnes.service.ServiceImpl">
                <property name="dao">
                    <ref local="dao"/>
                </property>
            </bean>
        </property>
        <property name="transactionAttributes">
            <props>
                <prop key="get*">PROPAGATION_SUPPORTS,readOnly</prop>
                <prop key="save*">PROPAGATION_REQUIRED</prop>
                <prop key="delete*">PROPAGATION_REQUIRED</prop>
            </props>
        </property>
    </bean>
</beans>
  • 12. satır: Firebird veritabanının URL’si. [dao] ve [service] katmanlarının testlerinde kullanılan veritabanını kullanmaya devam ediyoruz

[mvc-personnes-03B] web projesini Tomcat ortamında devreye alıyoruz:

'in testlerine hazırız. SGBD Firebird başlatıldı. [PERSONNES] tablosunun içeriği şu şekildedir:

Image

Tomcat da sırayla başlatılır. Bir tarayıcı kullanarak [http://localhost:8080/mvc-personnes-03B] URL'sini çağırıyoruz:

Image

Şu bağlantıyla [Ajout] bağlantısı ile yeni bir kişi ekliyoruz:

Veritabanında eklenmiş olup olmadığını kontrol ediyoruz:

Image

Okuyucunun başka testler yapması isteniyor: [modification, suppression].

Şimdi, sürüm 1'de yapılmış olan sürüm çakışması testini yapalım. [Firefox], U1 kullanıcısının tarayıcısı olacaktır. Bu tarayıcı, [http://localhost:8080/mvc-personnes-03B] URL'sini ister:

Image

[IE], U2 kullanıcısının tarayıcısı olacaktır. Bu kullanıcı da aynı URL'yi talep etmektedir:

Image

U1 kullanıcısı, [Perrichon] 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ı, formdaki [Annuler] bağlantısı aracılığıyla kişi listesine geri döner:

Image

U1 tarafından değiştirilmiş haliyle (adı büyük harfe dönüştürülmüş) [Perrichon] adlı kişiyi bulur.

Peki ya veritabanı? Bir bakalım:

Image

899 numaralı kişinin adı, U1 tarafından yapılan değişiklik sonucunda büyük harflerle yazılmıştır.

17.8. Conclusion

Ne yapmak istediğimizi hatırlayalım. Aşağıdaki 3 katmanlı mimariye sahip bir web uygulamamız vardı:

burada [dao] ve [service] katmanları, bellekteki bir veri listesiyle çalışıyordu ve bu nedenle web sunucusu kapatıldığında bu veriler kayboluyordu. Bu, sürüm 1'di. Sürüm 2'de, [service] ve [dao] katmanları, kişi listesinin bir veritabanı tablosunda yer alması için yeniden yazıldı. Böylece liste artık kalıcı hale geldi. Şimdi, SGBD'teki değişikliğin uygulamamız üzerindeki etkisini inceleyeceğiz. Bunun için web uygulamamızın üç yeni sürümünü oluşturacağız:

  • sürüm 3: SGBD, Postgres'tir
  • sürüm 4: SGBD, MySQL'tir
  • sürüm 5: SGBD, SQL Server Express 2005'tir

Değişiklikler aşağıdaki yerlerde yapılmaktadır:

  • [DaoImplFirebird] sınıfı, SGBD Firebird ile ilgili [dao] katmanının işlevlerini uygular. Bu ihtiyaç devam ederse, söz konusu sınıf sırasıyla [DaoImplPostgres], [DaoImplMySQL] ve [DaoImplSqlExpress] sınıflarıyla değiştirilecektir.
  • SGBD Firebird için iBATIS eşleme dosyası, sırasıyla [personnes-postgres.xml], [personnes-mysql.xml] ve [personnes-sqlexpress.xml] ile değiştirilecektir.
  • [dao] katmanındaki [DataSource] nesnesinin yapılandırması, SGBD'e özeldir. Dolayısıyla her sürümde değişecektir.
  • SGBD'in JDBC sürücüsü de her sürümde değişmektedir.

Bu noktalar dışında her şey aynı kalır. Aşağıda, bu yeni sürümleri, her birinin getirdiği yeniliklere odaklanarak açıklayacağız.