Montag, 13. September 2010

Generic Database Service for Java EE 5

Java EE 5 unterscheidet sich deutlich von seiner Vorgängerversion. Java EE 5 ist leichtgewichtig und nutzt Annotations und "Configuration by Exception" anstelle von Deployment Deskriptoren. Die Implementierung von Home- und speziellen Komponentenschnittstellen ist nun obsolet, sodass eine einfache Java Schnittstelle ausreichend ist. Java EE 6 geht sogar soweit, dass sofern EJBs nur lokal verwendet werden, gar keine Schnittstelle mehr zu implementieren ist.

Die Deklaration einer Java EE 5 Entität erfolgt über Annotationen. Die Annotation @Entity ist eine Marker-Annotation, die dem Persistentprovider anzeigt, dass es sich um ein Objekt handelt, das in einer Datenbank gespeichert werden soll. Zusätzlich zu der Marker-Annotation ist noch das Primärschlüsselfeld festzulegen. Das Primärschlüsselfeld wird mit @Id und @GeneratedValue (optional mit entsprechender Generierungsstrategie) gekennzeichnet. An der Entität können global zugreifbar mehrere "Named Queries" deklariert werden. "Named Queries" bieten sich im JPA-Umfeld aufgrund der Annotierung an der Entität an.

Komplexere Datenbankstrukturen sind mit der JPA umsetzbar. Die Standardrelationen einer Datenbank bis hin zu Vererbungsstrategien sind programmierbar. JPA unterstützt neben einfachen Datenbankabfragen mit der JPQL auch komplexere Joins und native SQL Queries. Typsichere und objektorientierte Abfragen mit der Criteria API werden allerdings erst in JPA 2 mit dem Java EE 6 Standard unterstützt.

Ein rudimentärer Datenbankservice  ist relativ einfach mit Java Generics umsetzbar. Der Service bietet dabei allgemeine Dienste zum Erzeugen, Löschen, Aktualisieren und Lesen von Entitäten an. Der in diesem Blogbeitrag implementierte Datenbankservice nutzt den EntityManager und die JPA Query Schnittstelle von Java EE 5.

Szenario: Datenbank Service

Schnittstelle des Datenbankservices:

package ccd.jee.services.db;

import java.util.List;

public interface DBService<T> {

    public void store(T t);
    public void remove(T t);
    public T update(T t);
    public T find(Class<T> t, Object id);
    public List<T> findByNamedQuery(String queryName);  
}

Implementierung des Datenbankservices (noch nicht vollständig ausprogrammiert!):

package ccd.jee.services.db;

import java.util.List;
import javax.ejb.Local;
import javax.ejb.Stateless;
import javax.ejb.TransactionAttribute;
import javax.ejb.TransactionAttributeType;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;

@Stateless
@Local(DBService.class)
public class DBServiceBean<T> implements DBService<T> {

    @PersistenceContext
    private EntityManager em;
     
    @Override
    public void store(T t) {

        em.persist(t);            
    }

    @Override
    public void remove(T t) {

        final T mt = em.merge(t);   
        em.remove(mt);      
    }

    @Override
    public T find(Class<T> t, Object id) {

        return em.find(t, id);      
    }

    @Override
    public T update(T t) {

        final T mt = em.merge(t);
        return mt;      
    }

    @Override
    @TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
    public List<T> findByNamedQuery(String queryName) {

        final Query query = em.createNamedQuery(queryName);
        return query.getResultList();
    }
}

Entität eines Blogeintrages (ohne den equals und hashCode Vertrag zu erfüllen!):

package ccd.jee.domain.blog;

import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;

@Entity
@NamedQueries({
      
        @NamedQuery(name = "queryAll", query= "SELECT o FROM BlogEntry o")
})
public class BlogEntry implements Serializable {
  
    @Id
    @GeneratedValue
    private int id;
  
    private String title;
    private String entry;
  
    public int getId() {
        return id;
    }
  
    public String getTitle() {
        return title;
    }
  
    public String getEntry() {
        return entry;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public void setEntry(String entry) {
        this.entry = entry;
    }

    @Override
    public String toString() {
        return "BlogEntry [entry=" + entry + ", title=" + title + "]";
    }
}

Schnittstelle des Blog Services:

package ccd.jee.session.beans.blog;

import java.util.List;
import ccd.jee.domain.blog.BlogEntry;

public interface Blog {

    public void storeBlogEntry(BlogEntry blogEntry);
    public void removeBlogEntry(BlogEntry blogEntry);
    public BlogEntry updateBlogEntry(BlogEntry blogEntry);
    public BlogEntry findBlogEntry(Class<BlogEntry> blogEntryClass, Integer id);   
    public List<BlogEntry> getAllBlogEntries();
}

Implementierung des Blog Services:

package ccd.jee.session.beans.blog;

import javax.ejb.EJB;
import javax.ejb.Remote;
import javax.ejb.Stateless;
import javax.naming.NamingException;
import ccd.jee.domain.blog.BlogEntry;
import ccd.jee.services.db.DBService;

@Stateless(mappedName="ejb/BlogService")
@Remote(Blog.class)
public class BlogBean implements Blog {

    @EJB
    private DBService<BlogEntry> dbService;
  
