Alternativa a Hibernate u ORM y ejemplo de jOOQ

Escrito por el , actualizado el .
java programacion planeta-codigo blog-stack
Comentarios

Los ORMs nos han facilitado el acceso a los datos de una base de datos relacional. Han solucionado algunos problemas pero traído consigo otros nuevos como el problema N+1 o la pérdida de control del modelo relacional. jOOQ forma parte de una nueva generación de herramientas que puede sustituir o complementar a otras como Hibernate. Y después de haberlo usado considero que puede ser factible.

jOOQ

Con el auge de los lenguajes de programación orientados a objetos han surgido varias herramientas que intentan hacer que el trabajo de unir el mundo orientado a objetos del lenguaje que empleemos y el modelo relacional de las bases de datos sea más transparente, estas herramientas son conocidas como Object Realtional Mapping (ORM). Una de las más conocidas y usada en la plataforma Java es Hibernate. Sin embargo, aunque facilitan el acceso a los datos no están exentas de problemas y están surgiendo nuevas alternativas para tratar de solventar algunos de ellos, una de ellas es jOOQ.

Si hemos usado Hibernate sabremos que aunque este ampliamente usado facilitando la conversión entre el modelo relacional en base de datos y el modelo orientado a objetos del lenguaje Java también presenta problemas. Uno de los problemas es que al abstraer el acceso a base de datos no somos tan conscientes de las sentencias SQL que se envían a la base de datos provocando los problemas N+1 y que la aplicación sea lenta, poco eficiente y sobrecargar la base de datos en el caso de realizar demasiadas consultas SQL. Otro problema es que cuando necesitamos realizar una consulta compleja o avanzada el lenguaje HQL no nos ofrezca todo lo que necesitamos haciendo que tengamos que escribir directamente la consulta en lenguaje SQL con lo que perdemos la validación del compilador y si usamos una funcionalidad específica de un motor de base de datos la independencia del mismo. También puede ocurrirnos que diseñamos los modelos para complacer al framework de persistencia ORM.

jOOQ es una herramienta que facilita el acceso a la base de datos usando un enfoque diferente de los ORM, no trata de crear una abstracción sobre la base de datos relacional sino que pone el modelo relacional como elemento central de la aplicación en cuanto a la persistencia. Algunas de las características destacables de jOOQ son:

  • La base de datos primero: los datos son probablemente lo más importante de una aplicación. En los ORM los modelos de objetos dirigen el modelo de base de datos, no siempre es sencillo (más) en bases de datos heredadas que no tienen la estructura necesaria usable por los ORMs. En jOOQ el modelo relacional dirige el modelo de objetos, para jOOQ el modelo relacional es más importante que el modelo de objetos.
  • jOOQ usa SQL como elemento central: en jOOQ se pueden construir las SQLs usando una API fluida con la que el compilador puede validar la sintaxis, metadatos y tipos de datos. Se evitan y se detectan rápidamente los errores de sintaxis con la ayuda del compilador y con la ayuda de un IDE se ofrece asistencia de código que facilita el uso de la API. Está a un nivel bastante cercano al lenguaje SQL.
  • SQL con tipado seguro: las sentencias se pueden construir usando código Java con la que el compilador validará el código y que los tipos de los datos usados sean los correctos, los errores los encontraremos en tiempo de compilación en vez de en tiempo de ejecución. jOOQ proporciona un DSL y una API fluida de fácil uso y lectura.
  • Generación de código: jOOQ genera clases a partir de los metadatos (el modelo relacional) de la base de datos. Cuando se renombre una tabla o campo en base de datos generados los modelos el compilador indicará los errores de compilación. Si en algún momento hay que renombrar una columna de la base de datos deberemos modificar los modelos, jOOQ permite regenerar las clases Java de acceso a la base de datos y el compilador nos avisará de aquello que no esté sincronizado entre la base de datos y el código Java.
  • Multi-Tenancy: permite configurar la base de datos a la que se accederá en desarrollo, pruebas y producción.
  • Active Records: jOOQ puede generar el código de acceso a la base de datos a partir del esquema, estas clases emplean el patrón Active Record. La implementación de este patrón ya proporciona las operaciones CRUD (uno de los avances de Hibernate) con lo que no tendremos que escribirlas para cada uno de los modelos de la aplicación, nos ahorraremos mucho código. Este código que se genera es opcional, jOOQ puede usarse simplemente para generar las sentencias SQL y usar JDBC sin la abstracción de los Active Records.
  • Estandarización: las bases de datos tienen diferencias en los dialectos SQL. jOOQ realiza transformaciones de expresiones SQL comunes a la correspondencia de la base de datos de forma que las SQLs escritas funcionen en todas las bases de datos de forma transparente, esto permite migrar de un sistema de datos sin cambiar el código de la aplicación. Este también era un avance proporcionado por los ORM, incluido Hibernate.
  • Ciclo de vida de las consultas: proporciona llamadas o hooks de forma que se puedan añadir comportamientos, por ejemplo para logging, manejo de transacciones, generación de identificadores, transformación de SQLs y más cosas.
  • Procedimientos almacenados: los procedimientos almacenados son ciudadanos de primera clase y pueden usarse de forma simple al contrario de lo que sucede en los ORM. Para algunas tareas los procedimientos almacenados son muy útiles y más eficientes.

