Objektif

Use case yang saya dapatkan adalah seperti berikut ini. Sebuah aplikasi dibangun dengan model SaaS. Setiap klien akan punya database sendiri. Berdasarkan kebutuhan tersebut, maka kira-kira nantinya akan terdapat sekitar 50 database dengan struktur (table-tablenya) yang sama persis. Karena database yang digunakan tergantung login credential, maka database mana yang akan digunakan ditentukan pada saat user login.

Kasusnya jadi tambah menarik karena ada kebutuhan transaksi antar database. Jadi, sebuah transaksi bisa melibatkan posting data ke dua koneksi database yang berbeda. Misalkan, sebuah transaksi bisnis melibatkan perpindahan dana antar klien, yang artinya akun klien A harus didebet dan akun klien B harus dikredit. Akun klien A dan klien berada di database yang berbeda, tapi transaksi tersebut harus diselesaikan sebagai satu kesatuan.

Solusi

(Sebelum Anda baca terlalu jauh, perhatikan bahwa solusi ini spesifik untuk Hibernate 3.x, JPA atau non-JPA)

Dalam melakukan akses data, komponen yang kita buat tidak berhubungan langsung dengan JDBC API. Komponen-komponen tersebut berinteraksi dengan satu lapisan abstraksi yang biasa disebut Object Relational Mapping (ORM).  Bila kita menuruti spek standar JEE 5 (setting standar SEAM menggunakan ini) yang sering disebut dengan JPA, maka interface yang kita gunakan untuk akses data adalah EntityManager. Bila kita menggunakan Hibernate, maka interface yang kita gunakan adalah Session.

data-access-1

Sebuah aplikasi SEAM hasil generate dari seam-gen, pada kondisi default, mempunyai konfigurasi seperti diagram diatas. Komponen-komponen yang kita buat (atau hasil generate dari seam-gen) berinteraksi dengan sebuah EntityManager, yang mana EntityManager tersebut menggunakan sebuah koneksi database. (AWAS: ini adalah penyederhanaan. Buat Anda yang punya pemahaman mendalam tentang JEE tentu tahu jika yang saya bicarakan disini adalah dalam konteks suatu persistence context. EntityManager yang kedua juga berarti ada persistence unit yang kedua. Untuk keterangan lihat disini).

Jadi, bagaimana caranya supaya kita bisa menggunakan beberapa koneksi database yang berbeda, dan menentukan koneksi mana yang dipakai pada saat tertentu? Triknya adalah dengan mengimplementasi interface org.hibernate.connection.ConnectionProvider. ConnectionProvider digunakan oleh Hibernate untuk mendapatkan koneksi database ketika sebuah session baru dibuat. (saya mempelajari trik ini dari sini).

Jadi, pada intinya kita sediakan beberapa koneksi database, dan kita sediakan pula implementasi ConnectionProvider yang akan memilih salah satu dari koneksi tersebut berdasarkan user yang sedang login. Diagram diatas seharusnya berubah menjadi seperti berikut ini.
data-access-2

Masalah lain yang harus diselesaikan adalah, kasus ini melibatkan transaksi antar database. Jadi dalam satu transaksi bisa melibatkan lebih dari satu koneksi database. Kita akan melakukannya dengan menambahkan satu lagi EntityManager kedalam aplikasi.  EntityManager kedua ini akan membungkus koneksi database kedua yang juga harus berpartisipasi dalam transaksi tersebut. Diagram diatas akan berubah lagi menjadi seperti berikut ini.

data-access-3

Lalu bagaimana dengan transaksi database? Dalam kasus ini, dua atau lebih koneksi database harus berpartisipasi dalam sebuah transaksi yang sama. Tidak ada masalah… Kita hanya harus merubah tipe koneksi, dari yang tadinya “local-transaction” menjadi “xa-transaction“. (bagi yang ingin tahu lebih banyak tentang apa itu local-transaction dan xa-transaction, silakan baca ini, ini, dan lihat video ini).

Ok, selanjutnya tinggal bagian mudahnya.. implementasi :)

Implementasi

Definisikan koneksi-koneksi database yang akan digunakan kedalam file xxx-ds.xml. Semua koneksi tersebut boleh berada di lebih dari satu file.

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

