[FEAT,API] review the manyToMany link table name to support better automatic models

- change link table name
  - nupport reverse add link, support table name with max size of 64 with hash reduce
This commit is contained in:
Edouard DUPIN 2025-04-11 01:41:43 +02:00
parent 401bd8318a
commit 464f844eed
5 changed files with 145 additions and 64 deletions

View File

@ -2,6 +2,8 @@ package org.atriasoft.archidata.dataAccess.addOnSQL;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.PreparedStatement; import java.sql.PreparedStatement;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.sql.SQLException; import java.sql.SQLException;
@ -26,6 +28,7 @@ import org.atriasoft.archidata.dataAccess.options.Condition;
import org.atriasoft.archidata.dataAccess.options.OptionSpecifyType; import org.atriasoft.archidata.dataAccess.options.OptionSpecifyType;
import org.atriasoft.archidata.dataAccess.options.OverrideTableName; import org.atriasoft.archidata.dataAccess.options.OverrideTableName;
import org.atriasoft.archidata.exception.DataAccessException; import org.atriasoft.archidata.exception.DataAccessException;
import org.atriasoft.archidata.exception.SystemException;
import org.atriasoft.archidata.tools.ConfigBaseVariable; import org.atriasoft.archidata.tools.ConfigBaseVariable;
import org.bson.types.ObjectId; import org.bson.types.ObjectId;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -86,20 +89,99 @@ public class AddOnManyToMany implements DataAccessAddOn {
return false; return false;
} }
public static String generateLinkTableNameField( public static String hashTo64Chars(final String input) {
try {
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
final byte[] hash = digest.digest(input.getBytes());
final StringBuilder hexString = new StringBuilder();
for (final byte b : hash) {
final String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString().substring(0, 64);
} catch (final NoSuchAlgorithmException e) {
throw new RuntimeException("Erreur lors du hachage de la chaîne", e);
}
}
public static String hashIfNeeded(final String input) {
if (input.length() > 64) {
// Keep only the 50 first chars
final String truncated = input.substring(0, Math.min(input.length(), 50));
final String fullHash = hashTo64Chars(input);
final String hashPart = fullHash.substring(0, 14);
return truncated + hashPart;
}
return input;
}
public record LinkTableWithMode(
String tableName,
boolean first) {}
public static LinkTableWithMode generateLinkTableNameField(
final String tableName, final String tableName,
final Field field, final Field field,
final QueryOptions options) throws Exception { final QueryOptions options) throws Exception {
final FieldName name = AnnotationTools.getFieldName(field, options); return generateLinkTableName(tableName, field);
return generateLinkTableName(tableName, name.inTable());
} }
public static String generateLinkTableName(final String tableName, final String name) { public static LinkTableWithMode generateLinkTableName(
String localName = name; final String tableAName,
if (name.endsWith("s")) { final String tableAFieldName,
localName = name.substring(0, name.length() - 1); final String tableBName,
final String tableBFieldName) {
if (tableAName.compareTo(tableBName) < 0) {
return new LinkTableWithMode(
hashIfNeeded(tableAName + "_" + tableAFieldName + "_link_" + tableBName + "_" + tableBFieldName),
true);
} }
return tableName + "_link_" + localName; return new LinkTableWithMode(
hashIfNeeded(tableBName + "_" + tableBFieldName + "_link_" + tableAName + "_" + tableAFieldName),
false);
}
public static LinkTableWithMode generateLinkTableName(
final String tableAName,
final String tableAFieldName,
final ManyToMany manyToMany) throws SystemException {
if (manyToMany == null) {
throw new SystemException("@ManyMany is a null pointer " + tableAName);
}
if (manyToMany.targetEntity() == null) {
throw new SystemException("@ManyMany target entity is a null pointer: " + tableAName);
}
if (manyToMany.mappedBy() == null || manyToMany.mappedBy().isEmpty()) {
throw new SystemException("@ManyMany mapped by is not defined: " + tableAName);
}
final String tableNameRemote = AnnotationTools.getTableName(manyToMany.targetEntity());
return generateLinkTableName(tableAName, tableAFieldName, tableNameRemote, manyToMany.mappedBy());
}
public static LinkTableWithMode generateLinkTableName(final String tableAName, final Field field)
throws SystemException {
if (field == null) {
// TODO: throw !!!!
}
final FieldName columnName = AnnotationTools.getFieldName(field, null);
final ManyToMany manyToMany = AnnotationTools.get(field, ManyToMany.class);
return generateLinkTableName(tableAName, columnName.inTable(), manyToMany);
}
public static LinkTableWithMode generateLinkTableName(final Class<?> clazz, final String fieldName)
throws SystemException {
if (clazz == null) {
throw new SystemException("@ManyMany class reference is a null pointer ");
}
if (fieldName == null || fieldName.isEmpty() || fieldName.isBlank()) {
throw new SystemException("@ManyMany field of class reference is not defined");
}
final String tableName = AnnotationTools.getTableName(clazz);
final Field requestedField = AnnotationTools.getFieldNamed(clazz, fieldName);
return generateLinkTableName(tableName, requestedField);
} }
public void generateConcatQuery( public void generateConcatQuery(
@ -112,18 +194,13 @@ public class AddOnManyToMany implements DataAccessAddOn {
@NotNull final CountInOut count, @NotNull final CountInOut count,
final QueryOptions options) throws Exception { final QueryOptions options) throws Exception {
final ManyToMany manyToMany = AnnotationTools.getManyToMany(field); final ManyToMany manyToMany = AnnotationTools.getManyToMany(field);
String linkTableName = generateLinkTableName(tableName, name); final LinkTableWithMode linkTable = generateLinkTableName(tableName, name, manyToMany);
if (manyToMany.mappedBy() != null && manyToMany.mappedBy().length() != 0) {
// TODO: get the remote table name .....
final String remoteTableName = AnnotationTools.getTableName(manyToMany.targetEntity());
linkTableName = generateLinkTableName(remoteTableName, manyToMany.mappedBy());
}
final Class<?> objectClass = (Class<?>) ((ParameterizedType) field.getGenericType()) final Class<?> objectClass = (Class<?>) ((ParameterizedType) field.getGenericType())
.getActualTypeArguments()[0]; .getActualTypeArguments()[0];
final String tmpVariable = "tmp_" + Integer.toString(count.value); final String tmpVariable = "tmp_" + Integer.toString(count.value);
querySelect.append(" (SELECT GROUP_CONCAT("); querySelect.append(" (SELECT GROUP_CONCAT(");
querySelect.append(tmpVariable); querySelect.append(tmpVariable);
if (manyToMany.mappedBy() == null || manyToMany.mappedBy().length() == 0) { if (linkTable.first()) {
querySelect.append(".object2Id "); querySelect.append(".object2Id ");
} else { } else {
querySelect.append(".object1Id "); querySelect.append(".object1Id ");
@ -147,7 +224,7 @@ public class AddOnManyToMany implements DataAccessAddOn {
} }
} }
querySelect.append("') FROM "); querySelect.append("') FROM ");
querySelect.append(linkTableName); querySelect.append(linkTable.tableName());
querySelect.append(" "); querySelect.append(" ");
querySelect.append(tmpVariable); querySelect.append(tmpVariable);
querySelect.append(" WHERE "); querySelect.append(" WHERE ");
@ -160,7 +237,7 @@ public class AddOnManyToMany implements DataAccessAddOn {
querySelect.append(" = "); querySelect.append(" = ");
querySelect.append(tmpVariable); querySelect.append(tmpVariable);
querySelect.append("."); querySelect.append(".");
if (manyToMany.mappedBy() == null || manyToMany.mappedBy().length() == 0) { if (linkTable.first()) {
querySelect.append("object1Id "); querySelect.append("object1Id ");
} else { } else {
querySelect.append("object2Id "); querySelect.append("object2Id ");
@ -168,7 +245,7 @@ public class AddOnManyToMany implements DataAccessAddOn {
if (!"sqlite".equals(ConfigBaseVariable.getDBType())) { if (!"sqlite".equals(ConfigBaseVariable.getDBType())) {
querySelect.append(" GROUP BY "); querySelect.append(" GROUP BY ");
querySelect.append(tmpVariable); querySelect.append(tmpVariable);
if (manyToMany.mappedBy() == null || manyToMany.mappedBy().length() == 0) { if (linkTable.first()) {
querySelect.append(".object1Id"); querySelect.append(".object1Id");
} else { } else {
querySelect.append(".object2Id"); querySelect.append(".object2Id");
@ -345,14 +422,14 @@ public class AddOnManyToMany implements DataAccessAddOn {
"Can not ManyToMany with other than List<Long> or List<UUID> or List<ObjectId> Model: List<" "Can not ManyToMany with other than List<Long> or List<UUID> or List<ObjectId> Model: List<"
+ objectClass.getCanonicalName() + ">"); + objectClass.getCanonicalName() + ">");
} }
final FieldName columnName = AnnotationTools.getFieldName(field, options); final LinkTableWithMode linkTable = generateLinkTableName(tableName, field);
final String linkTableName = generateLinkTableName(tableName, columnName.inTable()); final String obj1 = linkTable.first ? "object1Id" : "object2Id";
final String obj2 = linkTable.first ? "object2Id" : "object1Id";
actions.add(() -> { actions.add(() -> {
ioDb.deleteWhere(LinkTableGeneric.class, new OverrideTableName(linkTableName), ioDb.deleteWhere(LinkTableGeneric.class, new OverrideTableName(linkTable.tableName()),
new Condition(new QueryCondition("object1Id", "=", localKey)), new Condition(new QueryCondition(obj1, "=", localKey)),
new OptionSpecifyType("object1Id", localKey.getClass()), new OptionSpecifyType(obj1, localKey.getClass()), new OptionSpecifyType(obj2, objectClass));
new OptionSpecifyType("object2Id", objectClass));
}); });
asyncInsert(ioDb, tableName, localKey, field, data, actions, options); asyncInsert(ioDb, tableName, localKey, field, data, actions, options);
} }
@ -385,13 +462,14 @@ public class AddOnManyToMany implements DataAccessAddOn {
"Can not ManyToMany with other than List<Long> or List<UUID> or List<ObjectId> Model: List<" "Can not ManyToMany with other than List<Long> or List<UUID> or List<ObjectId> Model: List<"
+ objectClass.getCanonicalName() + ">"); + objectClass.getCanonicalName() + ">");
} }
final FieldName columnName = AnnotationTools.getFieldName(field, options); final LinkTableWithMode linkTable = generateLinkTableName(tableName, field);
final String linkTableName = generateLinkTableName(tableName, columnName.inTable());
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
final List<Object> dataCasted = (List<Object>) data; final List<Object> dataCasted = (List<Object>) data;
if (dataCasted.size() == 0) { if (dataCasted.size() == 0) {
return; return;
} }
final String obj1 = linkTable.first ? "object1Id" : "object2Id";
final String obj2 = linkTable.first ? "object2Id" : "object1Id";
final List<LinkTableGeneric> insertElements = new ArrayList<>(); final List<LinkTableGeneric> insertElements = new ArrayList<>();
for (final Object remoteKey : dataCasted) { for (final Object remoteKey : dataCasted) {
if (remoteKey == null) { if (remoteKey == null) {
@ -404,26 +482,23 @@ public class AddOnManyToMany implements DataAccessAddOn {
return; return;
} }
actions.add(() -> { actions.add(() -> {
ioDb.insertMultiple(insertElements, new OverrideTableName(linkTableName), ioDb.insertMultiple(insertElements, new OverrideTableName(linkTable.tableName()),
new OptionSpecifyType("object1Id", localKey.getClass()), new OptionSpecifyType(obj1, localKey.getClass()), new OptionSpecifyType(obj2, objectClass));
new OptionSpecifyType("object2Id", objectClass));
}); });
} }
@Override @Override
public void drop(final DBAccessSQL ioDb, final String tableName, final Field field, final QueryOptions options) public void drop(final DBAccessSQL ioDb, final String tableName, final Field field, final QueryOptions options)
throws Exception { throws Exception {
final FieldName columnName = AnnotationTools.getFieldName(field, options); final LinkTableWithMode linkTable = generateLinkTableName(tableName, field);
final String linkTableName = generateLinkTableName(tableName, columnName.inTable()); ioDb.drop(LinkTableGeneric.class, new OverrideTableName(linkTable.tableName()));
ioDb.drop(LinkTableGeneric.class, new OverrideTableName(linkTableName));
} }
@Override @Override
public void cleanAll(final DBAccessSQL ioDb, final String tableName, final Field field, final QueryOptions options) public void cleanAll(final DBAccessSQL ioDb, final String tableName, final Field field, final QueryOptions options)
throws Exception { throws Exception {
final FieldName columnName = AnnotationTools.getFieldName(field, options); final LinkTableWithMode linkTable = generateLinkTableName(tableName, field);
final String linkTableName = generateLinkTableName(tableName, columnName.inTable()); ioDb.cleanAll(LinkTableGeneric.class, new OverrideTableName(linkTable.tableName()));
ioDb.cleanAll(LinkTableGeneric.class, new OverrideTableName(linkTableName));
} }
public static void addLink( public static void addLink(
@ -433,12 +508,14 @@ public class AddOnManyToMany implements DataAccessAddOn {
final String column, final String column,
final Object remoteKey) throws Exception { final Object remoteKey) throws Exception {
if (ioDb instanceof final DBAccessSQL daSQL) { if (ioDb instanceof final DBAccessSQL daSQL) {
final String tableName = AnnotationTools.getTableName(clazz); final LinkTableWithMode linkTable = generateLinkTableName(clazz, column);
final String linkTableName = generateLinkTableName(tableName, column); final LinkTableGeneric insertElement = linkTable.first ? new LinkTableGeneric(localKey, remoteKey)
final LinkTableGeneric insertElement = new LinkTableGeneric(localKey, remoteKey); : new LinkTableGeneric(remoteKey, localKey);
daSQL.insert(insertElement, new OverrideTableName(linkTableName), final String obj1 = linkTable.first ? "object1Id" : "object2Id";
new OptionSpecifyType("object1Id", localKey.getClass()), final String obj2 = linkTable.first ? "object2Id" : "object1Id";
new OptionSpecifyType("object2Id", remoteKey.getClass())); daSQL.insert(insertElement, new OverrideTableName(linkTable.tableName()),
new OptionSpecifyType(obj1, localKey.getClass()),
new OptionSpecifyType(obj2, remoteKey.getClass()));
} else if (ioDb instanceof final DBAccessMorphia dam) { } else if (ioDb instanceof final DBAccessMorphia dam) {
} else { } else {
@ -454,13 +531,14 @@ public class AddOnManyToMany implements DataAccessAddOn {
final String column, final String column,
final Object remoteKey) throws Exception { final Object remoteKey) throws Exception {
if (ioDb instanceof final DBAccessSQL daSQL) { if (ioDb instanceof final DBAccessSQL daSQL) {
final String tableName = AnnotationTools.getTableName(clazz); final LinkTableWithMode linkTable = generateLinkTableName(clazz, column);
final String linkTableName = generateLinkTableName(tableName, column); final String obj1 = linkTable.first ? "object1Id" : "object2Id";
return daSQL.deleteWhere(LinkTableGeneric.class, new OverrideTableName(linkTableName), final String obj2 = linkTable.first ? "object2Id" : "object1Id";
new Condition(new QueryAnd(new QueryCondition("object1Id", "=", localKey), return daSQL.deleteWhere(LinkTableGeneric.class, new OverrideTableName(linkTable.tableName()),
new QueryCondition("object2Id", "=", remoteKey))), new Condition(new QueryAnd(new QueryCondition(obj1, "=", localKey),
new OptionSpecifyType("object1Id", localKey.getClass()), new QueryCondition(obj2, "=", remoteKey))),
new OptionSpecifyType("object2Id", remoteKey.getClass())); new OptionSpecifyType(obj1, localKey.getClass()),
new OptionSpecifyType(obj2, remoteKey.getClass()));
} else if (ioDb instanceof final DBAccessMorphia dam) { } else if (ioDb instanceof final DBAccessMorphia dam) {
return 0L; return 0L;
} else { } else {
@ -481,18 +559,21 @@ public class AddOnManyToMany implements DataAccessAddOn {
final int fieldId, final int fieldId,
final QueryOptions options) throws Exception { final QueryOptions options) throws Exception {
final ManyToMany manyToMany = AnnotationTools.getManyToMany(field); final ManyToMany manyToMany = AnnotationTools.getManyToMany(field);
if (manyToMany.mappedBy() != null && manyToMany.mappedBy().length() != 0) { if (manyToMany.mappedBy() == null || manyToMany.mappedBy().length() == 0) {
// not the reference model to create base: throw new SystemException("MappedBy must be set in ManyMany: " + tableName + " " + field.getName());
return;
} }
final String linkTableName = generateLinkTableNameField(tableName, field, options); final LinkTableWithMode linkTable = generateLinkTableNameField(tableName, field, options);
final QueryOptions options2 = new QueryOptions(new OverrideTableName(linkTableName)); if (linkTable.first()) {
final QueryOptions options2 = new QueryOptions(new OverrideTableName(linkTable.tableName()));
final Class<?> objectClass = (Class<?>) ((ParameterizedType) field.getGenericType()) final Class<?> objectClass = (Class<?>) ((ParameterizedType) field.getGenericType())
.getActualTypeArguments()[0]; .getActualTypeArguments()[0];
final Class<?> primaryType = primaryField.getType(); final Class<?> primaryType = primaryField.getType();
options2.add(new OptionSpecifyType("object1Id", primaryType)); final String obj1 = linkTable.first ? "object1Id" : "object2Id";
options2.add(new OptionSpecifyType("object2Id", objectClass)); final String obj2 = linkTable.first ? "object2Id" : "object1Id";
options2.add(new OptionSpecifyType(obj1, primaryType));
options2.add(new OptionSpecifyType(obj2, objectClass));
final List<String> sqlCommand = DataFactory.createTable(LinkTableGeneric.class, options2); final List<String> sqlCommand = DataFactory.createTable(LinkTableGeneric.class, options2);
postActionList.addAll(sqlCommand); postActionList.addAll(sqlCommand);
} }
}
} }

View File

@ -13,6 +13,6 @@ public class TypeManyToManyLongRoot extends GenericData {
public String otherData; public String otherData;
@ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyLongRemote.class) @ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyLongRemote.class, mappedBy = "remoteToParent")
public List<Long> remote; public List<Long> remote;
} }

View File

@ -16,6 +16,6 @@ public class TypeManyToManyLongRootExpand extends GenericData {
public String otherData; public String otherData;
@ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyLongRemote.class) @ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyLongRemote.class, mappedBy = "remoteToParent")
public List<TypeManyToManyLongRemote> remote; public List<TypeManyToManyLongRemote> remote;
} }

View File

@ -14,6 +14,6 @@ public class TypeManyToManyOIDRoot extends OIDGenericData {
public String otherData; public String otherData;
@ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyOIDRemote.class) @ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyOIDRemote.class, mappedBy = "remoteToParent")
public List<ObjectId> remote; public List<ObjectId> remote;
} }

View File

@ -16,6 +16,6 @@ public class TypeManyToManyOIDRootExpand extends OIDGenericData {
public String otherData; public String otherData;
@ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyOIDRemote.class) @ManyToMany(fetch = FetchType.LAZY, targetEntity = TypeManyToManyOIDRemote.class, mappedBy = "remoteToParent")
public List<TypeManyToManyOIDRemote> remote; public List<TypeManyToManyOIDRemote> remote;
} }