Los ORMs ofrecen como ventajas sobre el uso directo de JDBC la implementación de las operaciones CRUD, construir las SQLs con una API en vez de concatenando Strings propensos a errores al modificarlos y la independencia del motor de base de datos usado pudiendo cambiar a otro sin afectar al código de la aplicación. La navegación de las relaciones es más explícita que en Hibernate y obtener datos de múltiples tablas con jOOQ diferente.

Si nos convencen estas características y propiedades de jOOQ podemos empezar leyendo la guía de inicio donde se comenta los primeros pasos para usarlo. La documentación de jOOQ está bastante bien explicada pero no se comentan algunas cosas que al usarlo en un proyecto tendremos que buscar.

En el siguiente ejemplo mostraré como usar jOOQ y la configuración necesaria para emplearlo junto con Spring. En la siguiente configuración de Spring usando únicamente código Java se construye un DataSource, un Datasource con soporte de transacciones para el acceso a la base de datos, un ConnectionProvider de jOOQ que usará el DataSource para obtener las conexiones a la base de datos, con la clase Configuration realizamos la configuración de jOOQ y finalmente DSLContext es el objeto que usaremos para construir las sentencias SQL.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
package info.blogstack.services.spring;

import info.blogstack.persistence.daos.AdsenseDAO;
import info.blogstack.persistence.daos.AdsenseDAOImpl;
import info.blogstack.persistence.daos.ImportSourceDAO;
import info.blogstack.persistence.daos.ImportSourceDAOImpl;
import info.blogstack.persistence.daos.IndexationDAO;
import info.blogstack.persistence.daos.IndexationDAOImpl;
import info.blogstack.persistence.daos.LabelDAO;
import info.blogstack.persistence.daos.LabelDAOImpl;
import info.blogstack.persistence.daos.NewsletterDAO;
import info.blogstack.persistence.daos.NewsletterDAOImpl;
import info.blogstack.persistence.daos.PostDAO;
import info.blogstack.persistence.daos.PostDAOImpl;
import info.blogstack.persistence.daos.PostsIndexationsDAO;
import info.blogstack.persistence.daos.PostsIndexationsDAOImpl;
import info.blogstack.persistence.daos.PostsLabelsDAO;
import info.blogstack.persistence.daos.PostsLabelsDAOImpl;
import info.blogstack.persistence.daos.SourceDAO;
import info.blogstack.persistence.daos.SourceDAOImpl;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;
import org.jooq.ConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.SQLDialect;
import org.jooq.impl.DSL;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.impl.DefaultConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.ResourceTransactionManager;