<!DOCTYPE datasources
 PUBLIC "-//JBoss//DTD JBOSS JCA Config 1.5//EN"
 "http://www.jboss.org/j2ee/dtd/jboss-ds_1_5.dtd">

<datasources>
 <xa-datasource>
   <jndi-name>SALES_APPDatasource</jndi-name>
   <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
   <xa-datasource-property name="URL">jdbc:mysql://localhost:3306/sales_app</xa-datasource-property>
   <user-name>root</user-name>
   <transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
   <track-connection-by-tx />
   <max-pool-size>5</max-pool-size>
   <min-pool-size>1</min-pool-size>
   <blocking-timeout-millis>2000</blocking-timeout-millis>
   <idle-timeout-minutes>2</idle-timeout-minutes>
   <no-tx-separate-pools/>
   <exception-sorter-class-name>
       org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter
   </exception-sorter-class-name>
 </xa-datasource>

 <xa-datasource>
   <jndi-name>SALES_APPDatasource2</jndi-name>
   <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
   <xa-datasource-property name="URL">jdbc:mysql://localhost:3306/sales_app2</xa-datasource-property>
   <user-name>root</user-name>
   <transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
   <track-connection-by-tx />
   <max-pool-size>5</max-pool-size>
   <min-pool-size>1</min-pool-size>
   <blocking-timeout-millis>2000</blocking-timeout-millis>
   <idle-timeout-minutes>2</idle-timeout-minutes>
   <no-tx-separate-pools/>
   <exception-sorter-class-name>
     org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter
   </exception-sorter-class-name>
 </xa-datasource>

 <xa-datasource>
   <jndi-name>SALES_APPDatasource3</jndi-name>
   <xa-datasource-class>com.mysql.jdbc.jdbc2.optional.MysqlXADataSource</xa-datasource-class>
   <xa-datasource-property name="URL">jdbc:mysql://localhost:3306/sales_app3</xa-datasource-property>
   <user-name>root</user-name>
   <transaction-isolation>TRANSACTION_READ_COMMITTED</transaction-isolation>
   <track-connection-by-tx />
   <max-pool-size>5</max-pool-size>
   <min-pool-size>1</min-pool-size>
   <blocking-timeout-millis>2000</blocking-timeout-millis>
   <idle-timeout-minutes>2</idle-timeout-minutes>
   <no-tx-separate-pools/>
   <exception-sorter-class-name>
     org.jboss.resource.adapter.jdbc.vendor.MySQLExceptionSorter
   </exception-sorter-class-name>
 </xa-datasource>
</datasources>

Perhatikan bahwa driver class untuk XA berbeda dengan driver untuk koneksi local transaction. Silakan cari nama XA driver class yang untuk RDBMS yang Anda cari di google :) . Khusus untuk database MySQL yang saya gunakan ketika membuat prototype, property max-pool-size, min-pool-size, dan no-tx-separate-pools harus disertakan (kenapa? belum tau :) ). Untuk keterangan mengenai property-property tersebut silakan lihat disini.

Selanjutnya, tambahkan konfigurasi untuk persistence unit di components.xml dan persistence.xml. Disini kita akan menggunakan 2 persistence unit yang berbeda karena sebuah transaksi paling banyak akan menyertakan 2 koenksi database yang berbeda pula. Jika koneksi database yang diperlukan sebanyak 5 maka kita memerlukan 5 persistence unit yang berbeda.

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

 <persistence-unit name="SALES_APP">
   <provider>org.hibernate.ejb.HibernatePersistence</provider>
   <jta-data-source>java:/SALES_APPDatasource3</jta-data-source>
   <properties>
     <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect"/>
     <property name="hibernate.hbm2ddl.auto" value="update"/>
     <property name="hibernate.show_sql" value="true"/>
     <property name="hibernate.format_sql" value="true"/>
     <property name="hibernate.connection.provider_class" value="com.rudi.framework.MyConnectionProvider" />
     <property name="jboss.entity.manager.factory.jndi.name" value="java:/SALES_APPEntityManagerFactory"/>
     <property name="mfin.datasource.name" value="dataSourceName"/>
 </properties>
 </persistence-unit>
   <persistence-unit name="SALES_APP2">
   <provider>org.hibernate.ejb.HibernatePersistence</provider>
   <jta-data-source>java:/SALES_APPDatasource3</jta-data-source>
   <properties>
     <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect"/>
     <property name="hibernate.hbm2ddl.auto" value="update"/>
     <property name="hibernate.show_sql" value="true"/>
     <property name="hibernate.format_sql" value="true"/>
     <property name="hibernate.connection.provider_class" value="com.rudi.framework.MyConnectionProvider" />
     <property name="jboss.entity.manager.factory.jndi.name" value="java:/SALES_APP2EntityManagerFactory"/>
     <property name="mfin.datasource.name" value="dataSourceName2"/>
   </properties>
 </persistence-unit>
