JEPLayer ORM 1.0 : transacciones JTA y API fluida
viernes, diciembre 16, 2011 at 11:07PM
jmarranz

He publicado JEPLayer v1.0

JEPLayer es un sencillo ORM sobre JDBC y en esta versión también sobre la Java Transacion API (JTA). JEPLayer es de código abierto con licencia Apache v2

http://jeplayer.googlecode.com

JEPLayer es una herramienta de más bajo nivel que los ORMs de persistencia transparente tal y como Hibernate, JPA o JDO, JEPLayer responde a la necesidad de evitar las tareas más tediosas del uso de JDBC y/o JTA pero sin perder control alguno de las acciones de persistencia tal y como ocurre con los frameworks de alto nivel de persistencia transparente y sin reinventar la rueda como hacen otras herramientas redefiniendo la API JDBC y JTA. JEPLayer no substituye completamente a estas APIs tal que en aquellas acciones en donde JEPLayer no aporta nada respecto a la API JDBC/JTA, el programador puede cuando quiera acceder a los objetos JDBC/JTA que son protagonistas en cada etapa del ciclo de vida de una acción persistente (Connection,PreparedStatement,ResultSet,UserTransaction).

JEPLayer hace un gran énfasis en simplificar de forma extrema la demarcación de transaccione, tanto transacciones JDBC como JTA, en el caso de JTA utilizando la misma semántica que en EJB (o Spring), pero evitando la brutal invasión y viralidad que requieren otras alternativas para simplemente declarar que un conjunto de operaciones persistentes requieren un transacción (el típico REQUIRED).

Como novedades en esta versión se incluye el soporte de transacciones distribuidas JTA incluyendo la transaccionalidad para varias bases de datos (DataSource), falsas transacciones JTA basadas en puro JDBC con la misma semántica que EJB o Spring (aunque incompleta), una API fluida para queries similar a la de JPA incluyendo el uso de parámetros numerados o con nombres y binding automático (configurable) de campos en base de datos y atributos en beans.

 

En javaLobby se pueden consultar ejemplos de uso del API de JEPLayer v0.5 (en la versión en javaHispano no se ven bien los ejemplos al haberse portado la noticia al nuevo portal).

En general la API ha sufrido bastantes cambios en la v1.0, ha continuación mostraré algunos ejemplos de código en donde se muestra la nueva sintaxis, se supone MySQL en el caso de incluir SQL no estándar.

Por ejemplo dada una clase Contact:

public class Contact

{

    protected int id;

    protected String name;

    protected String phone;

    protected String email;

     public Contact(int id, String name, String phone, String email)

    {

        this.id = id;

        this.name = name;

        this.phone = phone;

        this.email = email;

    }

    public Contact()

    {

    }

  /* ... gets and sets ... */

}

 

 

Este es un ejemplo de creación de la tabla (DataSourceFactory es cualquier clase factoría a medida que obtenga un DataSource):

public class CreateDBModel

{

    public static void main(String[] args)

    {

        DataSourceFactory dsFactory = ...;

        DataSource ds = dsFactory.getDataSource();

        JEPLBootNonJTA boot = JEPLBootRoot.get().createJEPLBootNonJTA();

        JEPLNonJTADataSource jds = boot.createJEPLNonJTADataSource(ds);

         try

        {

            JEPLDAL dal = jds.createJEPLDAL();

            dal.createJEPLDALQuery("DROP TABLE IF EXISTS CONTACT").executeUpdate();

             dal.createJEPLDALQuery(

                "CREATE TABLE  CONTACT (" +

                "  ID INT NOT NULL AUTO_INCREMENT," +

                "  EMAIL VARCHAR(255) NOT NULL," +

                "  NAME VARCHAR(255) NOT NULL," +

                "  PHONE VARCHAR(255) NOT NULL," +

                "  PRIMARY KEY (ID)" +

                ")" +

                "ENGINE=InnoDB"

                ).executeUpdate();

        }

        finally

        {

            dsFactory.destroy();

        }

    }

}

La clase DAO correspondiente (algunos métodos son redundantes):

public class ContactDAO implements JEPLResultSetDAOListener<Contact>

{

    protected JEPLDAO<Contact> dao;

   

    public ContactDAO(JEPLDataSource ds)

    {

        this.dao = ds.createJEPLDAO(Contact.class);

        dao.addJEPLListener(this);

    }

 

    public JEPLDAO<Contact> getJEPLDAO()

    {

        return dao;

    }

 

    @Override

    public void setupJEPLResultSet(JEPLResultSet jrs,JEPLTask task)

