Adding associated records to Get, Query.

This commit is contained in:
2023-03-30 16:56:18 -05:00
parent 3df4513cd1
commit 7e368c6ff9
9 changed files with 576 additions and 23 deletions

View File

@ -329,6 +329,8 @@ public class GetAction
}
queryInput.setFilter(filter);
queryInput.setIncludeAssociations(getInput.getIncludeAssociations());
queryInput.setAssociationNamesToInclude(getInput.getAssociationNamesToInclude());
queryInput.setShouldFetchHeavyFields(getInput.getShouldFetchHeavyFields());
QueryOutput queryOutput = new QueryAction().execute(queryInput);

View File

@ -22,8 +22,13 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
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;
@ -31,13 +36,23 @@ import com.kingsrook.qqq.backend.core.actions.customizers.TableCustomizers;
import com.kingsrook.qqq.backend.core.actions.reporting.BufferedRecordPipe;
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;
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.QCriteriaOperator;
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.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.model.metadata.joins.JoinOn;
import com.kingsrook.qqq.backend.core.model.metadata.joins.QJoinMetaData;
import com.kingsrook.qqq.backend.core.model.metadata.tables.Association;
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleDispatcher;
import com.kingsrook.qqq.backend.core.modules.backend.QBackendModuleInterface;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.ListingHash;
/*******************************************************************************
@ -70,6 +85,14 @@ public class QueryAction
queryInput.getRecordPipe().setPostRecordActions(this::postRecordActions);
}
if(queryInput.getIncludeAssociations() && queryInput.getRecordPipe() != null)
{
//////////////////////////////////////////////
// todo - support this in the future maybe? //
//////////////////////////////////////////////
throw (new QException("Associations may not be fetched into a RecordPipe."));
}
QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher();
QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(queryInput.getBackend());
// todo pre-customization - just get to modify the request?
@ -86,11 +109,119 @@ public class QueryAction
postRecordActions(queryOutput.getRecords());
}
if(queryInput.getIncludeAssociations())
{
manageAssociations(queryInput, queryOutput);
}
return queryOutput;
}
/*******************************************************************************
**
*******************************************************************************/
private void manageAssociations(QueryInput queryInput, QueryOutput queryOutput) throws QException
{
QTableMetaData table = queryInput.getTable();
for(Association association : CollectionUtils.nonNullList(table.getAssociations()))
{
if(queryInput.getAssociationNamesToInclude() == null || queryInput.getAssociationNamesToInclude().contains(association.getName()))
{
// e.g., order -> orderLine
QJoinMetaData join = QContext.getQInstance().getJoin(association.getJoinName()); // todo ... ever need to flip?
// just assume this, at least for now... if(BooleanUtils.isTrue(association.getDoInserts()))
QueryInput nextLevelQueryInput = new QueryInput();
nextLevelQueryInput.setTableName(association.getAssociatedTableName());
nextLevelQueryInput.setIncludeAssociations(true);
nextLevelQueryInput.setAssociationNamesToInclude(buildNextLevelAssociationNamesToInclude(association.getName(), queryInput.getAssociationNamesToInclude()));
QQueryFilter filter = new QQueryFilter();
nextLevelQueryInput.setFilter(filter);
ListingHash<List<Serializable>, QRecord> outerResultMap = new ListingHash<>();
if(join.getJoinOns().size() == 1)
{
JoinOn joinOn = join.getJoinOns().get(0);
Set<Serializable> values = new HashSet<>();
for(QRecord record : queryOutput.getRecords())
{
Serializable value = record.getValue(joinOn.getLeftField());
values.add(value);
outerResultMap.add(List.of(value), record);
}
filter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.IN, new ArrayList<>(values)));
}
else
{
filter.setBooleanOperator(QQueryFilter.BooleanOperator.OR);
for(QRecord record : queryOutput.getRecords())
{
QQueryFilter subFilter = new QQueryFilter();
filter.addSubFilter(subFilter);
List<Serializable> values = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns())
{
Serializable value = record.getValue(joinOn.getLeftField());
values.add(value);
subFilter.addCriteria(new QFilterCriteria(joinOn.getRightField(), QCriteriaOperator.EQUALS, value));
}
outerResultMap.add(values, record);
}
}
QueryOutput nextLevelQueryOutput = new QueryAction().execute(nextLevelQueryInput);
for(QRecord record : nextLevelQueryOutput.getRecords())
{
List<Serializable> values = new ArrayList<>();
for(JoinOn joinOn : join.getJoinOns())
{
Serializable value = record.getValue(joinOn.getRightField());
values.add(value);
}
if(outerResultMap.containsKey(values))
{
for(QRecord outerRecord : outerResultMap.get(values))
{
outerRecord.withAssociatedRecord(association.getName(), record);
}
}
}
}
}
}
/*******************************************************************************
**
*******************************************************************************/
private Collection<String> buildNextLevelAssociationNamesToInclude(String name, Collection<String> associationNamesToInclude)
{
if(associationNamesToInclude == null)
{
return (associationNamesToInclude);
}
Set<String> rs = new HashSet<>();
for(String nextLevelCandidateName : associationNamesToInclude)
{
if(nextLevelCandidateName.startsWith(name + "."))
{
rs.add(nextLevelCandidateName.replaceFirst(name + ".", ""));
}
}
return (rs);
}
/*******************************************************************************
** Run the necessary actions on a list of records (which must be a mutable list - e.g.,
** not one created via List.of()). This may include setting display values,

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.get;
import java.io.Serializable;
import java.util.Collection;
import java.util.Map;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput;
@ -44,6 +45,15 @@ public class GetInput extends AbstractTableActionInput
private boolean shouldFetchHeavyFields = true;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
// if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. //
// to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
/*******************************************************************************
**
@ -229,4 +239,66 @@ public class GetInput extends AbstractTableActionInput
return (this);
}
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
public boolean getIncludeAssociations()
{
return (this.includeAssociations);
}
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
public void setIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
}
/*******************************************************************************
** Fluent setter for includeAssociations
*******************************************************************************/
public GetInput withIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
return (this);
}
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
public Collection<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public GetInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
}