</persistence>

Perhatikan line 15 dan 28. Disitu ditentukan bahwa EntityManager yang akan dihasilkan nantinya (melalui EntityManagerFactory) akan menggunakan ConnectionProvider buatan sendiri.

Perhatikan juga line 17 dan 30. Anda dapat menentukan property karangan sendiri untuk digunakan oleh ConnectionProvider nantinya.

Di file components.xml, tambahkan satu lagi managed-persistence-context, seperti di bawah ini.

<persistence:managed-persistence-context name="entityManager"
 auto-create="true"
 persistence-unit-jndi-name="java:/SALES_APPEntityManagerFactory"/>

 <persistence:managed-persistence-context name="entityManager2"
 auto-create="true"
 persistence-unit-jndi-name="java:/SALES_APP2EntityManagerFactory"/>

Selanjutnya, buat implementasi dari ConnectionProvider, seperti contoh berikut:

package org.rudi.framework;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

import javax.naming.InitialContext;
import javax.naming.NamingException;

import org.hibernate.HibernateException;
import org.hibernate.connection.ConnectionProvider;
import org.jboss.seam.contexts.Contexts;

public class MyConnectionProvider implements ConnectionProvider {
	private InitialContext ctx;

    private String dataSourceNameDefault = "SALES_APPDatasource2";

    public static final String DS_KEY = "rudi.datasource.name";

    private String dataSourceKey;

    public void configure(Properties props) throws HibernateException
    {
        try
        {
            ctx = new InitialContext(props);
            dataSourceKey = props.getProperty(DS_KEY);

        } catch (NamingException e)
        {
            throw new HibernateException(e.getMessage());
        }
    }

    public Connection getConnection() throws SQLException
    {

        try
        {
            if (Contexts.getSessionContext() != null)
            {
                String dataSourceName = (String)Contexts.getSessionContext().get(dataSourceKey);
                javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup("java:/" + dataSourceName);
                System.out.println(">>>returning "+dataSourceName);
                return ds.getConnection();
            }
            else
            {
                javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup("java:/" + dataSourceNameDefault);
                System.out.println(">>>returning default: "+dataSourceNameDefault);
                return ds.getConnection();
            }
        } catch (NamingException e)
        {
            throw new SQLException(e.getMessage());
        }

    }

    public void closeConnection(Connection conn) throws SQLException
    {
        if (conn != null)
        {
            conn.close();
        }
    }

    public void close()
    {
    }

    @Override
    public boolean supportsAggressiveRelease()
    {
        return false;
    }

}

Perhatikan line 43-44, disitulah inti dari class tersebut.

javax.sql.DataSource ds = (javax.sql.DataSource)ctx.lookup("java:/" + dataSourceName);

Line diatas adalah untuk mendapatkan sebuah DataSource yang di bind dengan alamat JNDI sesuai dengan String variable dataSourceName. Variable dataSourceName sendiri didapatkan dari nilai yang disimpan di session context, dengan line berikut ini.

String dataSourceName = (String)Contexts.getSessionContext().get(dataSourceKey);

Di line 28, kita mengakses property custom yang dijelaskan sebelum ini (lihat penjelasan tentang persistence.xml diatas). Value dari property tersebut digunakan di line 43 sebagai key untuk mendapatkan nama dataSource dari session.