        throws Exception

    {

    }

 

    @Override

    public Contact createObject(JEPLResultSet jrs) throws Exception

    {

        return new Contact();

    }

   

    @Override

    public void fillObject(Contact obj,JEPLResultSet jrs) throws Exception

    {

        ResultSet rs = jrs.getResultSet();

 

        obj.setId(rs.getInt("ID"));

        obj.setName(rs.getString("NAME"));

        obj.setPhone(rs.getString("PHONE"));

        obj.setEmail(rs.getString("EMAIL"));

    }

 

    public void insert(Contact contact)

    {

        int key = dao.createJEPLDALQuery(

               "INSERT INTO CONTACT (EMAIL, NAME, PHONE) VALUES (?, ?, ?)")

                .addParameters(contact.getEmail(),

                       contact.getName(),contact.getPhone())

                .getGeneratedKey(int.class);

        contact.setId(key);

    }

 

    public void update(Contact contact)

    {

        dao.createJEPLDALQuery(

               "UPDATE CONTACT SET EMAIL = ?, NAME = ?, PHONE = ? WHERE ID = ?")

                .addParameters(contact.getEmail(),contact.getName(),

                       contact.getPhone(),contact.getId())

                .setStrictMinRows(1).setStrictMaxRows(1)

                .executeUpdate();

    }

 

    public boolean delete(Contact obj)

    {

        return deleteById(obj.getId());

    }

 

    public boolean deleteById(int id)

    {

        // Only if there is no "inherited" rows or declared ON DELETE CASCADE

        return dao.createJEPLDALQuery("DELETE FROM CONTACT WHERE ID = ?")

                    .setStrictMinRows(0).setStrictMaxRows(1)

                    .addParameter(id)

                    .executeUpdate() > 0;

    }

 

    public int deleteAll()

    {

        // Only if "inherited" tables are empty or declared ON DELETE CASCADE

        return dao.createJEPLDALQuery("DELETE FROM CONTACT").executeUpdate();

    }

 

    public List<Contact> selectAll()

    {

        return dao.createJEPLDAOQuery("SELECT * FROM CONTACT").getResultList();

    }

 

    public JEPLResultSetDAO<Contact> selectAllResultSetDAO()

    {

        return dao.createJEPLDAOQuery(

                       "SELECT * FROM CONTACT").getJEPLResultSetDAO();

    }

 

    public List<Contact> selectJEPLDAOQueryRange(int from,int to)

    {

        return dao.createJEPLDAOQuery("SELECT * FROM CONTACT")

                .setFirstResult(from)

                .setMaxResults(to - from)

                .getResultList();

    }

 

    public Contact selectById(int id)

    {

        return dao.createJEPLDAOQuery("SELECT * FROM CONTACT WHERE ID = ?")

                .addParameter(id)

                .getSingleResult();

    }

 

    public List<Contact> selectByNameAndEMail(String name,String email)

    {

        return dao.createJEPLDAOQuery(

               "SELECT * FROM CONTACT WHERE NAME = ? AND EMAIL = ?")

                .addParameters(name,email)

                .getResultList();

    }

 

    public int selectCount()

    {

        return dao.createJEPLDALQuery("SELECT COUNT(*) FROM CONTACT")

                    .getOneRowFromSingleField(int.class);

    }

}

En la versión 1.0 se puede evitar el binding manual propio de implementar JEPLResultSetDAOListener y registrar una implementación por defecto (JEPLResultSetDAOListenerDefault) que hace el binding automáticamente. En el siguiente método que podemos añadir al DAO se utiliza el binding automático en una query concreta (podría registrarse a nivel global del DAO) 

    public List<Contact> selectAllExplicitResultSetDAOListenerBean()

    {

        JEPLResultSetDAOListenerDefault<Contact> listener =

                dao.getJEPLDataSource()

               .createJEPLResultSetDAOListenerDefault(Contact.class);

 

        return dao.createJEPLDAOQuery("SELECT * FROM CONTACT")

                .addJEPLListener(listener)

                .getResultList();

    }

 JEPLResultSetDAOListenerDefault admite interceptar atributos concretos que no sean mapeables automáticamente con el campo en la base de datos a través de JEPLRowBeanMapper. En este ejemplo es solamente ilustrativo, el mapeo automático funcionaría correctamente:

    public List<Contact> selectAllExplicitResultSetDAOListenerBeanWithMapper()

    {

        JEPLRowBeanMapper<Contact> rowMapper = new JEPLRowBeanMapper<Contact>()

        {

            public boolean setColumnInBean(Contact obj,JEPLResultSet jrs,

                       int col, String columnName, Object value, Method setter)

            {

                if (columnName.equalsIgnoreCase("email"))

                {

                    obj.setEmail((String)value);

                    return true;

                }

                return false;

            }

        };

        JEPLResultSetDAOListenerDefault<Contact> listener =

                dao.getJEPLDataSource()

                .createJEPLResultSetDAOListenerDefault(Contact.class,rowMapper);

 

        return dao.createJEPLDAOQuery("SELECT * FROM CONTACT")

                .addJEPLListener(listener)

                .getResultList();

    }


  Los parámetros de las queries pueden ser ahora también numerados:

    int key = dao.createJEPLDALQuery(

                "INSERT INTO CONTACT (EMAIL, NAME, PHONE) VALUES (?1, ?2, ?3)")

            .setParameter(1,contact.getEmail())

            .setParameter(2,contact.getName())

            .setParameter(3,contact.getPhone())

            .getGeneratedKey(int.class);

