From 4afe4deb300af4c861d4a7762dfc2f791a48c02e Mon Sep 17 00:00:00 2001 From: Martin Lowe Date: Wed, 21 Sep 2022 15:28:35 -0400 Subject: [PATCH] Add support for stored procedures in persistence --- .../dao/impl/BaseHibernateDao.java | 86 ++++++++++++------- .../persistence/model/HQLGenerator.java | 2 +- .../model/ParameterizedCallStatement.java | 47 ++++++++++ .../ParameterizedSQLStatementBuilder.java | 3 + 4 files changed, 105 insertions(+), 33 deletions(-) create mode 100644 persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedCallStatement.java diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/BaseHibernateDao.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/BaseHibernateDao.java index 9764ce3..25b0fee 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/BaseHibernateDao.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/dao/impl/BaseHibernateDao.java @@ -13,9 +13,13 @@ package org.eclipsefoundation.persistence.dao.impl; import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; import javax.inject.Inject; import javax.persistence.EntityManager; +import javax.persistence.ParameterMode; +import javax.persistence.Query; +import javax.persistence.StoredProcedureQuery; import javax.persistence.TypedQuery; import javax.transaction.Transactional; @@ -26,6 +30,7 @@ import org.eclipsefoundation.core.exception.MaintenanceException; import org.eclipsefoundation.core.response.PaginatedResultsFilter; import org.eclipsefoundation.persistence.dao.PersistenceDao; import org.eclipsefoundation.persistence.dto.BareNode; +import org.eclipsefoundation.persistence.model.ParameterizedCallStatement; import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.Clause; import org.eclipsefoundation.persistence.model.RDBMSQuery; import org.slf4j.Logger; @@ -47,31 +52,41 @@ public abstract class BaseHibernateDao implements PersistenceDao { if (isMaintenanceMode()) { throw new MaintenanceException(); } - // handle root request parameters (setting headers to track limit and max count) - if (q.isRoot()) { - handleRoot(q); - } - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("Querying DB using the following query: {}", q.getFilter().getSelectSql()); + // set up the query, either for stored procedures or standard SQL + Query query; + if (q.getFilter() instanceof ParameterizedCallStatement) { + query = getSecondaryEntityManager() + .createStoredProcedureQuery(((ParameterizedCallStatement) q.getFilter()).getCallStatement(), q.getDocType()); + } else { + query = getSecondaryEntityManager().createQuery(q.getFilter().getSelectSql(), q.getDocType()); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Querying DB using the following query: {}", q.getFilter().getSelectSql()); + } + + // handle root request parameters (setting headers to track limit and max count) + if (q.isRoot()) { + handleRoot(q); + } + // check if result set should be limited + if (q.getDTOFilter().useLimit() && q.useLimit()) { + LOGGER.debug("Querying DB using offset of {} and limit of {}", getOffset(q), getLimit(q)); + query = query.setFirstResult(getOffset(q)).setMaxResults(getLimit(q)); + } } - // build base query - TypedQuery query = getSecondaryEntityManager().createQuery(q.getFilter().getSelectSql(), q.getDocType()); // add ordinal parameters int ord = 1; for (Clause c : q.getFilter().getClauses()) { for (Object param : c.getParams()) { + // for each of params, if processing call statement, add params as registered params + if (q.getFilter() instanceof ParameterizedCallStatement) { + ((StoredProcedureQuery) query).registerStoredProcedureParameter(ord, param.getClass(), ParameterMode.IN); + } query.setParameter(ord++, param); } } - - // check if result set should be limited - if (q.getDTOFilter().useLimit() && q.useLimit()) { - LOGGER.debug("Querying DB using offset of {} and limit of {}", getOffset(q), getLimit(q)); - query = query.setFirstResult(getOffset(q)).setMaxResults(getLimit(q)); - } // run the query - return query.getResultList(); + return castResultsToType(query.getResultList(), q.getDocType()); } @Transactional @@ -138,10 +153,7 @@ public abstract class BaseHibernateDao implements PersistenceDao { EntityManager em = getSecondaryEntityManager(); // build base query - TypedQuery query = em - .createQuery( - q.getFilter() - .getCountSql(), Long.class); + TypedQuery query = em.createQuery(q.getFilter().getCountSql(), Long.class); // add ordinal parameters int ord = 1; for (Clause c : q.getFilter().getClauses()) { @@ -161,8 +173,8 @@ public abstract class BaseHibernateDao implements PersistenceDao { } /** - * Handles operations that should happen on a "root" or main DB request for a fulfilled request. This is done to - * ensure that values such as DB max are abided. + * Handles operations that should happen on a "root" or main DB request for a fulfilled request. This is done to ensure + * that values such as DB max are abided. */ private void handleRoot(RDBMSQuery q) { // check if count has been performed, if not do so and set it to the response @@ -171,12 +183,10 @@ public abstract class BaseHibernateDao implements PersistenceDao { } private int getLimit(RDBMSQuery q) { - return q.getLimit() > 0 - ? Math.min(q.getLimit(), - ConfigProvider.getConfig().getOptionalValue("eclipse.db.default.limit", Integer.class) - .orElseGet(() -> 1000)) - : ConfigProvider.getConfig().getOptionalValue("eclipse.db.default.limit.max", Integer.class) - .orElseGet(() -> 1000); + return q.getLimit() > 0 ? Math + .min(q.getLimit(), + ConfigProvider.getConfig().getOptionalValue("eclipse.db.default.limit", Integer.class).orElseGet(() -> 1000)) + : ConfigProvider.getConfig().getOptionalValue("eclipse.db.default.limit.max", Integer.class).orElseGet(() -> 1000); } private int getOffset(RDBMSQuery q) { @@ -193,6 +203,19 @@ public abstract class BaseHibernateDao implements PersistenceDao { return (limit * q.getPage()) - limit; } + /** + * Checked casting of results to properly typed list. + * + * @param the type to cast the list to + * @param l the list that needs to be cast to a type + * @param clazz the literal type to cast to + * @return the original list cast to the passed type + * @throws ClassCastException if the internal types do not match the passed type. + */ + private List castResultsToType(List l, Class clazz) { + return l.stream().map(clazz::cast).collect(Collectors.toList()); + } + @Override public HealthCheckResponse call() { HealthCheckResponseBuilder b = HealthCheckResponse.named("DB readiness"); @@ -203,8 +226,7 @@ public abstract class BaseHibernateDao implements PersistenceDao { } protected boolean isMaintenanceMode() { - return ConfigProvider.getConfig().getOptionalValue("eclipse.db.maintenance", Boolean.class) - .orElseGet(() -> false); + return ConfigProvider.getConfig().getOptionalValue("eclipse.db.maintenance", Boolean.class).orElseGet(() -> false); } /** @@ -218,9 +240,9 @@ public abstract class BaseHibernateDao implements PersistenceDao { } /** - * Allow for multiple connections to be associated with a datasource for separate read/write strategies. The - * secondary connection would be a read-only interface to reduce traffic on primary write enable database - * replicants. Default behaviour returns the the primary entity manager but this can be overriden. + * Allow for multiple connections to be associated with a datasource for separate read/write strategies. The secondary + * connection would be a read-only interface to reduce traffic on primary write enable database replicants. Default + * behaviour returns the the primary entity manager but this can be overriden. * * @return the secondary entity manager, defaults to the primary entity manager. */ diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/HQLGenerator.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/HQLGenerator.java index 0d92c3b..5b8bd8d 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/HQLGenerator.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/HQLGenerator.java @@ -22,7 +22,7 @@ import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.GroupBy import org.eclipsefoundation.persistence.model.ParameterizedSQLStatement.IJoin; public class HQLGenerator implements SQLGenerator { - private static final Pattern ORDINAL_PARAMETER_PATTERN = Pattern.compile("\\?(?!\\d)"); + public static final Pattern ORDINAL_PARAMETER_PATTERN = Pattern.compile("\\?(?!\\d)"); @Override public String getSelectSQL(ParameterizedSQLStatement src) { diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedCallStatement.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedCallStatement.java new file mode 100644 index 0000000..f2d7da5 --- /dev/null +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedCallStatement.java @@ -0,0 +1,47 @@ +/********************************************************************* +* Copyright (c) 2022 Eclipse Foundation. +* +* This program and the accompanying materials are made +* available under the terms of the Eclipse Public License 2.0 +* which is available at https://www.eclipse.org/legal/epl-2.0/ +* +* Author: Martin Lowe +* +* SPDX-License-Identifier: EPL-2.0 +**********************************************************************/ +package org.eclipsefoundation.persistence.model; + +/** + * Special use case SQL statement that allows for direct execution of SQL with minimal changes/modifications. Ordinals + * are still substituted, but SQL is directly set by callStatement variable. + */ +public class ParameterizedCallStatement extends ParameterizedSQLStatement { + private String callStatement; + + ParameterizedCallStatement(DtoTable base, SQLGenerator gen) { + super(base, gen); + } + + @Override + public String getSelectSql() { + throw new IllegalStateException("Cannot use select when referencing stored procedures"); + } + + @Override + public String getCountSql() { + throw new IllegalStateException("Cannot use count when referencing stored procedures"); + } + + /** + * Set thet call statement to be executed. + * + * @param callStatement + */ + public void setCallStatement(String callStatement) { + this.callStatement = callStatement; + } + + public String getCallStatement() { + return this.callStatement; + } +} diff --git a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java index 72b014f..f856290 100644 --- a/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java +++ b/persistence/runtime/src/main/java/org/eclipsefoundation/persistence/model/ParameterizedSQLStatementBuilder.java @@ -23,4 +23,7 @@ public class ParameterizedSQLStatementBuilder { return new ParameterizedSQLStatement(base, generator); } + public ParameterizedCallStatement buildCallStatement(DtoTable base) { + return new ParameterizedCallStatement(base, generator); + } } -- GitLab