Sampai disini, Anda mungkin sudah sangat bosan baca blog ini. Saya sarankan untuk relaks sejenak, nyalakan sebatang rokok atau buka situs ini. Jika Anda merasa sudah siap, silakan dilanjutkan lagi.

Kemudian, di komponen yang digunakan untuk otentikasi (default hasil generate dari seam-gen adalah class Authenticator) kita tentukan koneksi database mana yang akan digunakan pada saat seorang user login.

package com.rudi.action;

import org.jboss.seam.annotations.In;
import org.jboss.seam.annotations.Logger;
import org.jboss.seam.annotations.Name;
import org.jboss.seam.contexts.Contexts;
import org.jboss.seam.log.Log;
import org.jboss.seam.security.Credentials;
import org.jboss.seam.security.Identity;

import com.rudi.CONSTANT;

@Name("authenticator")
public class Authenticator {
	@Logger
	private Log log;

	@In
	Identity identity;
	@In
	Credentials credentials;

	public boolean authenticate() {
		log.info("authenticating {0}", credentials.getUsername());
		if ("admin".equals(credentials.getUsername())) {
			identity.addRole("admin");
			Contexts.getSessionContext().set(CONSTANT.DS1_KEY, "SALES_APPDatasource");
			Contexts.getSessionContext().set(CONSTANT.DS2_KEY, "SALES_APPDatasource2");
			return true;
		}else if("admin2".equals(credentials.getUsername())) {
			identity.addRole("admin");
			Contexts.getSessionContext().set(CONSTANT.DS1_KEY, "SALES_APPDatasource2");
			Contexts.getSessionContext().set(CONSTANT.DS2_KEY, "SALES_APPDatasource3");
			return true;
		}else if("admin3".equals(credentials.getUsername())) {
			identity.addRole("admin");
			Contexts.getSessionContext().set(CONSTANT.DS1_KEY, "SALES_APPDatasource3");
			Contexts.getSessionContext().set(CONSTANT.DS2_KEY, "SALES_APPDatasource");
			return true;
		}
		return false;
	}
}

Kelas diatas hanya sebagai contoh. Perhatikan bahwa untuk setiap user (admin, admin2, admin3), kita memasukkan suatu nilai String ke session. Nilai string tersebut adalah alamat JNDI dari dua koneksi database yang diperlukan.

Berikut adalah ilustrasi mengenai code yang diperlukan di prosedur yang akan menggunakan transaksi antar database.

@Name("transferAction")
public class TransferAction implements Serializable {
  @In EntityManager entityManager;
  @In EntityManager entityManager2;

  public void transferAntarBank(Long idAkunBank1,Long idAkunBank2, BigDecimal jumlah) {
    Transaction.instance().enlist(entityManager);
    Transaction.instance().enlist(entityManager2);
    Akun akunBank1 = entityManager.find(Akun.class, idAkunBank1);
    Akun akunBank2 = entityManager2.find(Akun.class, idAkunBank2);
    akunBank1.debet(jumlah);
    akunBank2.kredit(jumlah);

    entityManager.flush();
    entityManager2.flush();
  }

}

Di line 7 dan 8, kita mencantolkan kedua entityManager kepada Transaction yang sedang berjalan saat itu. Jika kita menggunakan managed persistence context (seperti contoh yang saya buat ini), SEAM secara otomatis akan memulai sebuah transaksi di fase JSF apply request dan dicommit (atau rollback) setelah invokeAction, dan satu transaksi lagi pada saat render response. Untuk keterangan tentang SEAM managed persistence context lihat disini. Untuk keterangan tentang fase-fase request di aplikasi JSF lihat disini.
Selanjutnya… selesai (akhirnya…!!!). Secara prinsip, itu saja yang perlu diketahui. Selamat mencoba. :)

Tags: , ,

3 Responses to “SEAM 2.1.x dengan Multi Database”

  1. Aerie says:

    Pertamaxxxxxxxxxxx

  2. Aerie says:

    Keduaxxxxxxx

  3. urangbiase says:

    Inilah yang sering kutanyakan dalam hati. Kelas berat iki artikelnya, mantap2. Semoga babeh sering2 g ditempat, biar kita bisa blogging ria :D .

Leave a Reply

You can use these tags: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>