All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.fhir.persistence.jdbc.dao.impl.FetchResourcePayloadsDAO Maven / Gradle / Ivy

There is a newer version: 4.11.1
Show newest version
/*
 * (C) Copyright IBM Corp. 2021
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ibm.fhir.persistence.jdbc.dao.impl;

import static com.ibm.fhir.persistence.jdbc.JDBCConstants.DOT;

import java.io.IOException;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.Instant;
import java.util.Calendar;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

import com.ibm.fhir.database.utils.api.IDatabaseTranslator;
import com.ibm.fhir.persistence.ResourcePayload;
import com.ibm.fhir.persistence.exception.FHIRPersistenceException;
import com.ibm.fhir.persistence.jdbc.exception.FHIRPersistenceDataAccessException;

/**
 * DAO to fetch resource ids using a time range and optional current resource id as a filter.
 * Useful for scanning the database for new resources since a given timestamp (for example
 * in export scenarios).
 */
public class FetchResourcePayloadsDAO {
    private static final Logger logger = Logger.getLogger(FetchResourcePayloadsDAO.class.getName());

    private static final Calendar UTC_CALENDAR = Calendar.getInstance(TimeZone.getTimeZone("UTC"));

    // The FHIR data schema name
    private final String schemaName;

    // The name of the resource type (e.g. "Patient")
    private final String resourceType;

    // Start scanning from this timestamp. Never null
    private final Instant fromLastUpdated;

    // Scan up to this timestamp if given. Can be null.
    private final Instant toLastUpdated;

    // Consumer to process each of the records read
    private final Function processor;

    // The database translator to help tweak the syntax needed for different DB support
    private final IDatabaseTranslator translator;

    public FetchResourcePayloadsDAO(IDatabaseTranslator translator, String schemaName, String resourceType, Instant fromLastUpdated, Instant toLastUpdated,
        Function processor) {
        this.translator = translator;
        this.schemaName = schemaName;
        this.resourceType = resourceType;
        this.fromLastUpdated = fromLastUpdated;
        this.toLastUpdated = toLastUpdated;
        this.processor = processor;
    }