@Configuration
@ComponentScan({ "info.blogstack" })
@EnableTransactionManagement
public class AppConfiguration {

    @Bean(destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:./misc/database/app");
        ds.setUsername("sa");
        ds.setPassword("sa");
        return ds;
    }

    @Bean
    public DataSource transactionAwareDataSource(DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }
    
    @Bean
    public ResourceTransactionManager transactionManager(DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }
    
    @Bean
    public ConnectionProvider connectionProvider(DataSource dataSource) {
        return new DataSourceConnectionProvider(dataSource);
    }
    
    @Bean
    public org.jooq.Configuration config(ConnectionProvider connectionProvider) {
        DefaultConfiguration config = new DefaultConfiguration();
        config.set(connectionProvider);
        config.set(SQLDialect.H2);
        return config;
    }
    
    @Bean
    public DSLContext dsl(org.jooq.Configuration config) {
        return DSL.using(config);
    }
    
    @Bean
    public AdsenseDAO adsenseDAO(DSLContext context) {
        return new AdsenseDAOImpl(context);
    }
    
    @Bean
    public ImportSourceDAO importSourceDAO(DSLContext context) {
        return new ImportSourceDAOImpl(context);
    }
    
    @Bean
    public IndexationDAO indexationDAO(DSLContext context) {
        return new IndexationDAOImpl(context);
    }
    
    @Bean
    public LabelDAO labelDAO(DSLContext context) {
        return new LabelDAOImpl(context);
    }
    
    @Bean
    public PostDAO postDAO(DSLContext context) {
        return new PostDAOImpl(context);
    }
    
    @Bean
    public PostsIndexationsDAO postsIndexationsDAO(DSLContext context) {
        return new PostsIndexationsDAOImpl(context);
    }
    
    @Bean
    public PostsLabelsDAO postsLabelsDAO(DSLContext context) {
        return new PostsLabelsDAOImpl(context);
    }
    
    @Bean
    public SourceDAO sourceDAO(DSLContext context) {
        return new SourceDAOImpl(context);
    }
    
    @Bean
    public NewsletterDAO newsletterDAO(DSLContext context) {
        return new NewsletterDAOImpl(context);
    }
}

Una de las cosas que tendremos que resolver es que al generar código y usar el patrón Active Record, si lo usamos ya que podemos usar jOOQ para generar únicamente las sentencias SQL o en los casos que lo hagamos pudiendo combiar Active Records para unos casos y sentencias SQL con JDBC para otros, puede que necesitemos incluir campos adicionales a los presentes en la base de datos que manejen cierta lógica en la aplicación, también puede que necesitemos incluir métodos de lógica de negocio adicionales. Para incluir estos datos y métodos tendremos que extender la clase Active Record que genera jOOQ. En aquellos sitios de la aplicación que necesitemos usar esas propiedades y métodos adicionales deberemos transformar la instancia de la clase que usa jOOQ (PostRecord) por la clase que tenga esos datos adicionales (AppPostRecord). Para ello la API de la clase Record ofrece el método into o from como muestro en el código de AppPostRecord a continuación. Esta es la solución que he usado en Blog Stack.

jOOQ genera automáticamente las clases que implementa el patrón Active Record y dispondremos de los métodos CRUD heredados de la clase Record.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
package info.blogstack.persistence.records;

import info.blogstack.misc.Globals;
import info.blogstack.misc.Utils;
import info.blogstack.persistence.jooq.Keys;
import info.blogstack.persistence.jooq.tables.interfaces.IPost;
import info.blogstack.persistence.jooq.tables.records.PostRecord;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;

public class AppPostRecord extends PostRecord {

    private static final long serialVersionUID = 2075090879800194733L;

    private String content;

    private Boolean fresh;