    @Override
    public void storeBlogEntry(BlogEntry blogEntry) {
      
        dbService.store(blogEntry);
    }
  
    @Override
    public void removeBlogEntry(BlogEntry blogEntry) {
      
        dbService.remove(blogEntry);
    }

    @Override
    public BlogEntry updateBlogEntry(BlogEntry blogEntry) {
              
        return dbService.update(blogEntry);
    }
  
    @Override
    public BlogEntry findBlogEntry(Class<BlogEntry> blogEntryClass, Integer id) {
      
        return dbService.find(BlogEntry.class, id);
    }

    @Override
    public List<BlogEntry> getAllBlogEntries() {
      
        return dbService.findByNamedQuery("queryAll");
    }
}

Unit-Test des Blog Services:

package ccd.jee.session.beans.blog;

import static org.junit.Assert.assertEquals;
import java.util.List;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import org.junit.BeforeClass;
import org.junit.Test;
import ccd.jee.domain.blog.BlogEntry;
import ccd.jee.session.beans.blog.Blog;

public class BlogTest {

    private static final String BLOG_ENTRY_DATA = "Blogeintrag";
    private static final String BLOG_ENTRY_TITEL = "Titel";  
    private static final int FIND_ID = 1;
    private static final String JNDI_NAME = "ejb/BlogService";
    private static Context context;
    private static Blog blog;
  
    @BeforeClass
    public static void setUp() throws NamingException {
      
        context = new InitialContext();      
        blog = (Blog) context.lookup(JNDI_NAME);              
    }
  
    @Test
    public void testStore() {
              
        blog.storeBlogEntry(getBlogEntry());
    }

    @Test
    public void testDelete() {
  
        final BlogEntry entry = getBlogEntry();
        blog.storeBlogEntry(entry);
        blog.removeBlogEntry(entry);
    }
  
    @Test
    public void testUpdate() {
  
        final BlogEntry entry = getBlogEntry();
        blog.storeBlogEntry(entry);
      
        entry.setTitle(BLOG_ENTRY_TITEL);
        blog.updateBlogEntry(entry);
    }
  
    @Test
    public void testFind() {
  
        final BlogEntry firstEntry = getBlogEntry();
        blog.storeBlogEntry(firstEntry);
      
        final BlogEntry secondEntry = blog.findBlogEntry(BlogEntry.class, new Integer(FIND_ID));
      
        assertEquals(BLOG_ENTRY_TITEL, secondEntry.getTitle());
        assertEquals(BLOG_ENTRY_DATA, secondEntry.getEntry());
    }
  
    @Test
    public void testFindAll() {
      
        final BlogEntry entry = getBlogEntry();
        blog.storeBlogEntry(entry);
          
        final List<BlogEntry> list = blog.getAllBlogEntries();      
        for(BlogEntry blogEntry : list) {
          
            assertEquals(BLOG_ENTRY_TITEL, blogEntry.getTitle());
            assertEquals(BLOG_ENTRY_DATA, blogEntry.getEntry());
        }
    }
  
    private BlogEntry getBlogEntry() {
              
        return getBlogEntry(BLOG_ENTRY_TITEL, BLOG_ENTRY_DATA);
    }
  
    private BlogEntry getBlogEntry(String title, String entry) {
      
        final BlogEntry blogEntry = new BlogEntry();
        blogEntry.setTitle(title);
        blogEntry.setEntry(entry);
      
        return blogEntry;
    }
}

Das Szenario veranschaulicht, wie einfach die Programmierung eines allgemeinen Datenbankservices mit den Boardmitteln von Java EE 5 ist. Der Datenbankservice ist noch nicht vollständig ausprogrammiert, sodass beispielsweise keine Abfragen mit Parametern und Löschoperationen per eindeutigem Identifier (UID) unterstützt werden. Die Erweiterung um diese Funktionalitäten ist allerdings unproblematisch. Mit dem Datenbankservice sind komplexere Entitäten und deren Relationen speicherbar. Die Relationen zwischen den Entitäten basieren auf dem Domainmodell und sind transparent  für den Datenbankservice.

Reflektiert man, wie aufwendig die Programmierung von Entity Beans im EJB 2 Standard ist und wieviel mehr Quellcode (Boilerplate Code) sowie Deployment Deskriptoren erstellt werden müssen, ist die JPA eine deutliche Erleichterung bei der Programmierung von Datenbankanbindungen.

Der Datenbankservice ist ein gutes Beispiel für die Anwendung des DRY-Prinzips. Würde man in jedem EJB den EntityManager zum Speichern von Daten anwenden, hätte man sehr viel mehr redundanten datenbankspezifischen Quellcode zu erstellen. Generische Programmierung ist deshalb geeignet, um das DRY-Prinzip einzuhalten. Abzuraten ist allerdings von einer generischen Programmierung mit der Basisklasse "Object", weil dabei die Typsicherheit verloren geht. Die Basisklasse "Object" sollte deshalb nur gezielt in geringer Dosis ihre Anwendung finden.

stay tuned


Der Rechtshinweis des Java Blog für Clean Code Developer ist bei der Verwendung und Weiterentwicklung des Quellcodes des Blogeintrages zu beachten.