    /**
     * Run the query, feeding each result row to the consumer
     * @param c
     * @return the last record processed
     * @throws FHIRPersistenceException
     */
    public ResourcePayload run(Connection c) throws FHIRPersistenceException {
        ResourcePayload result = null;

        final String lrTableName = resourceType + "_logical_resources";
        final String rTableName = resourceType + "_resources";
        StringBuilder query = new StringBuilder();
        query.append("SELECT lr.logical_id, r.last_updated, r.resource_id, r.data FROM ");
        query.append(schemaName).append(DOT).append(rTableName).append(" AS r, ");
        query.append(schemaName).append(DOT).append(lrTableName).append(" AS lr ");
        query.append(" WHERE r.is_deleted = 'N' ");
        query.append("   AND lr.current_resource_id = r.resource_id ");

        // Add the predicate for the optoional start
        // thiss aligns with the Bulk Data Specification.
        if (this.fromLastUpdated != null) {
            query.append("   AND r.last_updated >= ? ");
        }

        // Add the predicate for the optional end-stop
        if (this.toLastUpdated != null) {
            query.append(" AND r.last_updated <= ? ");
        }

        // It is crucial to ORDER BY both the last_updated timestamp and the resource_id so that
        // the scan can continue from where it left off. Unfortunately the index on last_updated
        // doesn't include resource_id, so a fetch from the table is unavoidable.
        query.append(" ORDER BY r.last_updated ");

        // For Postgres, the LIMIT is absolutely required to make sure we get a sort-free scan (the ORDER BY
        // is supported by an index, so the database should be able to start feeding results back immediately
        // without having to first gather everything for a sort, which makes this a whole lot faster and more
        // scalable. With this LIMIT, it does mean that we have to call this query more frequently, but because
        // it is so cheap, this doesn't hurt us.
        query.append(translator.limit(Integer.toString(1000)));

        final String select = query.toString();

        if (logger.isLoggable(Level.FINE)) {
            logger.fine("Fetch resource payload query: " + select);
        }

        try (PreparedStatement ps = c.prepareStatement(select)) {
            int a = 1;

            // Set the variables marking the start point of the scan
            if (this.fromLastUpdated != null) {
                ps.setTimestamp(a++, Timestamp.from(this.fromLastUpdated), UTC_CALENDAR);
            }

            // And where we want the scan to stop (e.g. exporting a limited time range)
            if (this.toLastUpdated != null) {
                ps.setTimestamp(a++, Timestamp.from(this.toLastUpdated), UTC_CALENDAR);
            }

            ResultSet rs = ps.executeQuery();
            while (rs.next()) {
                // make sure we get the timestamp as a UTC value
                String logicalId = rs.getString(1);
                Instant lastUpdated = rs.getTimestamp(2, UTC_CALENDAR).toInstant();
                long resourceId = rs.getLong(3);
                InputStream is = new GZIPInputStream(rs.getBinaryStream(4));
                result = new ResourcePayload(logicalId, lastUpdated, resourceId, is);
                Boolean cont = processor.apply(result);
                if (cont == null || !cont) {
                    // the processor has asked us to stop
                    break;
                }
            }
        } catch (IOException x) {
            logger.log(Level.SEVERE, "query: " + select + "[fromLastUpdated=" + fromLastUpdated + ", toLastUpdated=" + toLastUpdated + "]", x);
            throw new FHIRPersistenceDataAccessException("FetchResourceIds query failed");
        } catch (SQLException x) {
            logger.log(Level.SEVERE, "query: " + select + "[fromLastUpdated=" + fromLastUpdated + ", toLastUpdated=" + toLastUpdated + "]", x);
            throw new FHIRPersistenceDataAccessException("FetchResourceIds query failed");
        }

        // the last ResultPayload to be processed is returned as a convenience to the caller
        return result;
    }

    /**
     * Get a count of the resources matching the filter predicates...for debugging...slows
     * things down a lot
     * @param c
     * @return
     * @throws FHIRPersistenceException
     */
    public int count(Connection c) throws FHIRPersistenceException {
        int result;

        final String lrTableName = resourceType + "_logical_resources";
        final String rTableName = resourceType + "_resources";
        StringBuilder query = new StringBuilder();
        query.append("SELECT count(*) FROM ");
        query.append(schemaName).append(DOT).append(rTableName).append(" AS r, ");
        query.append(schemaName).append(DOT).append(lrTableName).append(" AS lr ");
        query.append(" WHERE r.is_deleted = 'N' ");
        query.append("   AND lr.current_resource_id = r.resource_id ");

        query.append("   AND r.last_updated >= ? ");

        // Add the predicate for the optional end-stop
        if (this.toLastUpdated != null) {
            query.append(" AND r.last_updated <= ? ");
        }

        final String select = query.toString();

        if (logger.isLoggable(Level.FINE)) {
            Timestamp from = Timestamp.from(fromLastUpdated);
            logger.fine("Resource count query: " + select + "; [" + fromLastUpdated + "]");
        }

        try (PreparedStatement ps = c.prepareStatement(select)) {
            int a = 1;


            // Set the variables marking the start point of the scan
            ps.setTimestamp(a++, Timestamp.from(fromLastUpdated), UTC_CALENDAR);

            // And where we want the scan to stop (e.g. exporting a limited time range)
            if (this.toLastUpdated != null) {
                ps.setTimestamp(a++, Timestamp.from(this.toLastUpdated), UTC_CALENDAR);
            }

            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                result = rs.getInt(1);
            } else {
                throw new IllegalStateException("no count result"); // not gonna happen
            }
        } catch (SQLException x) {
            logger.log(Level.SEVERE, "query: " + select + "[fromLastUpdated=" + fromLastUpdated + ", toLastUpdated=" + toLastUpdated + "]", x);
            throw new FHIRPersistenceDataAccessException("FetchResourceIds query failed");
        }
        return result;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy