Skip to content

Commit a6001af

Browse files
committed
Add overload of toQRecordOnlyChangedFields that allows primary keys to be included (more useful for the update use-case)
1 parent b028187 commit a6001af

File tree

4 files changed

+387
-60
lines changed

4 files changed

+387
-60
lines changed

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/data/QRecordEntity.java

Lines changed: 91 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,14 @@
4141
import java.util.Map;
4242
import java.util.Objects;
4343
import java.util.Optional;
44+
import java.util.function.Function;
4445
import com.kingsrook.qqq.backend.core.exceptions.QException;
4546
import com.kingsrook.qqq.backend.core.exceptions.QRuntimeException;
4647
import com.kingsrook.qqq.backend.core.logging.QLogger;
48+
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
4749
import com.kingsrook.qqq.backend.core.utils.CollectionUtils;
4850
import com.kingsrook.qqq.backend.core.utils.ListingHash;
51+
import com.kingsrook.qqq.backend.core.utils.ObjectUtils;
4952
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
5053

5154

@@ -61,6 +64,11 @@ public abstract class QRecordEntity
6164

6265
private Map<String, Serializable> originalRecordValues;
6366

67+
////////////////////////////////////////////////////////////////////////////////
68+
// map of entity class names to QTableMetaData objects that they helped build //
69+
////////////////////////////////////////////////////////////////////////////////
70+
private static Map<String, QTableMetaData> tableReferences = new HashMap<>();
71+
6472

6573

6674
/*******************************************************************************
@@ -95,6 +103,19 @@ public static <T extends QRecordEntity> T fromQRecord(Class<T> c, QRecord qRecor
95103

96104

97105

106+
/***************************************************************************
107+
** register a mapping between an entity class and a table that it is associated with.
108+
***************************************************************************/
109+
public static void registerTable(Class<? extends QRecordEntity> entityClass, QTableMetaData table)
110+
{
111+
if(entityClass != null && table != null)
112+
{
113+
tableReferences.put(entityClass.getName(), table);
114+
}
115+
}
116+
117+
118+
98119
/*******************************************************************************
99120
** Build an entity of this QRecord type from a QRecord
100121
**
@@ -176,7 +197,10 @@ protected <T extends QRecordEntity> void populateFromQRecord(QRecord qRecord, St
176197

177198

178199
/*******************************************************************************
179-
** Convert this entity to a QRecord.
200+
** Convert this entity to a QRecord. ALL fields in the entity will be set
201+
** in the QRecord. Note that, if you're using this for an input to the UpdateAction,
202+
** that this could cause values to be set to null, e.g., if you constructed
203+
** a entity from scratch, and didn't set all values in it!!
180204
**
181205
*******************************************************************************/
182206
public QRecord toQRecord() throws QRuntimeException
@@ -190,25 +214,7 @@ public QRecord toQRecord() throws QRuntimeException
190214
qRecord.setValue(qRecordEntityField.getFieldName(), (Serializable) qRecordEntityField.getGetter().invoke(this));
191215
}
192216

193-
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
194-
{
195-
@SuppressWarnings("unchecked")
196-
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
197-
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
198-
199-
if(associatedEntities != null)
200-
{
201-
/////////////////////////////////////////////////////////////////////////////////
202-
// do this so an empty list in the entity becomes an empty list in the QRecord //
203-
/////////////////////////////////////////////////////////////////////////////////
204-
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
205-
}
206-
207-
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
208-
{
209-
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
210-
}
211-
}
217+
toQRecordProcessAssociations(qRecord, (entity) -> entity.toQRecord());
212218

213219
return (qRecord);
214220
}
@@ -220,15 +226,65 @@ public QRecord toQRecord() throws QRuntimeException
220226

221227

222228