O con nombres (como en JPA):

    int key = dao.createJEPLDALQuery(

        "INSERT INTO CONTACT (EMAIL, NAME, PHONE) VALUES (:email,:name,:phone)")

            .setParameter("email",contact.getEmail())

            .setParameter("name",contact.getName())

            .setParameter("phone",contact.getPhone())

            .getGeneratedKey(int.class);

Se mantiene las técnicas de IoC a lo largo del ciclo de vida de cada acción persistente a través de listeners: JEPLConnectionListener, JEPLPreparedStatementListener, JEPLResultSetDALListener y JEPLResultSetDAOListener  (ver ejemplos en javaLobby y en manual).

Sigue existiendo aunque ahora usando la API fluida la posibilidad de consultar con un ResultSet especial pero ORM:

    public JEPLResultSetDAO<Contact> selectAllResultSetDAO() // Método de ContactDAO
    {
        return dao.createJEPLDAOQuery(
               "SELECT * FROM CONTACT").getJEPLResultSetDAO();
    }

    ContactDAO dao = ...;

    JEPLResultSetDAO<Contact> resSetDAO = dao.selectAllResultSetDAO();

    if (resSetDAO.isClosed()) throw new RuntimeException("UNEXPECTED");

    while(resSetDAO.next())

    {

        Contact contact = resSetDAO.getObject();

        System.out.println("Contact: " + contact.getName());

    }

    // Now we know is closed 

    if (!resSetDAO.isClosed()) throw new RuntimeException("UNEXPECTED");

 

Que al implementar la interfaz List podemos verlo como un List que carga objectos bajo demanda de la base de datos a medida que lo recorremos:


    ContactDAO dao = ...;

    List<Contact> resSetDAO = dao.selectAllResultSetDAO();

    if (((JEPLResultSetDAO)resSetDAO).isClosed())

        throw new RuntimeException("UNEXPECTED");

    for(Contact contact : resSetDAO) // Uses Iterator<Contact> 

    {

        System.out.println("Contact: " + contact.getName());

    }

    // Now we know is closed 

    if (!((JEPLResultSetDAO)resSetDAO).isClosed())

        throw new RuntimeException("UNEXPECTED");

 

Es posible que no necesitemos toda la parafernalia de DAOs, beans etc y simplemente queremos extraer datos a medida de la forma más simple posible sin todo el tedio de abrir y cerrar conexiones etc, para ello tenemos los objetos JEPLCachedResultSet que recogen los resultados y pueden ser recorridos como un ResultSet pero desconectado (suponemos dos filas de resultado):

 

    JEPLDataSource jds = ...;

    JEPLDAL dal = jds.createJEPLDAL();

    JEPLCachedResultSet resSet = dal.createJEPLDALQuery(

                "SELECT COUNT(*) AS CO,AVG(ID) AS AV FROM CONTACT")

                .getJEPLCachedResultSet();

 

    String[] colNames = resSet.getColumnLabels();

    if (colNames.length != 2) throw new RuntimeException("UNEXPECTED");

    if (!colNames[0].equals("CO")) throw new RuntimeException("UNEXPECTED");

    if (!colNames[1].equals("AV")) throw new RuntimeException("UNEXPECTED");

    if (resSet.size() != 2) throw new RuntimeException("UNEXPECTED");

 

    int count = resSet.getValue(1, 1, int.class); // Row 1, column 1 

    count = resSet.getValue(1, "CO", int.class);

 

    int avg = resSet.getValue(1, 2, int.class); // Row 1, column 2 

    avg = resSet.getValue(1, "AV", int.class);

  

 