View File

@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import com.kingsrook.qqq.backend.core.actions.QBackendTransaction;
@ -56,6 +57,14 @@ public class QueryInput extends AbstractTableActionInput
private List<QueryJoin> queryJoins = null;
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// if you say you want to includeAssociations, you can limit which ones by passing them in associationNamesToInclude. //
// if you leave it null, you get all associations defined on the table. if you pass it as empty, you get none. //
// to go to a recursive level of associations, you need to dot-qualify the names. e.g., A, B, A.C, A.D, A.C.E //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
private boolean includeAssociations = false;
private Collection<String> associationNamesToInclude = null;
/*******************************************************************************
@ -425,4 +434,67 @@ public class QueryInput extends AbstractTableActionInput
super.withTableName(tableName);
return (this);
}
/*******************************************************************************
** Getter for includeAssociations
*******************************************************************************/
public boolean getIncludeAssociations()
{
return (this.includeAssociations);
}
/*******************************************************************************
** Setter for includeAssociations
*******************************************************************************/
public void setIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
}
/*******************************************************************************
** Fluent setter for includeAssociations
*******************************************************************************/
public QueryInput withIncludeAssociations(boolean includeAssociations)
{
this.includeAssociations = includeAssociations;
return (this);
}
/*******************************************************************************
** Getter for associationNamesToInclude
*******************************************************************************/
public Collection<String> getAssociationNamesToInclude()
{
return (this.associationNamesToInclude);
}
/*******************************************************************************
** Setter for associationNamesToInclude
*******************************************************************************/
public void setAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
}
/*******************************************************************************
** Fluent setter for associationNamesToInclude
*******************************************************************************/
public QueryInput withAssociationNamesToInclude(Collection<String> associationNamesToInclude)
{
this.associationNamesToInclude = associationNamesToInclude;
return (this);
}
}

View File

@ -170,6 +170,10 @@ public class MemoryRecordStore
}
else
{
//////////////////////////////////////////////////////////////////////////////////
// make sure we're not giving back records that are all full of associations... //
//////////////////////////////////////////////////////////////////////////////////
qRecord.setAssociatedRecords(new HashMap<>());
records.add(qRecord);
}
}

View File

