Múltiples esquemas o bases de datos con jOOQ y Spring en Java

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

Aún en las aplicaciones monolíticas que comparten una única base de datos tratamos de dividirla en varios servicios que manejen cierto nicho de información con la intención de que un cambio en una parte sea transparente para las otras partes. Cada servicio de la aplicación monolítica podría potencialmente convertirse en un microservicio y en este caso para que cada micoservicio tenga un ciclo de vida independiente compartir la base de datos es algo a evitar. Incluso en las aplicaciones monolíticas podemos querer guardar cada nicho de información en su propio esquema para evitar acoplamiento entre las diferentes partes o también como forma de tener varios servidores de bases de datos y escalar la aplicación en cierta forma. En estos casos necesitaremos que la aplicación acceda a varios esquemas o bases de datos simultáneamente, con jOOQ y Spring es bastante sencillo.

Java

En el libro Building Microservices se pone de manifiesto que cada microservicio gira alrededor de un concepto que denomina seam, nicho o área de negocio. Este seam es el nicho de información que va a manejar el microservicio. Para hacer que los múltiples microservicios sean independientes y tengan su propio ciclo de vida este nicho de información se guarda en una base de datos o esquema propio de cada uno de modo que un cambio en el sistema en que guarda la información no afecte a otros microservicios como ocurriría si compartiesen la base de datos. Si un microservicio necesita información de otro la solicita mediante una API ya sea REST, usando Apache Thift, gRPC o de otro tipo evitando el acoplamiento a través de la base de datos y evitando que un microservicio conozca detalles internos de otro.

Imaginemos el caso de una empresa dedicada al comercio electrónico que ofrece productos a través de internet, un nicho de información podría ser el inventario formado por los productos ofrecidos y sus existencias, otro podría ser las compras. Los nichos podrían ser otros distintos, más numerosos o incluso los comentados más subdivididos, si una aplicación o proceso necesita acceder a todos estos nichos de información simultáneamente o tenemos una aplicación monolítica pero queremos tener cada nicho de información en varios esquemas o bases de datos, con jOOQ y Spring podemos acceder simultáneamente a múltiples esquemas o bases de datos de una forma bastante sencilla. Es importante tener en cuenta que con varios esquemas podremos mantener la integridad referencial de los datos a través de las claves externas, con varias bases de datos no.

Siguiendo el ejemplo de la empresa expuesta tendríamos dos bases de datos: inventory y purchases. Podemos tener un servicio que sea InventoryService y otro servicio que sea PurchasesService que contengan la lógica de negocio de cada área de negocio.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package io.github.picodotdev.blogbitix.multidatabase.service;

import org.springframework.transaction.annotation.Transactional;

import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.interfaces.IItem;
import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.records.ItemRecord;
import io.github.picodotdev.blogbitix.multidatabase.spring.AppConfiguration;

@Transactional(transactionManager = AppConfiguration.INVENTORY_TXM)
public interface InventoryService {

    @Transactional(transactionManager = AppConfiguration.INVENTORY_TXM, readOnly = true)
    Long count();    
    ItemRecord create(IItem item);    
    void changeStock(ItemRecord item, Long quantity) throws NoStockException;
}
 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
package io.github.picodotdev.blogbitix.multidatabase.service;

import org.jooq.DSLContext;

import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.Tables;
import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.interfaces.IItem;
import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.records.ItemRecord;

public class DefaultInventoryService implements InventoryService {

    private DSLContext context;

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

    @Override
    public Long count() {
        return context.selectCount().from(Tables.ITEM).fetchOne(0, Long.class);
    }

    @Override
    public ItemRecord create(IItem item) {
        ItemRecord record = context.newRecord(Tables.ITEM);
        record.from(item);        
        record.insert();        
        return record;
    }
    