    public AppPostRecord() {
        this.fresh = Boolean.FALSE;
    }
    
    public String getContent() {
        if (content == null && getContentcompressed() != null) {
            try {
                GZIPInputStream zis = new GZIPInputStream(new ByteArrayInputStream(getContentcompressed()));
                StringWriter sw = new StringWriter();
                IOUtils.copy(zis, sw);
                zis.close();
                sw.close();

                content = sw.toString();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return content;
    }

    public void setContent(String content) {
        this.content = content;
        try {
            String c = (content == null) ? "" : content;
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            GZIPOutputStream zos = new GZIPOutputStream(baos);
            StringReader sr = new StringReader(c);
            IOUtils.copy(sr, zos);
            sr.close();
            zos.close();

            setContentcompressed(baos.toByteArray());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public String getContentExcerpt() {
        Document document = Jsoup.parse(getContent());
        String text = document.text();
        
        int i = Math.min(text.length(), Globals.POST_EXCERPT_LENGHT);
        int n = text.indexOf(' ', i);
        if (n == -1) {
            n = i;
        }
        
        return text.substring(0, n);
    }

    public Boolean isFresh() {
        return fresh;
    }

    public void setFresh(Boolean fresh) {
        this.fresh = fresh;
    }

    public void updateHash() {     
        setHash(Utils.getHash(this, fetchParent(Keys.POST_SOURCE_ID)));
    }
    
    public DateTime getConsolidatedUpdateDate() {
        if (getUpdatedate() != null) {
            return getUpdatedate();
        } else if (getPublishdate() != null) {
            return getPublishdate();
        } else {
            return getCreationdate();
        }
    }

    public DateTime getConsolidatedPublishDate() {
        if (getPublishdate() != null) {
            return getPublishdate();
        } else {
            return getCreationdate();
        }
    }
    
    @Override
    public void setCreationdate(DateTime creationDate) {
        super.setCreationdate(creationDate);
        setDate(getConsolidatedUpdateDate());
    }
    
    @Override
    public void setUpdatedate(DateTime updateDate) {
        super.setUpdatedate(updateDate);
        setDate(getConsolidatedUpdateDate());
    }
    
    @Override
    public void setPublishdate(DateTime publishDate) {
        super.setPublishdate(publishDate);
        setDate(getConsolidatedUpdateDate());
    }

    @Override
    public void from(IPost from) {
        super.from(from);
        if (from instanceof AppPostRecord) {
            AppPostRecord afrom = (AppPostRecord) from;
            this.setFresh(afrom.isFresh());
            this.setContent(afrom.getContent());
        }
    }
}

Los desarrolladores de jOOQ abogan por la eliminación de capas en la arquitectura de la aplicación pero puede que aún preferimos desarrollar una capa que contenga las consultas a la base de datos que sea usada y compartida por el resto la aplicación para el acceso los datos, quizá más que una capa en este caso es una forma de organizar el código. Los Active Records proporcionan algunos métodos de consulta pero probablemente necesitaremos más. En el siguiente ejemplo podemos ver como son las consultas con jOOQ. Si necesitamos métodos de búsqueda adicionales a los que por defecto jOOQ proporciona en Blog Stack he creado una clase DAO por cada entidad de la base de datos. En el siguiente ejemplo se puede ver como se construyen las sentencias SQL con jOOQ usando su API fluida.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package info.blogstack.persistence.daos;

import info.blogstack.persistence.jooq.tables.records.LabelRecord;
import info.blogstack.persistence.jooq.tables.records.NewsletterRecord;
import info.blogstack.persistence.jooq.tables.records.PostRecord;
import info.blogstack.persistence.jooq.tables.records.SourceRecord;

import java.util.Collection;
import java.util.List;
import java.util.Map;

public interface PostDAO {

    List<PostRecord> findAll();

    List<PostRecord> findAll(Pagination pagination);

    List<PostRecord> findAllBySource(SourceRecord source, Pagination pagination);

    List<PostRecord> findAllByYearMonth(Integer year, Integer month);

    List<PostRecord> findAllByLabel(LabelRecord label, Pagination pagination);
    
    List<PostRecord> findAllByShared(boolean shared);
    
    List<PostRecord> findNewsletter();

    PostRecord findByURL(String url);

    PostRecord findByHash(String hash);

    Long countAll();

    Long countBy(SourceRecord source);

    Long countBy(LabelRecord label);
    
    Long countBy(NewsletterRecord newsletter);

    Long countAuthors();

    List<Map<String, Object>> getArchiveByDates();

    List<Map<String, Object>> getArchiveByLabels();

    List<Map<String, Object>> getArchiveBySources();
    
    List<Map<String, Object>> getArchiveByNewsletter(NewsletterRecord newsletter);
    
    int updateNewsletter(Collection<PostRecord> posts, NewsletterRecord newslettter);
}
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package info.blogstack.persistence.daos;

import static info.blogstack.persistence.jooq.Tables.LABEL;
import static info.blogstack.persistence.jooq.Tables.POST;
import static info.blogstack.persistence.jooq.Tables.POSTS_LABELS;
import static info.blogstack.persistence.jooq.Tables.SOURCE;
import info.blogstack.persistence.jooq.tables.Post;
import info.blogstack.persistence.jooq.tables.records.LabelRecord;
import info.blogstack.persistence.jooq.tables.records.NewsletterRecord;
import info.blogstack.persistence.jooq.tables.records.PostRecord;
import info.blogstack.persistence.jooq.tables.records.SourceRecord;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import org.joda.time.DateTime;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;

public class PostDAOImpl implements PostDAO {

    private DSLContext context;

    public PostDAOImpl(DSLContext context) {
        this.context = context;
    }

    @Override
    public List<PostRecord> findAll() {
        return context.selectFrom(POST).where(POST.VISIBLE.isTrue()).fetch();
    }

    @Override
    public List<PostRecord> findAll(Pagination pagination) {
        return context.selectFrom(POST).where(POST.VISIBLE.isTrue()).orderBy(pagination.getFields()).limit(pagination.getOffset(), pagination.getNumberOfRows()).fetch();
    }

    @Override
    public List<PostRecord> findAllBySource(SourceRecord source, Pagination pagination) {
        return context.selectFrom(POST).where(POST.VISIBLE.isTrue().and(POST.SOURCE_ID.eq(source.getId()))).orderBy(pagination.getFields())
                .limit(pagination.getOffset(), pagination.getNumberOfRows()).fetch();
    }

    @Override
    public List<PostRecord> findAllByYearMonth(Integer year, Integer month) {
        Post p = POST.as("p");
        return context.selectFrom(p).where(p.VISIBLE.isTrue().and("year(\"p\".publishDate) = ?", year).and("month(\"p\".publishDate) = ?", month)).orderBy(p.DATE.desc())
                .fetch();
    }

    @Override
    public List<PostRecord> findAllByLabel(LabelRecord label, Pagination pagination) {
        return context.select().from(POST).join(POSTS_LABELS).on(POST.ID.eq(POSTS_LABELS.POST_ID)).where(POST.VISIBLE.isTrue().and(POSTS_LABELS.LABEL_ID.in(label.getId())))
                .orderBy(pagination.getFields()).limit(pagination.getOffset(), pagination.getNumberOfRows()).fetchInto(POST);
    }
    
    @Override
    public List<PostRecord> findAllByShared(boolean shared) {
        return context.select().from(POST).where(POST.SHARED.eq(shared)).orderBy(POST.CREATIONDATE.asc()).fetchInto(POST);
    }
    
    @Override
    public List<PostRecord> findNewsletter() {
        DateTime friday = new DateTime().withDayOfWeek(5).withHourOfDay(7);
        DateTime tuesday = new DateTime().withDayOfWeek(2).withHourOfDay(7);
        DateTime date = null;

        if (friday.isBeforeNow()) {
            date = friday;
        } else if (tuesday.isBeforeNow()) {
            date = tuesday;
        }

        if (date == null) {
            return Collections.EMPTY_LIST;
        }
        
        return context.select().from(POST).where(POST.NEWSLETTER_ID.isNull()).and(POST.CREATIONDATE.lt(date)).orderBy(POST.CREATIONDATE.asc()).fetchInto(POST);
    }

    @Override
    public PostRecord findByURL(String url) {
        return context.selectFrom(POST).where(POST.URL.eq(url)).fetchOne();
    }

    @Override
    public PostRecord findByHash(String hash) {
        return context.selectFrom(POST).where(POST.HASH.eq(hash)).fetchOne();
    }

    @Override
    public Long countAll() {
        return context.selectCount().from(POST).where(POST.VISIBLE.isTrue()).fetchOne(0, Long.class);
    }

    @Override
    public Long countBy(SourceRecord source) {
        return context.selectCount().from(POST).where(POST.VISIBLE.isTrue().and(POST.SOURCE_ID.eq(source.getId()))).fetchOne(0, Long.class);
    }

    @Override
    public Long countBy(LabelRecord label) {
        return context.selectCount().from(POST).join(POSTS_LABELS).on(POST.ID.eq(POSTS_LABELS.POST_ID))
                .where(POST.VISIBLE.isTrue().and(POSTS_LABELS.LABEL_ID.in(label.getId()))).fetchOne(0, Long.class);
    }
    
    @Override
    public Long countBy(NewsletterRecord newsletter) {
        return context.selectCount().from(POST).where(POST.NEWSLETTER_ID.eq(newsletter.getId())).fetchOne(0, Long.class);
    }

    @Override
    public Long countAuthors() {
        return context.select(DSL.countDistinct(DSL.concat(POST.SOURCE_ID, POST.AUTHOR))).from(POST).fetchOne(0, Long.class);
    }

    @Override
    public List<Map<String, Object>> getArchiveByDates() {
        return context
                .fetch("select year(p.publishDate) as year, month(p.publishDate) as month, count(*) as posts from BLOGSTACK.POST as p where p.visible = true group by year(p.publishDate), month(p.publishDate) order by year(p.publishDate) desc, month(p.publishDate) desc")
                .intoMaps();
    }

    @Override
    public List<Map<String, Object>> getArchiveByLabels() {
        List<Map<String, Object>> d = context
                .fetch("select l.id as id, count(*) as posts from BLOGSTACK.LABEL as l inner join BLOGSTACK.POSTS_LABELS as pl on l.id = pl.label_id inner join BLOGSTACK.POST as p on pl.post_id = p.id where l.enabled = true and p.visible = true group by l.id order by l.name")
                .intoMaps();
        for (Map<String, Object> m : d) {
            LabelRecord label = context.selectFrom(LABEL).where(LABEL.ID.eq((Long) m.get("ID"))).fetchOne();
            m.put("LABEL", label);
        }
        return d;
    }

    @Override
    public List<Map<String, Object>> getArchiveBySources() {
        List<Map<String, Object>> d = context
                .fetch("select s.id as id, count(*) as posts from BLOGSTACK.SOURCE as s inner join BLOGSTACK.POST as p on s.id = p.source_id where p.visible = true group by s.id order by s.name")
                .intoMaps();
        for (Map<String, Object> m : d) {
            SourceRecord source = context.selectFrom(SOURCE).where(SOURCE.ID.eq((Long) m.get("ID"))).fetchOne();
            m.put("SOURCE", source);
        }
        return d;
    }
    
    @Override
    public List<Map<String, Object>> getArchiveByNewsletter(NewsletterRecord newsletter) {
        List<Map<String, Object>> d = context
                .fetch("select l.id as id, count(*) as posts from BLOGSTACK.LABEL as l inner join BLOGSTACK.POSTS_LABELS as pl on l.id = pl.label_id inner join BLOGSTACK.POST as p on pl.post_id = p.id where l.enabled = true and p.visible = true and p.newsletter_id = ? group by l.id order by l.name", newsletter.getId())
                .intoMaps();
        for (Map<String, Object> m : d) {
            LabelRecord label = context.selectFrom(LABEL).where(LABEL.ID.eq((Long) m.get("ID"))).fetchOne();
            m.put("LABEL", label);
        }
        return d;
    }
    
    @Override
    public int updateNewsletter(Collection<PostRecord> posts, NewsletterRecord newslettter) {
        Collection<Long> ids = new ArrayList<>();
        for (PostRecord post : posts) {
            ids.add(post.getId());
        } 
        return context.update(POST).set(POST.NEWSLETTER_ID, newslettter.getId()).where(POST.ID.in(ids)).execute();
    }
}

Para usar el generador de código de jOOQ con Gradle debemos añadir la siguiente configuración al archivo de construcción del proyecto, este generador se conectará a la base de datos, obtendrá los datos de esquema y generará todas las clases del paquete info.blogstack.persistence.jooq. Por ejemplo, puede que queramos usar JodaTime en vez de las clases Date y Timesptamp de la API de Java al menos si no usamos aún Java 8 y sus novedades.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
...

task generateJooq {
    Configuration configuration = new Configuration()
        .withJdbc(new Jdbc()
            .withDriver('org.h2.Driver')
            .withUrl('jdbc:h2:./misc/database/app')
            .withUser('sa')
            .withPassword('sa'))
        .withGenerator(new Generator()
            .withGenerate(new Generate()
                .withInterfaces(true)
                .withRelations(true))
            .withName('org.jooq.util.DefaultGenerator')
            .withDatabase(new Database()
                .withCustomTypes([
                    new CustomType()
                        .withName('org.joda.time.DateTime')
                        .withConverter('info.blogstack.persistence.records.DateTimeConverter')
                        
                ])
                .withForcedTypes([
                    new ForcedType()
                        .withName('org.joda.time.DateTime')
                        .withTypes('TIMESTAMP')                        
                ])
                .withName('org.jooq.util.h2.H2Database')
                .withIncludes('.*')
                .withExcludes('')
                .withInputSchema('BLOGSTACK'))
            .withTarget(new Target()
                .withPackageName('info.blogstack.persistence.jooq')
                .withDirectory('src/main/java')));

    GenerationTool.main(configuration)
}

...

Otra alternativa con algunas similitudes a jOOQ es JDBI pero en esta las consultas no tienen el soporte del compilador que ofrece jOOQ, otra es Slick para Scala. También tiene cierta similitud con MyBatis que existe desde hace bastante tiempo aunque jOOQ ofrece más posibilidades.

En el código fuente de Blog Stack está el código completo de uso de jOOQ, en el paquete info.blogstack.persistence están las clases relacionadas con la persistencia en una base de datos H2, usa Spring para los servicios y la transaccionalidad, en la clase AppConfiguration se encuentra la definición de ambas cosas y la integración con jOOQ. En la versión 0.1 está la misma aplicación pero usando Hibernate.

Como comentan en su propio blog usa ModelMapper y jOOQ para recuperar el control de tu modelo de dominio que probablemente no es nada menos que lo más importante de tu aplicación o negocio. Herramientas como jOOQ contribuyen a que haya razones para seguir usando Java.

jOOQ está licenciado de forma dual, ASL para la versión community que ofrece soporte para las bases de datos MySQL, PostgreSQL, SQLite, H2 y alguna más y una licencia comercial que ofrece soporte para bases de datos como Microsoft Access, Oracle y Microsoft SQL Server.