diff --git a/project/gradle.properties b/project/gradle.properties index c6125b7d35..215e32a508 100644 --- a/project/gradle.properties +++ b/project/gradle.properties @@ -1,2 +1,2 @@ group=org.babyfish.jimmer -version=0.9.3 +version=0.9.4 diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/Mutations.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/Mutations.kt index ab681ddf39..c009641417 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/Mutations.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/Mutations.kt @@ -26,5 +26,5 @@ fun SaveException.NotUnique.isMatched( ) @Suppress("UNCHECKED_CAST") -operator fun SaveException.NotUnique.get(prop: KProperty1<*, T>): T = +operator fun SaveException.NotUnique.get(prop: KProperty1<*, T>): T = getValue(prop.toImmutableProp()) as T \ No newline at end of file diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/KSaveCommandPartialDsl.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/KSaveCommandPartialDsl.kt index 7354db9c09..9586d91348 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/KSaveCommandPartialDsl.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/KSaveCommandPartialDsl.kt @@ -49,6 +49,10 @@ interface KSaveCommandPartialDsl { fun setAutoIdOnlyTargetChecking(prop: KProperty1<*, *>) + fun setKeyOnlyAsReferenceAll() + + fun setKeyOnlyAsReference(prop: KProperty1<*, *>) + fun setDissociateAction(prop: KProperty1<*, *>, action: DissociateAction) fun setLockMode(lockMode: LockMode) diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/impl/KSaveCommandDslImpl.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/impl/KSaveCommandDslImpl.kt index 280b7d83c8..69404e06ea 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/impl/KSaveCommandDslImpl.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/mutation/impl/KSaveCommandDslImpl.kt @@ -103,6 +103,14 @@ internal class KSaveCommandDslImpl( javaCommand = javaCommand.setAutoIdOnlyTargetChecking(prop.toImmutableProp()) } + override fun setKeyOnlyAsReferenceAll() { + javaCommand = javaCommand.setKeyOnlyAsReferenceAll() + } + + override fun setKeyOnlyAsReference(prop: KProperty1<*, *>) { + javaCommand = javaCommand.setKeyOnlyAsReference(prop.toImmutableProp()) + } + override fun setDissociateAction(prop: KProperty1<*, *>, action: DissociateAction) { javaCommand = javaCommand.setDissociateAction(prop.toImmutableProp(), action) } diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNonNullTable.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNonNullTable.kt index c49a04b853..615534f39a 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNonNullTable.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNonNullTable.kt @@ -12,5 +12,38 @@ interface KNonNullTable : KTable, KNonNullProps, Selection { fun > fetch(staticType: KClass): Selection + /** + * If you must convert table types using the "asTableEx()" + * function to find corresponding properties in IDE's + * intelligent suggestions, it likely indicates you are performing + * table join operations on collection-associated properties, for example: + * + * ``` + * sql.executeQuery(Book::class) { + * // Table join based on collection association `Book.authors` + * where(table.asTableEx().authors.firstName eq "Alex") + * select(table) + * } + * ``` + * + * This usage will lead to data duplication, and whether using + * SQL-level or application-level approaches, you'll need to handle + * data distinction yourself. More importantly, this duplication + * will invalidate the pagination mechanism. + * + * In fact, this usage is not recommended, and the purpose of + * "asTableEx()" is to remind you that you're using a feature + * that might cause problems. In most cases, sub-queries, + * especially implicit sub-queries, are more recommended. For example: + * + * ``` + * sql.executeQuery(Book::class) { + * where += table.authors { + * firstName eq "Alex" + * } + * select(table) + * } + * ``` + */ override fun asTableEx(): KNonNullTableEx } diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNullableTable.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNullableTable.kt index 4778d14df8..0750d4bb40 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNullableTable.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KNullableTable.kt @@ -23,5 +23,38 @@ interface KNullableTable : KTable, KNullableProps, Selection { override fun getAssociatedId(prop: ImmutableProp): KNullablePropExpression + /** + * If you must convert table types using the "asTableEx()" + * function to find corresponding properties in IDE's + * intelligent suggestions, it likely indicates you are performing + * table join operations on collection-associated properties, for example: + * + * ``` + * sql.executeQuery(Book::class) { + * // Table join based on collection association `Book.authors` + * where(table.asTableEx().authors.firstName eq "Alex") + * select(table) + * } + * ``` + * + * This usage will lead to data duplication, and whether using + * SQL-level or application-level approaches, you'll need to handle + * data distinction yourself. More importantly, this duplication + * will invalidate the pagination mechanism. + * + * In fact, this usage is not recommended, and the purpose of + * "asTableEx()" is to remind you that you're using a feature + * that might cause problems. In most cases, sub-queries, + * especially implicit sub-queries, are more recommended. For example: + * + * ``` + * sql.executeQuery(Book::class) { + * where += table.authors { + * firstName eq "Alex" + * } + * select(table) + * } + * ``` + */ override fun asTableEx(): KNullableTableEx } \ No newline at end of file diff --git a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KTable.kt b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KTable.kt index 0e763edaef..ea4e8c791f 100644 --- a/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KTable.kt +++ b/project/jimmer-sql-kotlin/src/main/kotlin/org/babyfish/jimmer/sql/kt/ast/table/KTable.kt @@ -2,11 +2,44 @@ package org.babyfish.jimmer.sql.kt.ast.table import org.babyfish.jimmer.sql.ast.impl.table.TableSelection import org.babyfish.jimmer.sql.kt.ast.expression.KNonNullExpression -import org.babyfish.jimmer.sql.kt.ast.expression.KPropExpression import org.babyfish.jimmer.sql.kt.ast.expression.isNotNull import org.babyfish.jimmer.sql.kt.ast.expression.isNull interface KTable : KProps { + + /** + * If you must convert table types using the "asTableEx()" + * function to find corresponding properties in IDE's + * intelligent suggestions, it likely indicates you are performing + * table join operations on collection-associated properties, for example: + * + * ``` + * sql.executeQuery(Book::class) { + * // Table join based on collection association `Book.authors` + * where(table.asTableEx().authors.firstName eq "Alex") + * select(table) + * } + * ``` + * + * This usage will lead to data duplication, and whether using + * SQL-level or application-level approaches, you'll need to handle + * data distinction yourself. More importantly, this duplication + * will invalidate the pagination mechanism. + * + * In fact, this usage is not recommended, and the purpose of + * "asTableEx()" is to remind you that you're using a feature + * that might cause problems. In most cases, sub-queries, + * especially implicit sub-queries, are more recommended. For example: + * + * ``` + * sql.executeQuery(Book::class) { + * where += table.authors { + * firstName eq "Alex" + * } + * select(table) + * } + * ``` + */ fun asTableEx(): KTableEx } diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/AbstractEntitySaveCommandImpl.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/AbstractEntitySaveCommandImpl.java index 37654e8055..90b02592c5 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/AbstractEntitySaveCommandImpl.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/AbstractEntitySaveCommandImpl.java @@ -138,6 +138,34 @@ public IdOnlyAutoCheckingCfg(Cfg prev, ImmutableProp prop, boolean checking) { } } + static class KeyOnlyAsReferenceCfg extends Cfg { + + final MapNode mapNode; + + final boolean defaultValue; + + public KeyOnlyAsReferenceCfg(Cfg prev, boolean defaultValue) { + super(prev); + IdOnlyAutoCheckingCfg p = prev.as(IdOnlyAutoCheckingCfg.class); + this.mapNode = p != null ? p.mapNode : null; + this.defaultValue = defaultValue; + } + + public KeyOnlyAsReferenceCfg(Cfg prev, ImmutableProp prop, boolean asReference) { + super(prev); + if (!prop.isAssociation(TargetLevel.PERSISTENT)) { + throw new IllegalArgumentException( + "The property \"" + + prop + + "\" is not association property" + ); + } + IdOnlyAutoCheckingCfg p = prev.as(IdOnlyAutoCheckingCfg.class); + this.mapNode = new MapNode<>(p != null ? p.mapNode : null, prop, asReference); + this.defaultValue = p != null && p.defaultValue; + } + } + static class TargetTransferModeCfg extends Cfg { final MapNode mapNode; @@ -237,6 +265,10 @@ static final class OptionsImpl implements SaveOptions { private final boolean autoCheckingAll; + private final Map keyOnlyAsReferenceMap; + + private final boolean keyOnlyAsReferenceAll; + private final Map dissociateActionMap; private final Map targetTransferModeMap; @@ -257,6 +289,7 @@ static final class OptionsImpl implements SaveOptions { DeleteModeCfg deleteModeCfg = cfg.as(DeleteModeCfg.class); KeyPropsCfg keyPropsCfg = cfg.as(KeyPropsCfg.class); IdOnlyAutoCheckingCfg idOnlyAutoCheckingCfg = cfg.as(IdOnlyAutoCheckingCfg.class); + KeyOnlyAsReferenceCfg keyOnlyAsReferenceCfg = cfg.as(KeyOnlyAsReferenceCfg.class); DissociationActionCfg dissociationActionCfg = cfg.as(DissociationActionCfg.class); TargetTransferModeCfg targetTransferModeCfg = cfg.as(TargetTransferModeCfg.class); LockModeCfg lockModeCfg = cfg.as(LockModeCfg.class); @@ -276,8 +309,10 @@ static final class OptionsImpl implements SaveOptions { deleteModeCfg.mode : DeleteMode.AUTO; this.keyPropMultiMap = MapNode.toMap(keyPropsCfg, it -> it.mapNode);; - this.autoCheckingMap = MapNode.toMap(idOnlyAutoCheckingCfg, it -> it.mapNode);; + this.autoCheckingMap = MapNode.toMap(idOnlyAutoCheckingCfg, it -> it.mapNode); this.autoCheckingAll = idOnlyAutoCheckingCfg != null && idOnlyAutoCheckingCfg.defaultValue; + this.keyOnlyAsReferenceMap = MapNode.toMap(keyOnlyAsReferenceCfg, it -> it.mapNode); + this.keyOnlyAsReferenceAll = keyOnlyAsReferenceCfg != null && keyOnlyAsReferenceCfg.defaultValue; this.dissociateActionMap = MapNode.toMap(dissociationActionCfg, it -> it.mapNode);; this.targetTransferModeMap = MapNode.toMap(targetTransferModeCfg, it -> it.mapNode);; this.targetTransferModeAll = targetTransferModeCfg != null ? @@ -363,6 +398,14 @@ public boolean isAutoCheckingProp(ImmutableProp prop) { return autoCheckingAll || Boolean.TRUE.equals(autoCheckingMap.get(prop)); } + public boolean isKeyOnlyAsReference(ImmutableProp prop) { + Boolean value = keyOnlyAsReferenceMap.get(prop); + if (value != null) { + return value; + } + return keyOnlyAsReferenceAll; + } + @Override public DissociateAction getDissociateAction(ImmutableProp prop) { DissociateAction action = dissociateActionMap.get(prop); diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/BatchEntitySaveCommandImpl.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/BatchEntitySaveCommandImpl.java index 29d6d3a2e6..f7ea8d0f19 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/BatchEntitySaveCommandImpl.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/BatchEntitySaveCommandImpl.java @@ -112,6 +112,16 @@ public BatchEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop, return new BatchEntitySaveCommandImpl<>(new IdOnlyAutoCheckingCfg(cfg, prop, checking)); } + @Override + public BatchEntitySaveCommand setKeyOnlyAsReferenceAll() { + return new BatchEntitySaveCommandImpl<>(new KeyOnlyAsReferenceCfg(cfg, true)); + } + + @Override + public BatchEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop, boolean asReference) { + return new BatchEntitySaveCommandImpl<>(new KeyOnlyAsReferenceCfg(cfg, prop,asReference)); + } + @Override public BatchEntitySaveCommand setDissociateAction(ImmutableProp prop, DissociateAction dissociateAction) { return new BatchEntitySaveCommandImpl<>(new DissociationActionCfg(cfg, prop, dissociateAction)); diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Deleter.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Deleter.java index 614906f9e4..0eb3fb0d66 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Deleter.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Deleter.java @@ -372,6 +372,11 @@ public boolean isAutoCheckingProp(ImmutableProp prop) { return false; } + @Override + public boolean isKeyOnlyAsReference(ImmutableProp prop) { + return false; + } + @Override public @Nullable ExceptionTranslator getExceptionTranslator() { return null; diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/PreHandler.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/PreHandler.java index 65ff1b78d3..e9bfdfb72e 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/PreHandler.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/PreHandler.java @@ -318,6 +318,10 @@ final QueryReason queryReason(boolean hasId, Collection drafts) { if (idGenerator == null) { ctx.throwNoIdGenerator(); } + ImmutableProp prop = ctx.path.getProp(); + if (prop != null && ctx.options.isKeyOnlyAsReference(prop) && isKeyOnly(drafts)) { + return QueryReason.KEY_ONLY_AS_REFERENCE; + } if (!(idGenerator instanceof IdentityIdGenerator)) { return QueryReason.IDENTITY_GENERATOR_REQUIRED; } @@ -542,6 +546,50 @@ private void validateAloneIds() { } } + private boolean isKeyOnly(Collection drafts) { + Set keyProps = ctx.options.getKeyProps(ctx.path.getType()); + if (keyProps == null || keyProps.isEmpty()) { + return false; + } + for (DraftSpi draft : drafts) { + if (!isKeyOnly(draft, keyProps)) { + return false; + } + } + return true; + } + + private boolean isKeyOnly(DraftSpi draft, Set keyProps) { + boolean hasKey = false; + for (ImmutableProp prop : ctx.path.getType().getProps().values()) { + if (!prop.isColumnDefinition()) { + continue; + } + boolean isLoaded = draft.__isLoaded(prop.getId()); + if (prop.isReference(TargetLevel.PERSISTENT)) { + Object value = draft.__get(prop.getId()); + if (value != null) { + ImmutableSpi target = (ImmutableSpi) value; + if (!target.__isLoaded(target.__type().getIdProp().getId())) { + isLoaded = false; + } + } + } + if (keyProps.contains(prop)) { + if (isLoaded) { + hasKey = true; + } else { + return false; + } + } else { + if (isLoaded) { + return false; + } + } + } + return hasKey; + } + private static Object columnValue(DraftSpi draft, ImmutableProp prop) { PropId propId = prop.getId(); if (!draft.__isLoaded(propId)) { diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/QueryReason.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/QueryReason.java index ce25b4e2c7..a9a6837c9d 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/QueryReason.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/QueryReason.java @@ -299,6 +299,14 @@ public enum QueryReason { */ IDENTITY_GENERATOR_REQUIRED, + /** + * When the configuration `keyOnlyObjectAsReference` + * of the association is allowed, the key-only object + * will be ignored, the keys must be converted to ids + * by extra select statement. + */ + KEY_ONLY_AS_REFERENCE, + /** * Direct use of a delete statement, but upon discovering that the * object being deleted has child objects and the user requires Jimmer diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptions.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptions.java index 737e9ab393..726bb7a2e4 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptions.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptions.java @@ -38,6 +38,8 @@ public interface SaveOptions { boolean isAutoCheckingProp(ImmutableProp prop); + boolean isKeyOnlyAsReference(ImmutableProp prop); + @Nullable ExceptionTranslator getExceptionTranslator(); @@ -124,6 +126,11 @@ public boolean isAutoCheckingProp(ImmutableProp prop) { return raw.isAutoCheckingProp(prop); } + @Override + public boolean isKeyOnlyAsReference(ImmutableProp prop) { + return raw.isKeyOnlyAsReference(prop); + } + @Override @Nullable public ExceptionTranslator getExceptionTranslator() { diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Shape.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Shape.java index 102d8912fe..8cf0cb9e75 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Shape.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/Shape.java @@ -4,6 +4,7 @@ import org.babyfish.jimmer.runtime.ImmutableSpi; import org.babyfish.jimmer.sql.ast.impl.value.PropertyGetter; import org.babyfish.jimmer.sql.runtime.JSqlClientImplementor; +import org.jetbrains.annotations.NotNull; import java.util.*; import java.util.function.Predicate; @@ -42,10 +43,12 @@ public static Shape fullOf(JSqlClientImplementor sqlClient, Class type) { return new Shape(immutableType, PropertyGetter.entityGetters(sqlClient, immutableType, null, null)); } + @NotNull public ImmutableType getType() { return type; } + @NotNull public List getGetters() { return getters; } diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SimpleEntitySaveCommandImpl.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SimpleEntitySaveCommandImpl.java index 923329318d..ffb4b834bd 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SimpleEntitySaveCommandImpl.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/impl/mutation/SimpleEntitySaveCommandImpl.java @@ -94,6 +94,16 @@ public SimpleEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop return new SimpleEntitySaveCommandImpl<>(new IdOnlyAutoCheckingCfg(cfg, prop, checking)); } + @Override + public SimpleEntitySaveCommand setKeyOnlyAsReferenceAll() { + return new SimpleEntitySaveCommandImpl<>(new KeyOnlyAsReferenceCfg(cfg, true)); + } + + @Override + public SimpleEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop, boolean asReference) { + return new SimpleEntitySaveCommandImpl<>(new KeyOnlyAsReferenceCfg(cfg, prop,asReference)); + } + @Override public SimpleEntitySaveCommand setDissociateAction(ImmutableProp prop, DissociateAction dissociateAction) { return new SimpleEntitySaveCommandImpl<>(new DissociationActionCfg(cfg, prop, dissociateAction)); diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/AbstractEntitySaveCommand.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/AbstractEntitySaveCommand.java index 46d2e182b8..ec00baca36 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/AbstractEntitySaveCommand.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/AbstractEntitySaveCommand.java @@ -44,6 +44,21 @@ public interface AbstractEntitySaveCommand { @NewChain AbstractEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop, boolean checking); + @NewChain + AbstractEntitySaveCommand setKeyOnlyAsReferenceAll(); + + @NewChain + AbstractEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop); + + @NewChain + AbstractEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop, boolean asReference); + + @NewChain + AbstractEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop); + + @NewChain + AbstractEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop, boolean asReference); + @NewChain default AbstractEntitySaveCommand setDissociateAction( TypedProp.Reference prop, diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/BatchEntitySaveCommand.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/BatchEntitySaveCommand.java index 764425d739..3f1f7e6865 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/BatchEntitySaveCommand.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/BatchEntitySaveCommand.java @@ -73,6 +73,32 @@ default BatchEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop @Override BatchEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop, boolean checking); + @NewChain + @Override + BatchEntitySaveCommand setKeyOnlyAsReferenceAll(); + + @NewChain + @Override + default BatchEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop) { + return setKeyOnlyAsReference(prop.unwrap(), true); + } + + @NewChain + @Override + default BatchEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop, boolean asReference) { + return setKeyOnlyAsReference(prop.unwrap(), asReference); + } + + @NewChain + @Override + default BatchEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop) { + return setKeyOnlyAsReference(prop, true); + } + + @NewChain + @Override + BatchEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop, boolean asReference); + @NewChain @Override default BatchEntitySaveCommand setDissociateAction( diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/SimpleEntitySaveCommand.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/SimpleEntitySaveCommand.java index 2bb3c32f45..77563e4e8b 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/SimpleEntitySaveCommand.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/mutation/SimpleEntitySaveCommand.java @@ -9,8 +9,6 @@ import org.babyfish.jimmer.sql.ast.table.Table; import org.babyfish.jimmer.sql.runtime.ExceptionTranslator; -import java.util.Collection; - public interface SimpleEntitySaveCommand extends Executable>, AbstractEntitySaveCommand { @@ -73,6 +71,32 @@ default SimpleEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp pro @Override SimpleEntitySaveCommand setAutoIdOnlyTargetChecking(ImmutableProp prop, boolean checking); + @NewChain + @Override + SimpleEntitySaveCommand setKeyOnlyAsReferenceAll(); + + @NewChain + @Override + default SimpleEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop) { + return setKeyOnlyAsReference(prop.unwrap(), true); + } + + @NewChain + @Override + default SimpleEntitySaveCommand setKeyOnlyAsReference(TypedProp.Association prop, boolean asReference) { + return setKeyOnlyAsReference(prop.unwrap(), asReference); + } + + @NewChain + @Override + default SimpleEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop) { + return setKeyOnlyAsReference(prop, true); + } + + @NewChain + @Override + SimpleEntitySaveCommand setKeyOnlyAsReference(ImmutableProp prop, boolean asReference); + @NewChain @Override default SimpleEntitySaveCommand setDissociateAction( diff --git a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/table/Table.java b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/table/Table.java index 66c2c1ac21..da2aa60415 100644 --- a/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/table/Table.java +++ b/project/jimmer-sql/src/main/java/org/babyfish/jimmer/sql/ast/table/Table.java @@ -43,5 +43,43 @@ public interface Table extends TableDelegate, TableTypeProvider, Selection > Selection fetch(Class viewType); + /** + *

If you must convert table types using the "asTableEx()" + * function to find corresponding properties in IDE's + * intelligent suggestions, it likely indicates you are performing + * table join operations on collection-associated properties, for example:

+ * + *
{@code
+     * BookTable table = BookTable.$;
+     * sql
+     *     .create(table)
+     *     // Table join based on collection association `Book.authors`
+     *     .where(table.asTableEx().authors().firstName().eq("Alex"))
+     *     select(table)
+     *     .execute();
+     * }
+ * + *

This usage will lead to data duplication, and whether using + * SQL-level or application-level approaches, you'll need to handle + * data distinction yourself. More importantly, this duplication + * will invalidate the pagination mechanism.

+ * + *

In fact, this usage is not recommended, and the purpose of + * "asTableEx()" is to remind you that you're using a feature + * that might cause problems. In most cases, sub-queries, + * especially implicit sub-queries, are more recommended. For example:

+ * + *
{@code
+     * BookTable table = BookTable.$;
+     * sql.createQuery(table)
+     *     .where(
+     *         table.authors(author ->
+     *            author.firstName().eq("Alex")
+     *         )
+     *     )
+     *     .select(table)
+     *     .execute();
+     * }
+ */ TableEx asTableEx(); } diff --git a/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptionsImpl.java b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptionsImpl.java index 5ba42f04a2..c96fc7fcb6 100644 --- a/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptionsImpl.java +++ b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/ast/impl/mutation/SaveOptionsImpl.java @@ -91,6 +91,11 @@ public boolean isAutoCheckingProp(ImmutableProp prop) { return false; } + @Override + public boolean isKeyOnlyAsReference(ImmutableProp prop) { + return false; + } + @Override public @Nullable ExceptionTranslator getExceptionTranslator() { return null; diff --git a/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/mutation/ShortAssociationTest.java b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/mutation/ShortAssociationTest.java new file mode 100644 index 0000000000..bf0ef34cbb --- /dev/null +++ b/project/jimmer-sql/src/test/java/org/babyfish/jimmer/sql/mutation/ShortAssociationTest.java @@ -0,0 +1,92 @@ +package org.babyfish.jimmer.sql.mutation; + +import org.babyfish.jimmer.sql.ast.impl.mutation.QueryReason; +import org.babyfish.jimmer.sql.common.AbstractMutationTest; +import org.babyfish.jimmer.sql.common.Constants; +import org.babyfish.jimmer.sql.model.Book; +import org.babyfish.jimmer.sql.model.BookProps; +import org.babyfish.jimmer.sql.model.Immutables; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; + +public class ShortAssociationTest extends AbstractMutationTest { + + @Test + public void testIdOnly() { + Book book = Immutables.createBook(draft -> { + draft.setId(Constants.learningGraphQLId1); + draft.setAuthorIds(Arrays.asList(Constants.alexId, Constants.danId)); + }); + executeAndExpectResult( + getSqlClient().getEntities().saveCommand(book), + ctx -> { + ctx.statement(it -> { + it.sql( + "delete from BOOK_AUTHOR_MAPPING " + + "where BOOK_ID = ? and AUTHOR_ID not in (?, ?)" + ); + }); + ctx.statement(it -> { + it.sql( + "merge into BOOK_AUTHOR_MAPPING tb_1_ " + + "using(values(?, ?)) tb_2_(BOOK_ID, AUTHOR_ID) " + + "--->on tb_1_.BOOK_ID = tb_2_.BOOK_ID and tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID " + + "when not matched then " + + "--->insert(BOOK_ID, AUTHOR_ID) " + + "--->values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)" + ); + }); + ctx.entity(it -> {}); + } + ); + } + + @Test + public void testKeyOnly() { + Book book = Immutables.createBook(draft -> { + draft.setId(Constants.learningGraphQLId1); + draft.addIntoAuthors(author -> { + author.setFirstName("Alex"); + author.setLastName("Banks"); + }); + draft.addIntoAuthors(author -> { + author.setFirstName("Dan"); + author.setLastName("Vanderkam"); + }); + }); + executeAndExpectResult( + getSqlClient().getEntities() + .saveCommand(book) + .setKeyOnlyAsReferenceAll(), + // or `setKeyOnlyAsReference(BookProps.AUTHORS)`, + ctx -> { + ctx.statement(it -> { + it.queryReason(QueryReason.KEY_ONLY_AS_REFERENCE); + it.sql( + "select tb_1_.ID, tb_1_.FIRST_NAME, tb_1_.LAST_NAME " + + "from AUTHOR tb_1_ " + + "where (tb_1_.FIRST_NAME, tb_1_.LAST_NAME) in ((?, ?), (?, ?))" + ); + }); + ctx.statement(it -> { + it.sql( + "delete from BOOK_AUTHOR_MAPPING " + + "where BOOK_ID = ? and AUTHOR_ID not in (?, ?)" + ); + }); + ctx.statement(it -> { + it.sql( + "merge into BOOK_AUTHOR_MAPPING tb_1_ " + + "using(values(?, ?)) tb_2_(BOOK_ID, AUTHOR_ID) " + + "--->on tb_1_.BOOK_ID = tb_2_.BOOK_ID and tb_1_.AUTHOR_ID = tb_2_.AUTHOR_ID " + + "when not matched then " + + "--->insert(BOOK_ID, AUTHOR_ID) " + + "--->values(tb_2_.BOOK_ID, tb_2_.AUTHOR_ID)" + ); + }); + ctx.entity(it -> {}); + } + ); + } +}