@ -22,18 +22,25 @@
package com.kingsrook.qqq.backend.core.actions.tables;
import java.util.ArrayList;
import java.util.List;
import com.kingsrook.qqq.backend.core.BaseTest;
import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryOutput;
import com.kingsrook.qqq.backend.core.model.data.QRecord;
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
import com.kingsrook.qqq.backend.core.utils.TestUtils;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
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.assertTrue;
/*******************************************************************************
@ -124,4 +131,193 @@ class QueryActionTest extends BaseTest
assertThat(records).isNotEmpty();
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociations() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter());
queryInput.setIncludeAssociations(true);
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QRecord order0 = queryOutput.getRecords().get(0);
assertEquals(2, order0.getAssociatedRecords().get("orderLine").size());
assertEquals(3, order0.getAssociatedRecords().get("extrinsics").size());
QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0);
assertEquals(1, orderLine00.getAssociatedRecords().get("extrinsics").size());
QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1);
assertEquals(2, orderLine01.getAssociatedRecords().get("extrinsics").size());
QRecord order1 = queryOutput.getRecords().get(1);
assertEquals(1, order1.getAssociatedRecords().get("orderLine").size());
assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociationsNoAssociationNamesToInclude() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter());
queryInput.setIncludeAssociations(true);
queryInput.setAssociationNamesToInclude(new ArrayList<>());
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QRecord order0 = queryOutput.getRecords().get(0);
assertTrue(CollectionUtils.nullSafeIsEmpty(order0.getAssociatedRecords()));
QRecord order1 = queryOutput.getRecords().get(1);
assertTrue(CollectionUtils.nullSafeIsEmpty(order1.getAssociatedRecords()));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociationsLimitedAssociationNamesToInclude() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter());
queryInput.setIncludeAssociations(true);
queryInput.setAssociationNamesToInclude(List.of("orderLine"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QRecord order0 = queryOutput.getRecords().get(0);
assertEquals(2, order0.getAssociatedRecords().get("orderLine").size());
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order0.getAssociatedRecords().get("extrinsics"))));
QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0);
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics"))));
QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1);
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics"))));
QRecord order1 = queryOutput.getRecords().get(1);
assertEquals(1, order1.getAssociatedRecords().get("orderLine").size());
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order1.getAssociatedRecords().get("extrinsics"))));
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociationsLimitedAssociationNamesToIncludeChildTableDuplicatedAssociationNameExcluded() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter());
queryInput.setIncludeAssociations(true);
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// say that we want extrinsics - but that should only get them from the top-level -- to get them from the child, we need orderLine.extrinsics //
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
queryInput.setAssociationNamesToInclude(List.of("orderLine", "extrinsics"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QRecord order0 = queryOutput.getRecords().get(0);
assertEquals(2, order0.getAssociatedRecords().get("orderLine").size());
assertEquals(3, order0.getAssociatedRecords().get("extrinsics").size());
QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0);
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics"))));
QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1);
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics"))));
QRecord order1 = queryOutput.getRecords().get(1);
assertEquals(1, order1.getAssociatedRecords().get("orderLine").size());
assertEquals(1, order1.getAssociatedRecords().get("extrinsics").size());
}
/*******************************************************************************
**
*******************************************************************************/
@Test
void testQueryAssociationsLimitedAssociationNamesToIncludeChildTableDuplicatedAssociationNameIncluded() throws QException
{
QContext.getQSession().withSecurityKeyValue(TestUtils.SECURITY_KEY_TYPE_STORE_ALL_ACCESS, true);
insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations();
QueryInput queryInput = new QueryInput();
queryInput.setTableName(TestUtils.TABLE_NAME_ORDER);
queryInput.setFilter(new QQueryFilter());
queryInput.setIncludeAssociations(true);
/////////////////////////////////////////////////////////////////////////////
// this time say we want the orderLine.extrinsics - not the top-level ones //
/////////////////////////////////////////////////////////////////////////////
queryInput.setAssociationNamesToInclude(List.of("orderLine", "orderLine.extrinsics"));
QueryOutput queryOutput = new QueryAction().execute(queryInput);
QRecord order0 = queryOutput.getRecords().get(0);
assertEquals(2, order0.getAssociatedRecords().get("orderLine").size());
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order0.getAssociatedRecords().get("extrinsics"))));
QRecord orderLine00 = order0.getAssociatedRecords().get("orderLine").get(0);
assertFalse(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine00.getAssociatedRecords().get("extrinsics"))));
QRecord orderLine01 = order0.getAssociatedRecords().get("orderLine").get(1);
assertFalse(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(orderLine01.getAssociatedRecords().get("extrinsics"))));
QRecord order1 = queryOutput.getRecords().get(1);
assertEquals(1, order1.getAssociatedRecords().get("orderLine").size());
assertTrue(CollectionUtils.nullSafeIsEmpty(CollectionUtils.nonNullCollection(order1.getAssociatedRecords().get("extrinsics"))));
}
/*******************************************************************************
**
*******************************************************************************/
private static void insert2OrdersWith3Lines3LineExtrinsicsAnd4OrderExtrinsicAssociations() throws QException
{
InsertInput insertInput = new InsertInput();
insertInput.setTableName(TestUtils.TABLE_NAME_ORDER);
insertInput.setRecords(List.of(
new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD123")
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC1").withValue("quantity", 1)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-1.1").withValue("value", "LINE-VAL-1")))
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC2").withValue("quantity", 2)
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.1").withValue("value", "LINE-VAL-2"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "LINE-EXT-2.2").withValue("value", "LINE-VAL-3")))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-1").withValue("value", "MY-VALUE-1"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-2").withValue("value", "MY-VALUE-2"))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "MY-FIELD-3").withValue("value", "MY-VALUE-3")),
new QRecord().withValue("storeId", 1).withValue("orderNo", "ORD124")
.withAssociatedRecord("orderLine", new QRecord().withValue("sku", "BASIC3").withValue("quantity", 3))
.withAssociatedRecord("extrinsics", new QRecord().withValue("key", "YOUR-FIELD-1").withValue("value", "YOUR-VALUE-1"))
));
new InsertAction().execute(insertInput);
}
}