diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java index d84edbef..674203dd 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/QueryInterface.java @@ -22,6 +22,9 @@ package com.kingsrook.qqq.backend.core.actions.interfaces; +import java.time.Instant; +import java.util.Set; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStat; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput; import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput; @@ -37,4 +40,49 @@ public interface QueryInterface ** *******************************************************************************/ QueryOutput execute(QueryInput queryInput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default void setQueryStat(QueryStat queryStat) + { + ////////// + // noop // + ////////// + } + + /******************************************************************************* + ** + *******************************************************************************/ + default QueryStat getQueryStat() + { + return (null); + } + + /******************************************************************************* + ** + *******************************************************************************/ + default void setQueryStatJoinTables(Set joinTableNames) + { + QueryStat queryStat = getQueryStat(); + if(queryStat != null) + { + queryStat.setJoinTables(joinTableNames); + } + } + + /******************************************************************************* + ** + *******************************************************************************/ + default void setQueryStatFirstResultTime() + { + QueryStat queryStat = getQueryStat(); + if(queryStat != null) + { + if(queryStat.getFirstResultTimestamp() == null) + { + queryStat.setFirstResultTimestamp(Instant.now()); + } + } + } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java index 0bf8e33c..6c5cc9e7 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/QueryAction.java @@ -23,19 +23,24 @@ package com.kingsrook.qqq.backend.core.actions.tables; import java.io.Serializable; +import java.time.Instant; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; import com.kingsrook.qqq.backend.core.actions.ActionHelper; import com.kingsrook.qqq.backend.core.actions.customizers.AbstractPostQueryCustomizer; import com.kingsrook.qqq.backend.core.actions.customizers.QCodeLoader; import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers; +import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipeBufferedWrapper; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStat; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStatManager; import com.kingsrook.qqq.backend.core.actions.values.QPossibleValueTranslator; import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter; import com.kingsrook.qqq.backend.core.context.QContext; @@ -98,11 +103,22 @@ public class QueryAction } } + QueryStat queryStat = new QueryStat(); + queryStat.setTableName(queryInput.getTableName()); + queryStat.setQQueryFilter(Objects.requireNonNullElse(queryInput.getFilter(), new QQueryFilter())); + queryStat.setStartTimestamp(Instant.now()); + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend()); // todo pre-customization - just get to modify the request? - QueryOutput queryOutput = qModule.getQueryInterface().execute(queryInput); - // todo post-customization - can do whatever w/ the result if you want + + QueryInterface queryInterface = qModule.getQueryInterface(); + queryInterface.setQueryStat(queryStat); + QueryOutput queryOutput = queryInterface.execute(queryInput); + + // todo post-customization - can do whatever w/ the result if you want? + + QueryStatManager.getInstance().add(queryStat); if(queryInput.getRecordPipe() instanceof BufferedRecordPipe bufferedRecordPipe) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java new file mode 100644 index 00000000..2c35f9f4 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStat.java @@ -0,0 +1,397 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterCriteria; +import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter; +import com.kingsrook.qqq.backend.core.model.data.QAssociation; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; +import com.kingsrook.qqq.backend.core.utils.ValueUtils; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStat extends QRecordEntity +{ + public static final String TABLE_NAME = "queryStat"; + + @QField() + private Integer id; + + @QField() + private String tableName; + + @QField() + private Instant startTimestamp; + + @QField() + private Instant firstResultTimestamp; + + @QField() + private Integer firstResultMillis; + + @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE) + private String joinTables; + + @QField(maxLength = 100, valueTooLongBehavior = ValueTooLongBehavior.TRUNCATE) + private String orderBys; + + @QAssociation(name = "queryStatFilterCriteria") + private List queryStatFilterCriteriaList; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setJoinTables(Collection joinTableNames) + { + if(CollectionUtils.nullSafeIsEmpty(joinTableNames)) + { + setJoinTables((String) null); + } + + setJoinTables(joinTableNames.stream().sorted().collect(Collectors.joining(","))); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void setQQueryFilter(QQueryFilter filter) + { + if(filter == null) + { + setQueryStatFilterCriteriaList(null); + setOrderBys(null); + } + else + { + ///////////////////////////////////////////// + // manage list of sub-records for criteria // + ///////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(filter.getCriteria()) && CollectionUtils.nullSafeIsEmpty(filter.getSubFilters())) + { + setQueryStatFilterCriteriaList(null); + } + else + { + ArrayList criteriaList = new ArrayList<>(); + setQueryStatFilterCriteriaList(criteriaList); + processFilterCriteria(filter, criteriaList); + } + + ////////////////////////////////////////////////////////////// + // set orderBys (comma-delimited concatenated string field) // + ////////////////////////////////////////////////////////////// + if(CollectionUtils.nullSafeIsEmpty(filter.getOrderBys())) + { + setOrderBys(null); + } + else + { + setOrderBys(filter.getOrderBys().stream().map(ob -> ob.getFieldName()).collect(Collectors.joining(","))); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void processFilterCriteria(QQueryFilter filter, ArrayList criteriaList) + { + for(QFilterCriteria criterion : CollectionUtils.nonNullList(filter.getCriteria())) + { + criteriaList.add(new QueryStatFilterCriteria() + .withFieldName(criterion.getFieldName()) + .withOperator(criterion.getOperator().name()) + .withValues(CollectionUtils.nonNullList(criterion.getValues()).stream().map(v -> ValueUtils.getValueAsString(v)).collect(Collectors.joining(",")))); + } + + for(QQueryFilter subFilter : CollectionUtils.nonNullList(filter.getSubFilters())) + { + processFilterCriteria(subFilter, criteriaList); + } + } + + + + /******************************************************************************* + ** Getter for tableName + *******************************************************************************/ + public String getTableName() + { + return (this.tableName); + } + + + + /******************************************************************************* + ** Setter for tableName + *******************************************************************************/ + public void setTableName(String tableName) + { + this.tableName = tableName; + } + + + + /******************************************************************************* + ** Fluent setter for tableName + *******************************************************************************/ + public QueryStat withTableName(String tableName) + { + this.tableName = tableName; + return (this); + } + + + + /******************************************************************************* + ** Getter for startTimestamp + *******************************************************************************/ + public Instant getStartTimestamp() + { + return (this.startTimestamp); + } + + + + /******************************************************************************* + ** Setter for startTimestamp + *******************************************************************************/ + public void setStartTimestamp(Instant startTimestamp) + { + this.startTimestamp = startTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for startTimestamp + *******************************************************************************/ + public QueryStat withStartTimestamp(Instant startTimestamp) + { + this.startTimestamp = startTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for firstResultTimestamp + *******************************************************************************/ + public Instant getFirstResultTimestamp() + { + return (this.firstResultTimestamp); + } + + + + /******************************************************************************* + ** Setter for firstResultTimestamp + *******************************************************************************/ + public void setFirstResultTimestamp(Instant firstResultTimestamp) + { + this.firstResultTimestamp = firstResultTimestamp; + } + + + + /******************************************************************************* + ** Fluent setter for firstResultTimestamp + *******************************************************************************/ + public QueryStat withFirstResultTimestamp(Instant firstResultTimestamp) + { + this.firstResultTimestamp = firstResultTimestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for firstResultMillis + *******************************************************************************/ + public Integer getFirstResultMillis() + { + return (this.firstResultMillis); + } + + + + /******************************************************************************* + ** Setter for firstResultMillis + *******************************************************************************/ + public void setFirstResultMillis(Integer firstResultMillis) + { + this.firstResultMillis = firstResultMillis; + } + + + + /******************************************************************************* + ** Fluent setter for firstResultMillis + *******************************************************************************/ + public QueryStat withFirstResultMillis(Integer firstResultMillis) + { + this.firstResultMillis = firstResultMillis; + return (this); + } + + + + /******************************************************************************* + ** Getter for joinTables + *******************************************************************************/ + public String getJoinTables() + { + return (this.joinTables); + } + + + + /******************************************************************************* + ** Setter for joinTables + *******************************************************************************/ + public void setJoinTables(String joinTables) + { + this.joinTables = joinTables; + } + + + + /******************************************************************************* + ** Fluent setter for joinTables + *******************************************************************************/ + public QueryStat withJoinTables(String joinTables) + { + this.joinTables = joinTables; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryStatFilterCriteriaList + *******************************************************************************/ + public List getQueryStatFilterCriteriaList() + { + return (this.queryStatFilterCriteriaList); + } + + + + /******************************************************************************* + ** Setter for queryStatFilterCriteriaList + *******************************************************************************/ + public void setQueryStatFilterCriteriaList(List queryStatFilterCriteriaList) + { + this.queryStatFilterCriteriaList = queryStatFilterCriteriaList; + } + + + + /******************************************************************************* + ** Fluent setter for queryStatFilterCriteriaList + *******************************************************************************/ + public QueryStat withQueryStatFilterCriteriaList(List queryStatFilterCriteriaList) + { + this.queryStatFilterCriteriaList = queryStatFilterCriteriaList; + return (this); + } + + + + /******************************************************************************* + ** Getter for orderBys + *******************************************************************************/ + public String getOrderBys() + { + return (this.orderBys); + } + + + + /******************************************************************************* + ** Setter for orderBys + *******************************************************************************/ + public void setOrderBys(String orderBys) + { + this.orderBys = orderBys; + } + + + + /******************************************************************************* + ** Fluent setter for orderBys + *******************************************************************************/ + public QueryStat withOrderBys(String orderBys) + { + this.orderBys = orderBys; + return (this); + } + + + + /******************************************************************************* + ** Getter for id + *******************************************************************************/ + public Integer getId() + { + return (this.id); + } + + + + /******************************************************************************* + ** Setter for id + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + *******************************************************************************/ + public QueryStat withId(Integer id) + { + this.id = id; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java new file mode 100644 index 00000000..ffb29a8b --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatFilterCriteria.java @@ -0,0 +1,162 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStatFilterCriteria extends QRecordEntity +{ + private Integer queryStatId; + private String fieldName; + private String operator; + private String values; + + + + /******************************************************************************* + ** Getter for queryStatId + *******************************************************************************/ + public Integer getQueryStatId() + { + return (this.queryStatId); + } + + + + /******************************************************************************* + ** Setter for queryStatId + *******************************************************************************/ + public void setQueryStatId(Integer queryStatId) + { + this.queryStatId = queryStatId; + } + + + + /******************************************************************************* + ** Fluent setter for queryStatId + *******************************************************************************/ + public QueryStatFilterCriteria withQueryStatId(Integer queryStatId) + { + this.queryStatId = queryStatId; + return (this); + } + + + + /******************************************************************************* + ** Getter for fieldName + *******************************************************************************/ + public String getFieldName() + { + return (this.fieldName); + } + + + + /******************************************************************************* + ** Setter for fieldName + *******************************************************************************/ + public void setFieldName(String fieldName) + { + this.fieldName = fieldName; + } + + + + /******************************************************************************* + ** Fluent setter for fieldName + *******************************************************************************/ + public QueryStatFilterCriteria withFieldName(String fieldName) + { + this.fieldName = fieldName; + return (this); + } + + + + /******************************************************************************* + ** Getter for operator + *******************************************************************************/ + public String getOperator() + { + return (this.operator); + } + + + + /******************************************************************************* + ** Setter for operator + *******************************************************************************/ + public void setOperator(String operator) + { + this.operator = operator; + } + + + + /******************************************************************************* + ** Fluent setter for operator + *******************************************************************************/ + public QueryStatFilterCriteria withOperator(String operator) + { + this.operator = operator; + return (this); + } + + + + /******************************************************************************* + ** Getter for values + *******************************************************************************/ + public String getValues() + { + return (this.values); + } + + + + /******************************************************************************* + ** Setter for values + *******************************************************************************/ + public void setValues(String values) + { + this.values = values; + } + + + + /******************************************************************************* + ** Fluent setter for values + *******************************************************************************/ + public QueryStatFilterCriteria withValues(String values) + { + this.values = values; + return (this); + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java new file mode 100644 index 00000000..eda40a98 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/helpers/querystats/QueryStatManager.java @@ -0,0 +1,217 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats; + + +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.context.QContext; +import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.session.QSession; +import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class QueryStatManager +{ + private static QueryStatManager queryStatManager = null; + + // todo - support multiple qInstances? + private QInstance qInstance; + private Supplier sessionSupplier; + + private boolean active = false; + private List queryStats = new ArrayList<>(); + + private ScheduledExecutorService executorService; + + + + /******************************************************************************* + ** Singleton constructor + *******************************************************************************/ + private QueryStatManager() + { + + } + + + + /******************************************************************************* + ** Singleton accessor + *******************************************************************************/ + public static QueryStatManager getInstance() + { + if(queryStatManager == null) + { + queryStatManager = new QueryStatManager(); + } + return (queryStatManager); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void start(Supplier sessionSupplier) + { + qInstance = QContext.getQInstance(); + this.sessionSupplier = sessionSupplier; + + active = true; + queryStats = new ArrayList<>(); + + executorService = Executors.newSingleThreadScheduledExecutor(); + executorService.scheduleAtFixedRate(new QueryStatManagerInsertJob(), 60, 60, TimeUnit.SECONDS); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void stop() + { + active = false; + queryStats.clear(); + + if(executorService != null) + { + executorService.shutdown(); + executorService = null; + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void add(QueryStat queryStat) + { + if(active) + { + synchronized(this) + { + if(queryStat.getFirstResultTimestamp() == null) + { + //////////////////////////////////////////////// + // in case it didn't get set in the interface // + //////////////////////////////////////////////// + queryStat.setFirstResultTimestamp(Instant.now()); + } + + /////////////////////////////////////////////// + // compute the millis (so you don't have to) // + /////////////////////////////////////////////// + if(queryStat.getStartTimestamp() != null && queryStat.getFirstResultTimestamp() != null && queryStat.getFirstResultMillis() == null) + { + long millis = queryStat.getFirstResultTimestamp().toEpochMilli() - queryStat.getStartTimestamp().toEpochMilli(); + queryStat.setFirstResultMillis((int) millis); + } + + queryStats.add(queryStat); + } + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private List getListAndReset() + { + if(queryStats.isEmpty()) + { + return Collections.emptyList(); + } + + synchronized(this) + { + List returnList = queryStats; + queryStats = new ArrayList<>(); + return (returnList); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static class QueryStatManagerInsertJob implements Runnable + { + private static final QLogger LOG = QLogger.getLogger(QueryStatManagerInsertJob.class); + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run() + { + try + { + QContext.init(getInstance().qInstance, getInstance().sessionSupplier.get()); + + List list = getInstance().getListAndReset(); + LOG.info(logPair("queryStatListSize", list.size())); + + if(list.isEmpty()) + { + return; + } + + try + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(QueryStat.TABLE_NAME); + insertInput.setRecords(list.stream().map(qs -> qs.toQRecord()).toList()); + new InsertAction().execute(insertInput); + } + catch(Exception e) + { + LOG.error("Error inserting query stats", e); + } + } + finally + { + QContext.clear(); + } + } + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QAssociation.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QAssociation.java new file mode 100644 index 00000000..7d5ccca1 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QAssociation.java @@ -0,0 +1,45 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.data; + + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +/******************************************************************************* + ** Annotation to place onto fields in a QRecordEntity, to mark them as associated + ** record lists + ** + *******************************************************************************/ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.RUNTIME) +public @interface QAssociation +{ + /******************************************************************************* + ** + *******************************************************************************/ + String name(); + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java index ee6827d1..87242d5b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java @@ -23,8 +23,12 @@ package com.kingsrook.qqq.backend.core.model.data; import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedParameterizedType; +import java.lang.reflect.AnnotatedType; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Type; import java.math.BigDecimal; import java.time.Instant; import java.time.LocalDate; @@ -37,6 +41,7 @@ import java.util.Optional; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException; import com.kingsrook.qqq.backend.core.logging.QLogger; +import com.kingsrook.qqq.backend.core.utils.CollectionUtils; import com.kingsrook.qqq.backend.core.utils.ListingHash; @@ -47,7 +52,8 @@ public abstract class QRecordEntity { private static final QLogger LOG = QLogger.getLogger(QRecordEntity.class); - private static final ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>(); + private static final ListingHash, QRecordEntityField> fieldMapping = new ListingHash<>(); + private static final ListingHash, QRecordEntityAssociation> associationMapping = new ListingHash<>(); @@ -75,17 +81,34 @@ public abstract class QRecordEntity ** Build an entity of this QRecord type from a QRecord ** *******************************************************************************/ - protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException + protected void populateFromQRecord(QRecord qRecord) throws QRuntimeException { try { - List fieldList = getFieldList(this.getClass()); - for(QRecordEntityField qRecordEntityField : fieldList) + for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass())) { Serializable value = qRecord.getValue(qRecordEntityField.getFieldName()); Object typedValue = qRecordEntityField.convertValueType(value); qRecordEntityField.getSetter().invoke(this, typedValue); } + + for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + { + List associatedRecords = qRecord.getAssociatedRecords().get(qRecordEntityAssociation.getAssociationAnnotation().name()); + if(associatedRecords == null) + { + qRecordEntityAssociation.getSetter().invoke(this, (Object) null); + } + else + { + List associatedEntityList = new ArrayList<>(); + for(QRecord associatedRecord : CollectionUtils.nonNullList(associatedRecords)) + { + associatedEntityList.add(QRecordEntity.fromQRecord(qRecordEntityAssociation.getAssociatedType(), associatedRecord)); + } + qRecordEntityAssociation.getSetter().invoke(this, associatedEntityList); + } + } } catch(Exception e) { @@ -105,12 +128,30 @@ public abstract class QRecordEntity { QRecord qRecord = new QRecord(); - List fieldList = getFieldList(this.getClass()); - for(QRecordEntityField qRecordEntityField : fieldList) + for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass())) { qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this)); } + for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass())) + { + List associatedEntities = (List) qRecordEntityAssociation.getGetter().invoke(this); + String associationName = qRecordEntityAssociation.getAssociationAnnotation().name(); + + if(associatedEntities != null) + { + ///////////////////////////////////////////////////////////////////////////////// + // do this so an empty list in the entity becomes an empty list in the QRecord // + ///////////////////////////////////////////////////////////////////////////////// + qRecord.withAssociatedRecords(associationName, new ArrayList<>()); + } + + for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities)) + { + qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord()); + } + } + return (qRecord); } catch(Exception e) @@ -154,15 +195,73 @@ public abstract class QRecordEntity + /******************************************************************************* + ** + *******************************************************************************/ + public static List getAssociationList(Class c) + { + if(!associationMapping.containsKey(c)) + { + List associationList = new ArrayList<>(); + for(Method possibleGetter : c.getMethods()) + { + if(isGetter(possibleGetter)) + { + Optional setter = getSetterForGetter(c, possibleGetter); + + if(setter.isPresent()) + { + String fieldName = getFieldNameFromGetter(possibleGetter); + Optional associationAnnotation = getQAssociationAnnotation(c, fieldName); + + if(associationAnnotation.isPresent()) + { + Class listTypeParam = (Class) getListTypeParam(possibleGetter.getReturnType(), possibleGetter.getAnnotatedReturnType()); + associationList.add(new QRecordEntityAssociation(fieldName, possibleGetter, setter.get(), listTypeParam, associationAnnotation.orElse(null))); + } + } + else + { + LOG.info("Getter method [" + possibleGetter.getName() + "] does not have a corresponding setter."); + } + } + } + associationMapping.put(c, associationList); + } + return (associationMapping.get(c)); + } + + + /******************************************************************************* ** *******************************************************************************/ public static Optional getQFieldAnnotation(Class c, String fieldName) + { + return (getAnnotationOnField(c, QField.class, fieldName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional getQAssociationAnnotation(Class c, String fieldName) + { + return (getAnnotationOnField(c, QAssociation.class, fieldName)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public static Optional getAnnotationOnField(Class c, Class annotationClass, String fieldName) { try { Field field = c.getDeclaredField(fieldName); - return (Optional.ofNullable(field.getAnnotation(QField.class))); + return (Optional.ofNullable(field.getAnnotation(annotationClass))); } catch(NoSuchFieldException e) { @@ -197,7 +296,7 @@ public abstract class QRecordEntity { if(method.getParameterTypes().length == 0 && method.getName().matches("^get[A-Z].*")) { - if(isSupportedFieldType(method.getReturnType())) + if(isSupportedFieldType(method.getReturnType()) || isSupportedAssociation(method.getReturnType(), method.getAnnotatedReturnType())) { return (true); } @@ -261,4 +360,41 @@ public abstract class QRecordEntity ///////////////////////////////////////////// } + + + /******************************************************************************* + ** + *******************************************************************************/ + private static boolean isSupportedAssociation(Class returnType, AnnotatedType annotatedType) + { + Class listTypeParam = getListTypeParam(returnType, annotatedType); + return (listTypeParam != null && QRecordEntity.class.isAssignableFrom(listTypeParam)); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Class getListTypeParam(Class listType, AnnotatedType annotatedType) + { + if(listType.equals(List.class)) + { + if(annotatedType instanceof AnnotatedParameterizedType apt) + { + AnnotatedType[] annotatedActualTypeArguments = apt.getAnnotatedActualTypeArguments(); + for(AnnotatedType annotatedActualTypeArgument : annotatedActualTypeArguments) + { + Type type = annotatedActualTypeArgument.getType(); + if(type instanceof Class c) + { + return (c); + } + } + } + } + + return (null); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityAssociation.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityAssociation.java new file mode 100644 index 00000000..1262e339 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityAssociation.java @@ -0,0 +1,110 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.data; + + +import java.lang.reflect.Method; + + +/******************************************************************************* + ** Reflective information about an association in a QRecordEntity + *******************************************************************************/ +public class QRecordEntityAssociation +{ + private final String fieldName; + private final Method getter; + private final Method setter; + + private final Class associatedType; + + private final QAssociation associationAnnotation; + + + + /******************************************************************************* + ** Constructor. + *******************************************************************************/ + public QRecordEntityAssociation(String fieldName, Method getter, Method setter, Class associatedType, QAssociation associationAnnotation) + { + this.fieldName = fieldName; + this.getter = getter; + this.setter = setter; + this.associatedType = associatedType; + this.associationAnnotation = associationAnnotation; + } + + + + /******************************************************************************* + ** Getter for fieldName + ** + *******************************************************************************/ + public String getFieldName() + { + return fieldName; + } + + + + /******************************************************************************* + ** Getter for getter + ** + *******************************************************************************/ + public Method getGetter() + { + return getter; + } + + + + /******************************************************************************* + ** Getter for setter + ** + *******************************************************************************/ + public Method getSetter() + { + return setter; + } + + + + /******************************************************************************* + ** Getter for associatedType + ** + *******************************************************************************/ + public Class getAssociatedType() + { + return associatedType; + } + + + + /******************************************************************************* + ** Getter for associationAnnotation + ** + *******************************************************************************/ + public QAssociation getAssociationAnnotation() + { + return associationAnnotation; + } + +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java index a1f05986..a2e899b3 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntityTest.java @@ -23,10 +23,14 @@ package com.kingsrook.qqq.backend.core.model.data; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; import com.kingsrook.qqq.backend.core.BaseTest; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.data.testentities.Item; import com.kingsrook.qqq.backend.core.model.data.testentities.ItemWithPrimitives; +import com.kingsrook.qqq.backend.core.model.data.testentities.LineItem; +import com.kingsrook.qqq.backend.core.model.data.testentities.Order; import com.kingsrook.qqq.backend.core.model.metadata.fields.DisplayFormat; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType; @@ -34,6 +38,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -252,7 +257,103 @@ class QRecordEntityTest extends BaseTest assertEquals(QFieldType.STRING, qTableMetaData.getField("sku").getType()); assertEquals(QFieldType.INTEGER, qTableMetaData.getField("quantity").getType()); + } + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOrderWithAssociationsToQRecord() throws QException + { + Order order = new Order(); + order.setOrderNo("ORD001"); + order.setLineItems(List.of( + new LineItem().withSku("ABC").withQuantity(1), + new LineItem().withSku("DEF").withQuantity(2) + )); + + QRecord qRecord = order.toQRecord(); + assertEquals("ORD001", qRecord.getValueString("orderNo")); + List lineItems = qRecord.getAssociatedRecords().get("lineItems"); + assertNotNull(lineItems); + assertEquals(2, lineItems.size()); + assertEquals("ABC", lineItems.get(0).getValueString("sku")); + assertEquals(1, lineItems.get(0).getValueInteger("quantity")); + assertEquals("DEF", lineItems.get(1).getValueString("sku")); + assertEquals(2, lineItems.get(1).getValueInteger("quantity")); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testOrderWithoutAssociationsToQRecord() throws QException + { + Order order = new Order(); + order.setOrderNo("ORD001"); + order.setLineItems(null); + + QRecord qRecord = order.toQRecord(); + assertEquals("ORD001", qRecord.getValueString("orderNo")); + List lineItems = qRecord.getAssociatedRecords().get("lineItems"); + assertNull(lineItems); + + order.setLineItems(new ArrayList<>()); + qRecord = order.toQRecord(); + lineItems = qRecord.getAssociatedRecords().get("lineItems"); + assertNotNull(lineItems); + assertEquals(0, lineItems.size()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQRecordWithAssociationsToOrder() throws QException + { + QRecord qRecord = new QRecord() + .withValue("orderNo", "ORD002") + .withAssociatedRecords("lineItems", List.of( + new QRecord().withValue("sku", "AB12").withValue("quantity", 42), + new QRecord().withValue("sku", "XY89").withValue("quantity", 47) + )); + + Order order = qRecord.toEntity(Order.class); + assertEquals("ORD002", order.getOrderNo()); + assertEquals(2, order.getLineItems().size()); + assertEquals("AB12", order.getLineItems().get(0).getSku()); + assertEquals(42, order.getLineItems().get(0).getQuantity()); + assertEquals("XY89", order.getLineItems().get(1).getSku()); + assertEquals(47, order.getLineItems().get(1).getQuantity()); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Test + void testQRecordWithoutAssociationsToOrder() throws QException + { + QRecord qRecord = new QRecord().withValue("orderNo", "ORD002"); + Order order = qRecord.toEntity(Order.class); + assertEquals("ORD002", order.getOrderNo()); + assertNull(order.getLineItems()); + + qRecord.withAssociatedRecords("lineItems", null); + order = qRecord.toEntity(Order.class); + assertNull(order.getLineItems()); + + qRecord.withAssociatedRecords("lineItems", new ArrayList<>()); + order = qRecord.toEntity(Order.class); + assertNotNull(order.getLineItems()); + assertEquals(0, order.getLineItems().size()); } } \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/LineItem.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/LineItem.java new file mode 100644 index 00000000..6522c4f9 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/LineItem.java @@ -0,0 +1,102 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.data.testentities; + + +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Sample of an entity that can be converted to & from a QRecord + *******************************************************************************/ +public class LineItem extends QRecordEntity +{ + @QField() + private String sku; + + @QField() + private Integer quantity; + + + + /******************************************************************************* + ** Getter for sku + *******************************************************************************/ + public String getSku() + { + return (this.sku); + } + + + + /******************************************************************************* + ** Setter for sku + *******************************************************************************/ + public void setSku(String sku) + { + this.sku = sku; + } + + + + /******************************************************************************* + ** Fluent setter for sku + *******************************************************************************/ + public LineItem withSku(String sku) + { + this.sku = sku; + return (this); + } + + + + /******************************************************************************* + ** Getter for quantity + *******************************************************************************/ + public Integer getQuantity() + { + return (this.quantity); + } + + + + /******************************************************************************* + ** Setter for quantity + *******************************************************************************/ + public void setQuantity(Integer quantity) + { + this.quantity = quantity; + } + + + + /******************************************************************************* + ** Fluent setter for quantity + *******************************************************************************/ + public LineItem withQuantity(Integer quantity) + { + this.quantity = quantity; + return (this); + } + +} \ No newline at end of file diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Order.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Order.java new file mode 100644 index 00000000..10a30ac0 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/model/data/testentities/Order.java @@ -0,0 +1,104 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.model.data.testentities; + + +import java.util.List; +import com.kingsrook.qqq.backend.core.model.data.QAssociation; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** Sample of an entity that can be converted to & from a QRecord + *******************************************************************************/ +public class Order extends QRecordEntity +{ + @QField() + private String orderNo; + + @QAssociation(name = "lineItems") + private List lineItems; + + + + /******************************************************************************* + ** Getter for orderNo + *******************************************************************************/ + public String getOrderNo() + { + return (this.orderNo); + } + + + + /******************************************************************************* + ** Setter for orderNo + *******************************************************************************/ + public void setOrderNo(String orderNo) + { + this.orderNo = orderNo; + } + + + + /******************************************************************************* + ** Fluent setter for orderNo + *******************************************************************************/ + public Order withOrderNo(String orderNo) + { + this.orderNo = orderNo; + return (this); + } + + + + /******************************************************************************* + ** Getter for lineItems + *******************************************************************************/ + public List getLineItems() + { + return (this.lineItems); + } + + + + /******************************************************************************* + ** Setter for lineItems + *******************************************************************************/ + public void setLineItems(List lineItems) + { + this.lineItems = lineItems; + } + + + + /******************************************************************************* + ** Fluent setter for lineItems + *******************************************************************************/ + public Order withLineItems(List lineItems) + { + this.lineItems = lineItems; + return (this); + } + +} \ No newline at end of file diff --git a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java index c82a18d4..212e3a8f 100644 --- a/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java +++ b/qqq-backend-module-rdbms/src/main/java/com/kingsrook/qqq/backend/module/rdbms/actions/RDBMSQueryAction.java @@ -33,6 +33,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.stream.Collectors; import com.kingsrook.qqq.backend.core.actions.interfaces.QueryInterface; +import com.kingsrook.qqq.backend.core.actions.tables.helpers.querystats.QueryStat; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.logging.QLogger; import com.kingsrook.qqq.backend.core.model.actions.tables.query.JoinsContext; @@ -56,6 +57,8 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf { private static final QLogger LOG = QLogger.getLogger(RDBMSQueryAction.class); + private QueryStat queryStat; + /******************************************************************************* @@ -278,4 +281,26 @@ public class RDBMSQueryAction extends AbstractRDBMSAction implements QueryInterf return (statement); } + + + /******************************************************************************* + ** Getter for queryStat + *******************************************************************************/ + @Override + public QueryStat getQueryStat() + { + return (this.queryStat); + } + + + + /******************************************************************************* + ** Setter for queryStat + *******************************************************************************/ + @Override + public void setQueryStat(QueryStat queryStat) + { + this.queryStat = queryStat; + } + }