Respecto a transaccione, podemos como siempre agrupar llamadas persistentes en la misma transacción:

 

    final JEPLDataSource jds = ...;

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        public Contact exec() throws Exception

        {

            Contact contact = new Contact();

            contact.setName("A Contact object");

            contact.setPhone("9999999");

            contact.setEmail("contact@world.com");

            ContactDAO dao = new ContactDAO(jds);

            dao.insert(contact);

 

            Contact contact2 = dao.selectById(contact.getId());

            return contact2;

        }

    };

    Contact contact = jds.exec(task);

 

Aunque ahora se distinguen transacciones JDBC (non-JTA) de transacciones JTA. Ejemplo JDBC:

 

    JEPLNonJTADataSource jds = boot.createJEPLNonJTADataSource(ds);

    jds.setDefaultAutoCommit(false);

    ...

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        public Contact exec() throws Exception

        {

             ...

        }

    };

    Contact contact = jds.exec(task);

 

la transaccionalidad  puede también declararse a través de una anotación:

 

    JEPLNonJTADataSource jds = ...;

    ...

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        @JEPLTransactionalNonJTA

        public Contact exec() throws Exception

        {

            ... 

        }

    };

    Contact contact = jds.exec(task);

 

O a través de un parámetro autoCommit (este ejemplo no es transaccional porque autoCommit es true):

    JEPLNonJTADataSource jds = ...;

    ...

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        public Contact exec() throws Exception

        {

            ... 

        }

    };

    Contact contact = jds.exec(task,true); 

 

En esta versión podemos declarar transacciones JTA

    UserTransaction txn = ...;

    TransactionManager txnMgr = ...;

    DataSource ds = ...;

 

    JEPLBootJTA boot = JEPLBootRoot.get().createJEPLBootJTA();

    boot.setUserTransaction(txn);

    boot.setTransactionManager(txnMgr);

 

    JEPLJTADataSource jds = boot.createJEPLJTADataSource(ds);

    jds.setDefaultJEPLTransactionPropagation(

               JEPLTransactionPropagation.REQUIRED);

    ...

 

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        public Contact exec() throws Exception

        {

            return ...; // Database actions 

        }

    };

    Contact contact = jds.exec(task);

 

También con una anotación:

 

    ...

 

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        @JEPLTransactionalJTA 

        public Contact exec() throws Exception

        {

            return ...; // Database actions 

        }

    };

    Contact contact = jds.exec(task);

 

O con un parámetro en cada caso:

   JEPLTask<Contact> task = new JEPLTask<Contact>()

   {

     @Override

     public Contact exec() throws Exception

     {

         return ...; // Database actions 

     }

   };

   Contact contact = jds.exec(task,JEPLTransactionPropagation.REQUIRED);

 

Con la posibilidad de poder definir de la misma forma (configuración global, anotación, parámetro en cada llamada) transacciones distribuidas que afecten a varias bases de datos y por tanto utilizando two-phase commit:

    UserTransaction txn = ...;

    TransactionManager txnMgr = ...;

    DataSource ds1 = ...;

    DataSource ds2 = ...;

 

    JEPLBootJTA boot = JEPLBootRoot.get().createJEPLBootJTA();

    boot.setUserTransaction(txn);

    boot.setTransactionManager(txnMgr);

 

    JEPLJTAMultipleDataSource jdsMgr = boot.getJEPLJTAMultipleDataSource();

    final JEPLJTADataSource jds1 = boot.createJEPLJTADataSource(ds1);

    final JEPLJTADataSource jds2 = boot.createJEPLJTADataSource(ds2);

 

    JEPLTask<Contact> task = new JEPLTask<Contact>()

    {

        @Override

        public Contact exec() throws Exception

        {

            // Database actions using jds1 and jds2 

            return ...;

        }

    };

    jdsMgr.exec(task);

 

Utilizando la misma semántica transaccional aplicada a un DataSource pero en este caso a todos. Por ejemplo:

  JEPLJTAMultipleDataSource jdsMgr = boot.get JEPLJTAMultipleDataSource();

  jdsMgr.setDefaultJEPLTransactionPropagation(

           JEPLTransactionPropagation.REQUIRED);

 

Idem anotación y parámetro en exec de  JEPLJTAMultipleDataSource

Que lo disfruteis.



    public JEPLResultSetDAO<Contact> selectAllResultSetDAO()
    {
        return dao.createJEPLDAOQuery(
               "SELECT * FROM CONTACT").getJEPLResultSetDAO();
    }
Article originally appeared on javaHispano (http://www.javahispano.org/).
See website for complete article licensing information.