229+
/***************************************************************************
230+
*
231+
***************************************************************************/
232+
private void toQRecordProcessAssociations(QRecord outputRecord, Function<QRecordEntity, QRecord> toRecordFunction) throws Exception
233+
{
234+
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
235+
{
236+
@SuppressWarnings("unchecked")
237+
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
238+
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
239+
240+
if(associatedEntities != null)
241+
{
242+
outputRecord.withAssociatedRecords(associationName, new ArrayList<>());
243+
for(QRecordEntity associatedEntity : associatedEntities)
244+
{
245+
outputRecord.withAssociatedRecord(associationName, toRecordFunction.apply(associatedEntity));
246+
}
247+
}
248+
}
249+
}
250+
251+
252+
223253
/*******************************************************************************
224-
**
254+
** Overload of toQRecordOnlyChangedFields that preserves original behavior of
255+
** that method, which is, to NOT includePrimaryKey
225256
*******************************************************************************/
257+
@Deprecated(since = "includePrimaryKey param was added")
226258
public QRecord toQRecordOnlyChangedFields()
259+
{
260+
return toQRecordOnlyChangedFields(false);
261+
}
262+
263+
264+
265+
/*******************************************************************************
266+
** Useful for the use-case of:
267+
** - fetch a QRecord (e.g., QueryAction or GetAction)
268+
** - build a QRecordEntity out of it
269+
** - change a field (or two) in it
270+
** - want to pass it into an UpdateAction, and want to see only the fields that
271+
** you know you changed get passed in to UpdateAction (e.g., PATCH semantics).
272+
**
273+
** But also - per the includePrimaryKey param, include the primaryKey in the
274+
** records (e.g., to tell the Update which records to update).
275+
**
276+
** Also, useful for:
277+
** - construct new entity, calling setters to populate some fields
278+
** - pass that entity into
279+
*******************************************************************************/
280+
public QRecord toQRecordOnlyChangedFields(boolean includePrimaryKey)
227281
{
228282
try
229283
{
230284
QRecord qRecord = new QRecord();
231285

286+
String primaryKeyFieldName = ObjectUtils.tryElse(() -> tableReferences.get(getClass().getName()).getPrimaryKeyField(), null);
287+
232288
for(QRecordEntityField qRecordEntityField : getFieldList(this.getClass()))
233289
{
234290
Serializable thisValue = (Serializable) qRecordEntityField.getGetter().invoke(this);
@@ -238,31 +294,16 @@ public QRecord toQRecordOnlyChangedFields()
238294
originalValue = originalRecordValues.get(qRecordEntityField.getFieldName());
239295
}
240296

241-
if(!Objects.equals(thisValue, originalValue))
297+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
298+
// if this value and the original value don't match - OR - this is the table's primary key field - then put the value in the record. //
299+
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
300+
if(!Objects.equals(thisValue, originalValue) || (includePrimaryKey && Objects.equals(primaryKeyFieldName, qRecordEntityField.getFieldName())))
242301
{
243302
qRecord.setValue(qRecordEntityField.getFieldName(), thisValue);
244303
}
245304
}
246305

247-
for(QRecordEntityAssociation qRecordEntityAssociation : getAssociationList(this.getClass()))
248-
{
249-
@SuppressWarnings("unchecked")
250-
List<? extends QRecordEntity> associatedEntities = (List<? extends QRecordEntity>) qRecordEntityAssociation.getGetter().invoke(this);
251-
String associationName = qRecordEntityAssociation.getAssociationAnnotation().name();
252-
253-
if(associatedEntities != null)
254-
{
255-
/////////////////////////////////////////////////////////////////////////////////
256-
// do this so an empty list in the entity becomes an empty list in the QRecord //
257-
/////////////////////////////////////////////////////////////////////////////////
258-
qRecord.withAssociatedRecords(associationName, new ArrayList<>());
259-
}
260-
261-
for(QRecordEntity associatedEntity : CollectionUtils.nonNullList(associatedEntities))
262-
{
263-
qRecord.withAssociatedRecord(associationName, associatedEntity.toQRecord());
264-
}
265-
}
306+
toQRecordProcessAssociations(qRecord, (entity) -> entity.toQRecordOnlyChangedFields(includePrimaryKey));
266307

267308
return (qRecord);
268309
}
@@ -488,15 +529,15 @@ private static boolean isSupportedFieldType(Class<?> returnType)
488529
{
489530
// todo - more types!!
490531
return (returnType.equals(String.class)
491-
|| returnType.equals(Integer.class)
492-
|| returnType.equals(int.class)
493-
|| returnType.equals(Boolean.class)
494-
|| returnType.equals(boolean.class)
495-
|| returnType.equals(BigDecimal.class)
496-
|| returnType.equals(Instant.class)
497-
|| returnType.equals(LocalDate.class)
498-
|| returnType.equals(LocalTime.class)
499-
|| returnType.equals(byte[].class));
532+
|| returnType.equals(Integer.class)
533+
|| returnType.equals(int.class)
534+
|| returnType.equals(Boolean.class)
535+
|| returnType.equals(boolean.class)
536+
|| returnType.equals(BigDecimal.class)
537+
|| returnType.equals(Instant.class)
538+
|| returnType.equals(LocalDate.class)
539+
|| returnType.equals(LocalTime.class)
540+
|| returnType.equals(byte[].class));
500541
/////////////////////////////////////////////
501542
// note - this list has implications upon: //
502543
// - QFieldType.fromClass //

qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/tables/QTableMetaData.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,14 +68,6 @@ public class QTableMetaData implements QAppChildMetaData, Serializable, MetaData
6868
private String name;
6969
private String label;
7070

71-
// TODO: resolve confusion over:
72-
// Is this name of what backend the table is stored in (yes)
73-
// Or the "name" of the table WITHIN the backend (no)
74-
// although that's how "backendName" is used in QFieldMetaData.
75-
// Idea:
76-
// rename "backendName" here to "backend"
77-
// add "nameInBackend" (or similar) for the table name in the backend
78-
// OR - add a whole "backendDetails" object, with different details per backend-type
7971
private String backendName;
8072
private String primaryKeyField;
8173
private boolean isHidden = false;
@@ -184,6 +176,12 @@ public QTableMetaData withFieldsFromEntity(Class<? extends QRecordEntity> entity
184176
}
185177
}
186178

179+
///////////////////////////////////////////////////////////////////////////////////////////////////
180+
// stash a reference from this entityClass to this table in the QRecordEntity class //
181+
// (used within that class later, if it wants to know about a table that an Entity helped build) //
182+
///////////////////////////////////////////////////////////////////////////////////////////////////
183+
QRecordEntity.registerTable(entityClass, this);
184+
187185
return (this);
188186
}
189187

0 commit comments

Comments
 (0)