Build MapLoader using Data Connection

This tutorial shows how to build a custom map loader that uses a configured data connection to load data not present in an IMap.

This tutorial builds a custom implementation of MapLoader. For the most common use cases an out-of-the-box implementation is also provided via GenericMapLoader.

Before you begin

To complete this tutorial, you need the following:

Prerequisites Useful resources

Java 17 or newer

Maven or Gradle

Docker

Step 1. Create and populate database

This tutorial uses Docker to run the Postgres database.

Run the following command to start Postgres:

docker run --name postgres --rm -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres

Start psql client:

docker exec -it postgres psql -U postgres

Create a table my_table and populate it with data:

CREATE TABLE my_table(id INTEGER PRIMARY KEY, value VARCHAR(128));

INSERT INTO my_table VALUES (0, 'zero');
INSERT INTO my_table VALUES (1, 'one');
INSERT INTO my_table VALUES (2, 'two');
INSERT INTO my_table VALUES (3, 'three');
INSERT INTO my_table VALUES (4, 'four');
INSERT INTO my_table VALUES (5, 'five');
INSERT INTO my_table VALUES (6, 'six');
INSERT INTO my_table VALUES (7, 'seven');
INSERT INTO my_table VALUES (8, 'eight');
INSERT INTO my_table VALUES (9, 'nine');

Step 2. Create new java project

Create a blank Java project named pipeline-service-data-connection-example and copy the Gradle or Maven file into it:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.example</groupId>
    <artifactId>maploader-data-connection-example</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>maploader-data-connection-example</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.release>17</maven.compiler.release>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.hazelcast</groupId>
            <artifactId>hazelcast</artifactId>
            <version>6.0.0-SNAPSHOT</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.24.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-slf4j2-impl</artifactId>
            <version>2.24.1</version>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>42.7.4</version>
        </dependency>
    </dependencies>
</project>

Step 3. Implement MapLoader

The following map loader implements the com.hazelcast.map.MapLoader and com.hazelcast.map.MapLoaderLifecycleSupport interfaces.

public class SimpleMapLoader implements MapLoader<Integer, String>, MapLoaderLifecycleSupport {

    private JdbcDataConnection jdbcDataConnection;

    // ...
}

To implement the MapLoaderLifecycleSupport interface you need the following methods:

    // ...

    @Override
    public void init(HazelcastInstance instance, Properties properties, String mapName) {
        jdbcDataConnection = instance.getDataConnectionService()
                .getAndRetainDataConnection("my_data_connection", JdbcDataConnection.class);
    }

    @Override
    public void destroy() {
        jdbcDataConnection.release();
    }

    // ...

To implement the MapLoader interface we need the following methods:

    @Override
    public String load(Integer key) {
        try (Connection connection = jdbcDataConnection.getConnection();
             PreparedStatement statement = connection.prepareStatement("SELECT value FROM my_table WHERE id = ?")) {

            statement.setInt(1, key);
            ResultSet resultSet = statement.executeQuery();
            String value = null;
            if (resultSet.next()) {
                value = resultSet.getString("value");
            }
            return value;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to load value for key=" + key, e);
        }
    }

    @Override
    public Map<Integer, String> loadAll(Collection<Integer> keys) {
        Map<Integer, String> resultMap = new HashMap<>();
        StringBuilder queryBuilder = new StringBuilder("SELECT id, value FROM my_table WHERE id IN (");

        // Construct query for batch retrieval
        keys.forEach(key -> queryBuilder.append("?,"));
        queryBuilder.setLength(queryBuilder.length() - 1); // Remove last comma
        queryBuilder.append(")");

        try (Connection connection = jdbcDataConnection.getConnection();
             PreparedStatement statement = connection.prepareStatement(queryBuilder.toString())) {

            int index = 1;
            for (Integer key : keys) {
                statement.setInt(index++, key);
            }

            ResultSet resultSet = statement.executeQuery();
            while (resultSet.next()) {
                resultMap.put(resultSet.getInt("id"), resultSet.getString("value"));
            }
            return resultMap;
        } catch (SQLException e) {
            throw new RuntimeException("Failed to load values", e);
        }
    }

    @Override
    public Iterable<Integer> loadAllKeys() {
        List<Integer> keys = new ArrayList<>();
        try (Connection connection = jdbcDataConnection.getConnection();
             PreparedStatement statement = connection.prepareStatement("SELECT id FROM my_table");
             ResultSet resultSet = statement.executeQuery()) {

            while (resultSet.next()) {
                keys.add(resultSet.getInt("id"));
            }
            return keys;
        } catch (Exception e) {
            throw new RuntimeException("Failed to load all keys", e);
        }
    }

Step 4. Create example MapLoader app

Configure the data connection:

public class MapLoaderExampleApp {
    public static void main(String[] args) {
        Config config = new Config();

        DataConnectionConfig dcc = new DataConnectionConfig("my_data_connection");
        dcc.setType("JDBC");
        dcc.setProperty("jdbcUrl", "jdbc:postgresql://172.17.0.2/postgres");
        dcc.setProperty("user", "postgres");
        dcc.setProperty("password", "postgres");
        config.addDataConnectionConfig(dcc);

    }
}

Configure an IMap named my_map with the map loader:

public class MapLoaderExampleApp {
    public static void main(String[] args) {
        // ...

        MapStoreConfig mapStoreConfig = new MapStoreConfig();
        mapStoreConfig.setClassName(SimpleMapLoader.class.getName());

        MapConfig mapConfig = new MapConfig("my_map");
        mapConfig.setMapStoreConfig(mapStoreConfig);
        config.addMapConfig(mapConfig);


    }
}

Create a HazelcastInstance with the Config, get the IMap and read some data:

public class MapLoaderExampleApp {
    public static void main(String[] args) {
        // ...

        HazelcastInstance hz = Hazelcast.newHazelcastInstance(config);
        IMap<Integer, String> map = hz.getMap("my_map");

        System.out.println("1 maps to " + map.get(1));
        System.out.println("42 maps to " + map.get(10));
    }
}

When you run this class you should see the following output:

1 maps to one
42 maps to null

Next steps

Read through the Dynamic Configuration section to find out how to add the DataConnection config and new IMap config with MapStore dynamically.