[jOOQ/jOOQ#14985] Support many-to-many path joins

This commit is contained in:
Lukas Eder 2023-04-28 17:40:40 +02:00
parent d5577c20db
commit caa3dca7db
10 changed files with 289 additions and 13 deletions

View File

@ -80,6 +80,7 @@ import org.jooq.meta.ForeignKeyDefinition;
import org.jooq.meta.IdentityDefinition;
import org.jooq.meta.IndexDefinition;
import org.jooq.meta.InverseForeignKeyDefinition;
import org.jooq.meta.ManyToManyKeyDefinition;
import org.jooq.meta.PackageDefinition;
import org.jooq.meta.RoutineDefinition;
import org.jooq.meta.SchemaDefinition;
@ -292,13 +293,21 @@ public class DefaultGeneratorStrategy extends AbstractGeneratorStrategy {
if (fk.getTable().getForeignKeys(referenced).size() == 1)
return getJavaMethodName(referenced, mode);
}
if (definition instanceof InverseForeignKeyDefinition fk) {
else if (definition instanceof InverseForeignKeyDefinition fk) {
TableDefinition referencing = fk.getReferencingTable();
if (fk.getTable().getInverseForeignKeys(referencing).size() == 1)
return getJavaMethodName(referencing, mode);
}
else if (definition instanceof ManyToManyKeyDefinition k) {
TableDefinition t1 = k.getForeignKey1().getReferencedTable();
TableDefinition t2 = k.getForeignKey2().getReferencedTable();
if (t1.getManyToManyKeys(t2).size() == 1)
return getJavaMethodName(t2, mode);
else
return getJavaClassName0LC(k.getForeignKey2(), Mode.DEFAULT);
}
}
return getJavaClassName0LC(definition, Mode.DEFAULT);

View File

@ -180,6 +180,7 @@ import org.jooq.meta.IndexColumnDefinition;
import org.jooq.meta.IndexDefinition;
import org.jooq.meta.InverseForeignKeyDefinition;
import org.jooq.meta.JavaTypeResolver;
import org.jooq.meta.ManyToManyKeyDefinition;
import org.jooq.meta.PackageDefinition;
import org.jooq.meta.ParameterDefinition;
import org.jooq.meta.RoutineDefinition;
@ -6711,18 +6712,17 @@ public class JavaGenerator extends AbstractGenerator {
}
}
List<InverseForeignKeyDefinition> inboundFKs = table.getInverseForeignKeys();
if (inboundFKs.size() > 0 && generateGlobalKeyReferences()) {
if (generateImplicitJoinPathsToMany() && generateGlobalKeyReferences()) {
List<InverseForeignKeyDefinition> inboundFKs = table.getInverseForeignKeys();
// Inbound (to-many) implicit join paths
if (generateImplicitJoinPathsToMany()) {
if (inboundFKs.size() > 0) {
Map<TableDefinition, Long> pathCounts = inboundFKs.stream().collect(groupingBy(InverseForeignKeyDefinition::getReferencingTable, counting()));
inboundFKLoop:
for (InverseForeignKeyDefinition foreignKey : inboundFKs) {
final String keyMethodName = out.ref(getStrategy().getJavaMethodName(foreignKey));
if (outboundKeyMethodNames.contains(keyMethodName)) {
if (!outboundKeyMethodNames.add(keyMethodName)) {
log.warn("Ambiguous key name",
"The database object " + foreignKey.getQualifiedOutputName()
+ " generates an inbound key method name " + keyMethodName
@ -6755,7 +6755,6 @@ public class JavaGenerator extends AbstractGenerator {
out.javadoc(
"Get the implicit to-many join path to the <code>" + foreignKey.getReferencingTable().getQualifiedName() + "</code> table"
+ (pathCounts.get(foreignKey.getReferencingTable()) > 1 ? ", via the <code>" + foreignKey.getInputName() + "</code> key" : "")
+ ".<p><strong>EXPERIMENTAL! DO NOT USE THIS FEATURE YET.</strong>"
);
if (scala) {
@ -6784,6 +6783,55 @@ public class JavaGenerator extends AbstractGenerator {
out.println("}");
}
}
List<ManyToManyKeyDefinition> manyToManyKeys = table.getManyToManyKeys();
Map<TableDefinition, Long> pathCountsManytoMany = manyToManyKeys.stream().collect(groupingBy(d -> d.getForeignKey2().getReferencedTable(), counting()));
manyToManyKeyLoop:
for (ManyToManyKeyDefinition manyToManyKey : manyToManyKeys) {
final String keyMethodName = out.ref(getStrategy().getJavaMethodName(manyToManyKey));
if (!outboundKeyMethodNames.add(keyMethodName)) {
log.warn("Ambiguous key name",
"The database object " + manyToManyKey.getQualifiedOutputName()
+ " generates an inbound key method name " + keyMethodName
+ " which conflicts with the previously generated outbound key method name."
+ " Use a custom generator strategy to disambiguate the types. More information here:\n"
+ " - https://www.jooq.org/doc/latest/manual/code-generation/codegen-generatorstrategy/\n"
+ " - https://www.jooq.org/doc/latest/manual/code-generation/codegen-matcherstrategy/"
);
continue manyToManyKeyLoop;
}
final String key1MethodName = out.ref(getStrategy().getJavaMethodName(manyToManyKey.getForeignKey1().getInverse()));
final String key2MethodName = out.ref(getStrategy().getJavaMethodName(manyToManyKey.getForeignKey2()));
final TableDefinition referencedTable = manyToManyKey.getForeignKey2().getReferencedTable();
final String referencedTableClassName = out.ref(getStrategy().getFullJavaClassName(referencedTable));
out.javadoc(
"Get the implicit many-to-many join path to the <code>" + referencedTable.getQualifiedName() + "</code> table"
+ (pathCountsManytoMany.get(referencedTable) > 1 ? ", via the <code>" + manyToManyKey.getInputName() + "</code> key" : "")
);
if (scala) {
out.println("%sdef %s: %s = %s().%s()", visibility(), scalaWhitespaceSuffix(keyMethodName), referencedTableClassName, key1MethodName, key2MethodName);
}
else if (kotlin) {
if (generateImplicitJoinPathsAsKotlinProperties()) {
out.println("%sval %s: %s", visibility(), keyMethodName, referencedTableClassName);
out.tab(1).println("get(): %s = %s().%s()", referencedTableClassName, key1MethodName, key2MethodName);
}
else {
out.println();
out.println("%sfun %s(): %s = %s().%s()", visibility(), keyMethodName, referencedTableClassName, key1MethodName, key2MethodName);
}
}
else {
out.println("%s%s %s() {", visibility(), referencedTableClassName, keyMethodName);
out.println("return %s().%s();", key1MethodName, key2MethodName);
out.println("}");
}
}
}
}
}

View File

@ -43,7 +43,9 @@ import static org.jooq.impl.DSL.table;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.jooq.Record;
import org.jooq.Table;
@ -166,7 +168,7 @@ implements
for (UniqueKeyDefinition uk : getKeys())
for (ForeignKeyDefinition fk : uk.getForeignKeys())
result.add(fk.inverse());
result.add(fk.getInverse());
return result;
}
@ -182,6 +184,52 @@ implements
return result;
}
@Override
public final List<ManyToManyKeyDefinition> getManyToManyKeys() {
List<ManyToManyKeyDefinition> result = new ArrayList<>();
for (InverseForeignKeyDefinition k : getInverseForeignKeys()) {
ukLoop:
for (UniqueKeyDefinition uk : k.getReferencingTable().getKeys()) {
if (!uk.getKeyColumns().containsAll(k.getReferencingColumns()))
continue ukLoop;
fkLoop:
for (ForeignKeyDefinition fk : k.getReferencingTable().getForeignKeys()) {
if (!k.getForeignKey().equals(fk)) {
Set<ColumnDefinition> columns = new HashSet<>();
columns.addAll(k.getForeignKey().getKeyColumns());
// [#13639] The two FKs must not overlap
for (ColumnDefinition c : fk.getKeyColumns())
if (!columns.add(c))
continue fkLoop;
// [#13639] Require UK = FK1 + FK2
if (!columns.equals(new HashSet<>(uk.getKeyColumns())))
continue fkLoop;
result.add(new DefaultManyToManyKeyDefinition(k.getForeignKey(), uk, fk));
}
}
}
}
return result;
}
@Override
public final List<ManyToManyKeyDefinition> getManyToManyKeys(TableDefinition referencing) {
List<ManyToManyKeyDefinition> result = new ArrayList<>();
for (ManyToManyKeyDefinition key : getManyToManyKeys())
if (referencing.equals(key.getForeignKey2().getReferencedTable()))
result.add(key);
return result;
}
@Override
public final List<CheckConstraintDefinition> getCheckConstraints() {
return getDatabase().getRelations().getCheckConstraints(this);

View File

@ -102,7 +102,7 @@ public class DefaultForeignKeyDefinition extends AbstractConstraintDefinition im
}
@Override
public InverseForeignKeyDefinition inverse() {
public InverseForeignKeyDefinition getInverse() {
return new DefaultInverseForeignKeyDefinition(this);
}
}

View File

@ -80,7 +80,7 @@ public class DefaultInverseForeignKeyDefinition extends AbstractDefinition imple
}
@Override
public ForeignKeyDefinition foreignKey() {
public ForeignKeyDefinition getForeignKey() {
return foreignKey;
}
}

View File

@ -0,0 +1,82 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.meta;
import java.util.List;
/**
* @author Lukas Eder
*/
public class DefaultManyToManyKeyDefinition extends AbstractConstraintDefinition implements ManyToManyKeyDefinition {
private final ForeignKeyDefinition foreignKey1;
private final UniqueKeyDefinition uniqueKey;
private final ForeignKeyDefinition foreignKey2;
public DefaultManyToManyKeyDefinition(
ForeignKeyDefinition foreignKey1,
UniqueKeyDefinition uniqueKey,
ForeignKeyDefinition foreignKey2
) {
super(uniqueKey.getSchema(), uniqueKey.getTable(), uniqueKey.getName(), uniqueKey.enforced());
this.foreignKey1 = foreignKey1;
this.uniqueKey = uniqueKey;
this.foreignKey2 = foreignKey2;
}
@Override
public UniqueKeyDefinition getUniqueKey() {
return uniqueKey;
}
@Override
public List<ColumnDefinition> getKeyColumns() {
return uniqueKey.getKeyColumns();
}
@Override
public ForeignKeyDefinition getForeignKey1() {
return foreignKey1;
}
@Override
public ForeignKeyDefinition getForeignKey2() {
return foreignKey2;
}
}

View File

@ -92,5 +92,5 @@ public interface ForeignKeyDefinition extends ConstraintDefinition {
/**
* Get the inverse key.
*/
InverseForeignKeyDefinition inverse();
InverseForeignKeyDefinition getInverse();
}

View File

@ -41,6 +41,9 @@ import java.util.List;
/**
* An object holding information about an inverse foreign key relationship.
* <p>
* This constraint represents the {@link UniqueKeyDefinition} that enforces the
* uniqueness at the referenced end of the {@link ForeignKeyDefinition}.
*
* @author Lukas Eder
*/
@ -64,5 +67,5 @@ public interface InverseForeignKeyDefinition extends ConstraintDefinition {
/**
* Get the foreign key this is an inverse of.
*/
ForeignKeyDefinition foreignKey();
ForeignKeyDefinition getForeignKey();
}

View File

@ -0,0 +1,75 @@
/*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Other licenses:
* -----------------------------------------------------------------------------
* Commercial licenses for this work are available. These replace the above
* ASL 2.0 and offer limited warranties, support, maintenance, and commercial
* database integrations.
*
* For more information, please visit: https://www.jooq.org/legal/licensing
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package org.jooq.meta;
import java.util.List;
/**
* An object holding information about an many to many foreign key relationship.
* <p>
* This constraint represents the {@link UniqueKeyDefinition} that enforces the
* relationship in the relationship table. The {@link UniqueKeyDefinition} is a
* composite key, whose columns are completely contained in the two outbound
* {@link ForeignKeyDefinition} specified by {@link #getForeignKey1()} and
* {@link #getForeignKey2()}. The order of {@link ForeignKeyDefinition} depends
* on the navigation direction.
*
* @author Lukas Eder
*/
public interface ManyToManyKeyDefinition extends ConstraintDefinition {
/**
* The list of columns making up the unique key.
*/
List<ColumnDefinition> getKeyColumns();
/**
* The unique key that enforces the relationship.
*/
UniqueKeyDefinition getUniqueKey();
/**
* Get the first foreign key in the relationship.
*/
ForeignKeyDefinition getForeignKey1();
/**
* Get the second foreign key in the relationship.
*/
ForeignKeyDefinition getForeignKey2();
}

View File

@ -138,6 +138,17 @@ public interface TableDefinition extends Definition {
*/
List<InverseForeignKeyDefinition> getInverseForeignKeys(TableDefinition referencing);
/**
* Get the many to many keys for this table.
*/
List<ManyToManyKeyDefinition> getManyToManyKeys();
/**
* Get the many to many keys for this table referencing another, specific
* table.
*/
List<ManyToManyKeyDefinition> getManyToManyKeys(TableDefinition referencing);
/**
* Get the <code>CHECK</code> constraints for this table.
*/