    @Override
    public void changeStock(ItemRecord item, Long quantity) throws NoStockException {
        Long stock = item.getStock() - quantity;
        if (stock < 0) {
            throw new NoStockException();
        }
        item.setStock(stock);
        item.update();
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package io.github.picodotdev.blogbitix.multidatabase.service;

import org.springframework.transaction.annotation.Transactional;

import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.records.ItemRecord;
import io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.records.PurchaseRecord;
import io.github.picodotdev.blogbitix.multidatabase.spring.AppConfiguration;

@Transactional(transactionManager = AppConfiguration.PURCHASES_TXM)
public interface PurchasesService {

    @Transactional(transactionManager = AppConfiguration.PURCHASES_TXM, readOnly = true)
    Long count();
    PurchaseRecord create(ItemRecord item, long quantity) throws NoStockException;
}
 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
package io.github.picodotdev.blogbitix.multidatabase.service;

import java.time.LocalDateTime;

import org.jooq.DSLContext;

import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.records.ItemRecord;
import io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.Tables;
import io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.records.PurchaseRecord;

public class DefaultPurchasesService implements PurchasesService {

    private DSLContext context;
    private InventoryService inventory;

    public DefaultPurchasesService(DSLContext context, InventoryService inventory) {
        this.context = context;
        this.inventory = inventory;
    }

    @Override
    public Long count() {
        return context.selectCount().from(Tables.PURCHASE).fetchOne(0, Long.class);
    }
    
    @Override
    public PurchaseRecord create(ItemRecord item, long quantity) throws NoStockException {
        inventory.changeStock(item, -quantity);

        PurchaseRecord record = context.newRecord(Tables.PURCHASE);
        record.setCreationdate(LocalDateTime.now());
        record.setQuantity(quantity);
        record.setPrice(item.getPrice());
        record.setItemId(item.getId());
        record.insert();        
        return record;
    }
}

Al realizarse una compra a través del servicio PurchasesService se ha de modificar el inventario del producto cosa que no se hace en el propio servicio de compras sino que se llama al servicio InventoryService para que haga lo que deba, en el ejemplo modificar el inventario pero en un futuro podría ser enviar además una notificación o correo electrónico indicando que el stock es bajo si cae por debajo de determinado número, el servicio de compras no debe conocer nada de esto ya que el inventario no forma parte de su nicho de información. En el ejemplo es una llamada usando un método de una clase pero podría ser una llamada a una API REST o RPC si realmente fueran microservicios.

Las sencillas clases Item y Purchase generadas con jOOQ implementan las siguientes interfaces:

  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
/**
 * This class is generated by jOOQ
 */
package io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.interfaces;


import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

import javax.annotation.Generated;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;


/**
 * This class is generated by jOOQ.
 */
@Generated(
    value = {
        "http://www.jooq.org",
        "jOOQ version:3.7.0"
    },
    comments = "This class is generated by jOOQ"
)
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public interface IItem extends Serializable {

    /**
     * Setter for <code>INVENTORY.ITEM.ID</code>.
     */
    public void setId(Long value);

    /**
     * Getter for <code>INVENTORY.ITEM.ID</code>.
     */
    @NotNull
    public Long getId();

    /**
     * Setter for <code>INVENTORY.ITEM.CREATIONDATE</code>.
     */
    public void setCreationdate(LocalDateTime value);

    /**
     * Getter for <code>INVENTORY.ITEM.CREATIONDATE</code>.
     */
    public LocalDateTime getCreationdate();

    /**
     * Setter for <code>INVENTORY.ITEM.NAME</code>.
     */
    public void setName(String value);

    /**
     * Getter for <code>INVENTORY.ITEM.NAME</code>.
     */
    @Size(max = 256)
    public String getName();

    /**
     * Setter for <code>INVENTORY.ITEM.DESCRIPTION</code>.
     */
    public void setDescription(String value);

    /**
     * Getter for <code>INVENTORY.ITEM.DESCRIPTION</code>.
     */
    @Size(max = 2147483647)
    public String getDescription();

    /**
     * Setter for <code>INVENTORY.ITEM.STOCK</code>.
     */
    public void setStock(Long value);

    /**
     * Getter for <code>INVENTORY.ITEM.STOCK</code>.
     */
    public Long getStock();

    /**
     * Setter for <code>INVENTORY.ITEM.PRICE</code>.
     */
    public void setPrice(BigDecimal value);

    /**
     * Getter for <code>INVENTORY.ITEM.PRICE</code>.
     */
    public BigDecimal getPrice();

    // -------------------------------------------------------------------------
 // FROM and INTO
 // -------------------------------------------------------------------------

    /**
     * Load data from another generated Record/POJO implementing the common interface IItem
     */
    public void from(io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.interfaces.IItem from);

    /**
     * Copy data into another generated Record/POJO implementing the common interface IItem
     */
    public <E extends io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.interfaces.IItem> E into(E into);
}
 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
/**
 * This class is generated by jOOQ
 */
package io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.interfaces;


import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

import javax.annotation.Generated;
import javax.validation.constraints.NotNull;


/**
 * This class is generated by jOOQ.
 */
@Generated(
    value = {
        "http://www.jooq.org",
        "jOOQ version:3.7.0"
    },
    comments = "This class is generated by jOOQ"
)
@SuppressWarnings({ "all", "unchecked", "rawtypes" })
public interface IPurchase extends Serializable {

    /**
     * Setter for <code>PURCHASES.PURCHASE.ID</code>.
     */
    public void setId(Long value);

    /**
     * Getter for <code>PURCHASES.PURCHASE.ID</code>.
     */
    @NotNull
    public Long getId();

    /**
     * Setter for <code>PURCHASES.PURCHASE.CREATIONDATE</code>.
     */
    public void setCreationdate(LocalDateTime value);

    /**
     * Getter for <code>PURCHASES.PURCHASE.CREATIONDATE</code>.
     */
    public LocalDateTime getCreationdate();

    /**
     * Setter for <code>PURCHASES.PURCHASE.QUANTITY</code>.
     */
    public void setQuantity(Long value);

    /**
     * Getter for <code>PURCHASES.PURCHASE.QUANTITY</code>.
     */
    public Long getQuantity();

    /**
     * Setter for <code>PURCHASES.PURCHASE.PRICE</code>.
     */
    public void setPrice(BigDecimal value);

    /**
     * Getter for <code>PURCHASES.PURCHASE.PRICE</code>.
     */
    public BigDecimal getPrice();

    /**
     * Setter for <code>PURCHASES.PURCHASE.ITEM_ID</code>.
     */
    public void setItemId(Long value);

    /**
     * Getter for <code>PURCHASES.PURCHASE.ITEM_ID</code>.
     */
    public Long getItemId();

    // -------------------------------------------------------------------------
 // FROM and INTO
 // -------------------------------------------------------------------------

    /**
     * Load data from another generated Record/POJO implementing the common interface IPurchase
     */
    public void from(io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.interfaces.IPurchase from);

    /**
     * Copy data into another generated Record/POJO implementing the common interface IPurchase
     */
    public <E extends io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.interfaces.IPurchase> E into(E into);
}

El acceso a una base de datos usando jOOQ se consigue a través de la clase DSLContext, cada servicio recibe uno diferente que debemos definir en el contenedor de dependencias de Spring. Si fuesen dos bases de datos diferentes realmente debería haber definidos dos bean DataSource uno para cada servicio pero solo hay uno porque en el ejemplo se usa la base de datos H2 y se accede no con un servidor sino al fichero directamente. También en el ejemplo realmente no son necesarios dos (uno para cada servicio) TransactionManager, TransactionAwareDataSourceProxy, ConnectionProvider, Config y DSLContext pues solo hay una base de datos pero por mostrar más fielmente como sería el caso siendo dos bases de datos completamente diferentes lo he puesto así.

  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
package io.github.picodotdev.blogbitix.multidatabase.spring;

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.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.support.ResourceTransactionManager;

import io.github.picodotdev.blogbitix.multidatabase.service.InventoryService;
import io.github.picodotdev.blogbitix.multidatabase.service.DefaultInventoryService;
import io.github.picodotdev.blogbitix.multidatabase.service.PurchasesService;
import io.github.picodotdev.blogbitix.multidatabase.service.DefaultPurchasesService;

@Configuration
@ComponentScan({ "io.github.picodotdev.blogbitix.jooq" })
@EnableTransactionManagement
public class AppConfiguration {

    public static final String INVENTORY_TXM = "inventoryTransactionManager";
    public static final String PURCHASES_TXM = "purchasesTransactionManager";

    @Bean(name = "dataSource", destroyMethod = "close")
    @Primary
    @ConfigurationProperties(prefix = "datasource.primary")
    public DataSource dataSource() {
        BasicDataSource ds = new BasicDataSource();
        // ds.setDriverClassName("org.postgresql.Driver");
        // ds.setUrl("jdbc:postgresql://localhost:5432/app");
        // ds.setUsername("sa");
        // ds.setPassword("sa");
        // ds.setDriverClassName("com.mysql.jdbc.Driver");
        // ds.setUrl("jdbc:mysql://localhost:3306/app");
        // ds.setUsername("root");
        // ds.setPassword("");
        ds.setDriverClassName("org.h2.Driver");
        ds.setUrl("jdbc:h2:./misc/database/app");
        ds.setUsername("sa");
        ds.setPassword("sa");
        return ds;
    }

    @Bean(name = INVENTORY_TXM)
    public ResourceTransactionManager inventoryTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = PURCHASES_TXM)
    public ResourceTransactionManager purchasesTransactionManager(@Qualifier("dataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean(name = "inventoryTransactionAwareDataSourceProxy")
    public TransactionAwareDataSourceProxy inventoryTransactionAwareDataSourceProxy(@Qualifier("dataSource") DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }

    @Bean(name = "purchasesTransactionAwareDataSourceProxy")
    public TransactionAwareDataSourceProxy purchasesTransactionAwareDataSourceProxy(@Qualifier("dataSource") DataSource dataSource) {
        return new TransactionAwareDataSourceProxy(dataSource);
    }

    @Bean(name = "inventoryConnectionProvider")
    public ConnectionProvider inventoryConnectionProvider(
            @Qualifier("inventoryTransactionAwareDataSourceProxy") TransactionAwareDataSourceProxy transactionAwareDataSourceProxy) {
        return new DataSourceConnectionProvider(transactionAwareDataSourceProxy);
    }

    @Bean(name = "purchasesConnectionProvider")
    public ConnectionProvider purchasesConnectionProvider(
            @Qualifier("purchasesTransactionAwareDataSourceProxy") TransactionAwareDataSourceProxy transactionAwareDataSourceProxy) {
        return new DataSourceConnectionProvider(transactionAwareDataSourceProxy);
    }

    @Bean(name = "inventoryConfig")
    public org.jooq.Configuration inventoryConfig(@Qualifier("inventoryConnectionProvider") ConnectionProvider connectionProvider) {
        DefaultConfiguration config = new DefaultConfiguration();
        config.set(connectionProvider);
        // config.set(SQLDialect.POSTGRES_9_4);
        // config.set(SQLDialect.MYSQL);
        config.set(SQLDialect.H2);
        return config;
    }

    @Bean(name = "purchasesConfig")
    public org.jooq.Configuration purchasesConfig(@Qualifier("purchasesConnectionProvider") ConnectionProvider connectionProvider) {
        DefaultConfiguration config = new DefaultConfiguration();
        config.set(connectionProvider);
        // config.set(SQLDialect.POSTGRES_9_4);
        // config.set(SQLDialect.MYSQL);
        config.set(SQLDialect.H2);
        return config;
    }

    @Bean(name = "inventoryDSLContext")
    public DSLContext inventoryDSLContext(@Qualifier("inventoryConfig") org.jooq.Configuration config) {
        return DSL.using(config);
    }

    @Bean(name = "purchasesDSLContext")
    public DSLContext purchasesDSLContext(@Qualifier("purchasesConfig") org.jooq.Configuration config) {
        return DSL.using(config);
    }

    @Bean
    public InventoryService inventoryService(@Qualifier("inventoryDSLContext") DSLContext context) {
        return new DefaultInventoryService(context);
    }

    @Bean
    public PurchasesService purchasesService(@Qualifier("purchasesDSLContext") DSLContext context, InventoryService inventory) {
        return new DefaultPurchasesService(context, inventory);
    }
}

Podemos crear la base de datos y los dos esquemas con una tarea de Gradle y con Liquibase, con el comando ./gradlew updateDatabase, a continuación solo una parte del archivo build.gradle completo y los archivos XML de actualización de los esquemas.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
task updateDatabase() << {
    tasks.updateDatabaseInventory.execute()
    tasks.updateDatabasePurchases.execute()
}

task updateDatabaseInventory(type:Exec) {
    commandLine "misc/liquibase-3.4.1/liquibase", '--classpath=misc/libs/h2-1.4.189.jar', '--driver=org.h2.Driver',
        '--changeLogFile=misc/database/inventory-changelog.xml', '--url=jdbc:h2:./misc/database/app', '--username=sa', '--password=sa', 'update'
}

task updateDatabasePurchases(type:Exec) {
    commandLine "misc/liquibase-3.4.1/liquibase", '--classpath=misc/libs/h2-1.4.189.jar', '--driver=org.h2.Driver',
        '--changeLogFile=misc/database/purchases-changelog.xml', '--url=jdbc:h2:./misc/database/app', '--username=sa', '--password=sa', 'update'
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
    
    <property name="schema" value="inventory"/>
    
    <changeSet id="1" author="picodotdev">
        <comment>Creación de la base de datos</comment>
        <sql>create schema ${schema}</sql>
        <createTable tableName="item" schemaName="${schema}">
            <column autoIncrement="true" name="id" type="BIGINT">
                <constraints nullable="false" primaryKey="true" />
            </column>
            <column name="creationDate" type="TIMESTAMP" />
            <column name="name" type="VARCHAR(256)" />
            <column name="description" type="TEXT" />
            <column name="stock" type="BIGINT" />
            <column name="price" type="DECIMAL(7,2)" />
        </createTable>
    </changeSet>
</databaseChangeLog>
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-2.0.xsd">
    
    <property name="schema" value="purchases"/>
    <property name="inventorySchema" value="inventory"/>
    
    <changeSet id="1" author="picodotdev">
        <comment>Creación de la base de datos</comment>
        <sql>create schema ${schema}</sql>
        <createTable tableName="purchase" schemaName="${schema}">
            <column autoIncrement="true" name="id" type="BIGINT">
                <constraints nullable="false" primaryKey="true" />
            </column>
            <column name="creationDate" type="TIMESTAMP" />
            <column name="quantity" type="BIGINT" />
            <column name="price" type="DECIMAL(7,2)" />
            <column name="item_id" type="BIGINT">
                <constraints references="${inventorySchema}.item(id)" foreignKeyName="item_id"/>
            </column>
        </createTable>
    </changeSet>
</databaseChangeLog>
Base de datos con varios esquemas, inventory y purchases

Como en jOOQ la fuente de la verdad es la base de datos los modelos se generan a partir de ella usando otra tarea de Gradle, generará las clases con las que trabajaremos en la aplicación con el comando ./gradlew generateModels. Las clases son las del paquete io.github.picodotdev.blogbitix.multidatabase.jooq.

 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 generateModels << {
    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)
                .withPojos(true)
                .withRelations(true)
                .withValidationAnnotations(true))
            .withName('org.jooq.util.DefaultGenerator')
            .withDatabase(new Database()
                .withCustomTypes([
                    new CustomType()
                        .withName('java.time.LocalDateTime')
                        .withConverter('io.github.picodotdev.blogbitix.multidatabase.misc.TimestampConverter')
                ])
                .withForcedTypes([
                    new ForcedType()
                        .withName('java.time.LocalDateTime')
                        .withTypes('TIMESTAMP')                     
                ])
                .withName('org.jooq.util.h2.H2Database')
                .withIncludes('.*')
                .withExcludes('')
                .withSchemata([
                    new Schema().withInputSchema('INVENTORY'),
                    new Schema().withInputSchema('PURCHASES')
                ]))
            .withTarget(new Target()
                .withPackageName('io.github.picodotdev.blogbitix.multidatabase.jooq')
                .withDirectory('src/main/java')))

    GenerationTool.main(configuration)
}

Este sería el programa de ejemplo iniciado con Spring Boot que usa ambos servicios, el de inventario y el de compras, creando un producto y haciendo una compra junto con su salida en la terminal. Ejecutándolo repetidamente con ./gradlew run veremos aumenta el número de productos y compras guardados en cada tabla de los dos esquemas.

 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
package io.github.picodotdev.blogbitix.multidatabase;

import java.math.BigDecimal;
import java.time.LocalDateTime;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.pojos.Item;
import io.github.picodotdev.blogbitix.multidatabase.jooq.inventory.tables.records.ItemRecord;
import io.github.picodotdev.blogbitix.multidatabase.jooq.purchases.tables.records.PurchaseRecord;
import io.github.picodotdev.blogbitix.multidatabase.service.InventoryService;
import io.github.picodotdev.blogbitix.multidatabase.service.PurchasesService;

@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class, DataSourceTransactionManagerAutoConfiguration.class })
public class Main implements CommandLineRunner {

    @Autowired
    private InventoryService inventoryService;

    @Autowired
    private PurchasesService purchasesService;

    @Override
    public void run(String... args) throws Exception {
        System.out.println("Insertando datos...");        
        ItemRecord item = inventoryService.create(new Item(null, LocalDateTime.now(), "Libro", "Un gran libro", 10l, new BigDecimal("7.99")));
        PurchaseRecord purchase = purchasesService.create(item, 2);        

        System.out.printf("Número productos: %d%n", inventoryService.count());
        System.out.printf("Número compras: %d%n", purchasesService.count());
    }

    public static void main(String[] args) throws Exception {
        SpringApplication application = new SpringApplication(Main.class);
        application.setApplicationContextClass(AnnotationConfigApplicationContext.class);
        SpringApplication.run(Main.class, args);
    }
}
1
2
Número productos: 2
Número compras: 2
Ejecución del ejemplo multidatabase

Si quieres obtener más información sobre varias de las herramientas como jOOQ, Liquibase, Gradle o Spring Boot que forman en el momento de escribir este artículo el actual estado del arte en Java puedes leer los diferentes artículos que he he escrito sobre ellos de forma específica:

El código fuente completo del ejemplo puedes descargarlo del repositorio de ejemplos alojado en GitHub.