You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cayenne.apache.org by aa...@apache.org on 2026/07/20 16:39:09 UTC
(cayenne) 03/05: CAY-2980 Improve model name generation
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/cayenne.git
commit e33e741ec3291337ddb686f4c8bfa562d0c9c587
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Mon Jul 20 15:52:21 2026 +0200
CAY-2980 Improve model name generation
auto-renaming properties conflicting with Persistent object
---
RELEASE-NOTES.txt | 1 +
ai-plugin/references/model-naming-conventions.md | 14 +++++++----
ai-plugin/skills/cayenne-model-naming/SKILL.md | 11 +++++----
.../dbsync/naming/BaseObjectNameGenerator.java | 23 ++++++++++++++++--
.../naming/DefaultObjectNameGeneratorTest.java | 28 ++++++++++++++++++++++
.../project/validation/NameValidationHelper.java | 25 ++++++++++---------
6 files changed, 77 insertions(+), 25 deletions(-)
diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt
index 1dcfd71c4..dfdb33ca7 100644
--- a/RELEASE-NOTES.txt
+++ b/RELEASE-NOTES.txt
@@ -27,6 +27,7 @@ CAY-2974 CayenneSqlException with a reference to translated query
CAY-2975 Mnemonic table aliases in generated SQL
CAY-2978 AI skill: "cayenne-model-naming"
CAY-2979 AI skill: "cayenne-full-db-sync"
+CAY-2980 Improve model name generation
Bug Fixes:
diff --git a/ai-plugin/references/model-naming-conventions.md b/ai-plugin/references/model-naming-conventions.md
index 32aec7471..c65b83005 100644
--- a/ai-plugin/references/model-naming-conventions.md
+++ b/ai-plugin/references/model-naming-conventions.md
@@ -143,11 +143,15 @@ The three cases above don't exhaust the ways a purely mechanical transliteration
you spot a name where a human reading the underlying DB name would obviously do better, and the fix
is defensible (not a guess), apply the same conservative treatment. Some more examples:
-- **Genuinely illegal identifiers** the generator passed through — a name starting with a digit, or
- literally `class`, whose getter `getClass()` collides with the final `Object.getClass()`. Java
- *keywords* are **not** a problem — `default`, `package`, `return` and the like compile fine, since
- class generation prefixes the field/parameter name with `_` and embeds the capitalized name in the
- accessors (`getDefault()` / `setDefault()`). Leave keyword-named properties alone.
+- **Genuinely illegal identifiers** the generator passed through — chiefly a name starting with a
+ digit. Java *keywords* are **not** a problem — `default`, `package`, `return` and the like compile
+ fine, since class generation prefixes the field/parameter name with `_` and embeds the capitalized
+ name in the accessors (`getDefault()` / `setDefault()`); leave keyword-named properties alone.
+ Property names clashing with base-class getters (`class`, `objectId`, `objectContext`,
+ `persistenceState`, `snapshotVersion` — e.g. `getClass()` collides with the final
+ `Object.getClass()`) are qualified with the entity name by the generator itself (`class` on
+ `student` → `studentClass`) — already handled; models imported by older Cayenne versions may still
+ carry them raw, and those do need the same entity-qualified rename.
- **Lost acronym casing** — `HTTPURL` → `Gametype`-style collapse loses the acronym; `httpUrl` /
`url` may read better than `httpurl`.
- **Plural table → singular entity** — a `CUSTOMERS` table yields `Customers`; an entity is a single
diff --git a/ai-plugin/skills/cayenne-model-naming/SKILL.md b/ai-plugin/skills/cayenne-model-naming/SKILL.md
index 44b1af552..fb26687d9 100644
--- a/ai-plugin/skills/cayenne-model-naming/SKILL.md
+++ b/ai-plugin/skills/cayenne-model-naming/SKILL.md
@@ -95,11 +95,12 @@ correct — leave them.** Flag only the cases the deterministic generator can't
names (`customer`, `orders`); a relationship is a role/property, and the prefix is noise there.
**Leave the entity names alone** — the prefix on classes is the user's choice, and renaming
entities regenerates classes.
-4. **Other clear, defensible improvements** — illegal identifiers (digit-leading names, or `class`,
- whose getter would clash with `Object.getClass()` — Java keywords are fine, cgen escapes them),
- lost acronym casing, obvious cryptic abbreviations applied consistently,
- plural-table-to-singular-entity. Conservative by default; when unsure, leave the baseline name
- and ask.
+4. **Other clear, defensible improvements** — illegal identifiers (digit-leading names; Java
+ keywords are fine — cgen escapes them — and properties clashing with base-class getters like
+ `class` / `objectId` are auto-qualified with the entity name by the generator, though older
+ imports may carry them raw), lost acronym casing, obvious cryptic abbreviations applied
+ consistently, plural-table-to-singular-entity. Conservative by default; when unsure, leave the
+ baseline name and ask.
Relationship cleanup is anchored on the **DbRelationship** — it's the first-class citizen, since
every FK has one whether or not an ObjRelationship was generated on top. The rules above (run-together,
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
index 26d230ade..b0af9b546 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/naming/BaseObjectNameGenerator.java
@@ -22,6 +22,7 @@ import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
import org.apache.cayenne.map.DbRelationship;
+import org.apache.cayenne.project.validation.NameValidationHelper;
import org.apache.cayenne.util.Util;
import java.util.List;
@@ -43,7 +44,9 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator {
@Override
public String objAttributeName(DbAttribute dbAttribute) {
- return Util.underscoredToJava(dbAttribute.getName(), false);
+ String name = Util.underscoredToJava(dbAttribute.getName(), false);
+ String entityName = dbAttribute.getEntity() != null ? dbAttribute.getEntity().getName() : null;
+ return fixPersistentBaseProperty(name, entityName);
}
@Override
@@ -72,7 +75,23 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator {
String baseName = toMany
? toManyBase(joins, targetEntityName)
: toOneBase(joins, targetEntityName);
- return Util.underscoredToJava(baseName, false);
+ String name = Util.underscoredToJava(baseName, false);
+
+ DbEntity sourceEntity = joins.getFirst().getRelationship().getSourceEntity();
+ return fixPersistentBaseProperty(name, sourceEntity != null ? sourceEntity.getName() : null);
+ }
+
+ /**
+ * Qualifies a property name that has a getter in Object or PersistentObject with the entity name, the way
+ * a human would ("class" on "student" becomes "studentClass") — such a name can't be generated as-is.
+ */
+ private String fixPersistentBaseProperty(String name, String entityName) {
+
+ if (entityName == null || !NameValidationHelper.getInstance().invalidPersistentObjectProperty(name)) {
+ return name;
+ }
+
+ return Util.underscoredToJava(dbEntityBaseName(entityName) + "_" + name, false);
}
protected boolean isToMany(DbRelationship... relationshipChain) {
diff --git a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
index 214573df7..35a24568d 100644
--- a/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
+++ b/cayenne-dbsync/src/test/java/org/apache/cayenne/dbsync/naming/DefaultObjectNameGeneratorTest.java
@@ -18,6 +18,8 @@
****************************************************************/
package org.apache.cayenne.dbsync.naming;
+import java.sql.Types;
+
import org.apache.cayenne.map.DbAttribute;
import org.apache.cayenne.map.DbEntity;
import org.apache.cayenne.map.DbJoin;
@@ -260,4 +262,30 @@ public class DefaultObjectNameGeneratorTest {
assertEquals("name", generator.objAttributeName(new DbAttribute("NAME")));
assertEquals("artistName", generator.objAttributeName(new DbAttribute("ARTIST_NAME")));
}
+
+ @Test
+ public void objAttributeName_PersistentBaseProperty() {
+
+ // names with a getter in Object or PersistentObject are qualified with the entity name
+ DbEntity student = new DbEntity("STUDENT");
+ assertEquals("studentClass", generator.objAttributeName(new DbAttribute("CLASS", Types.VARCHAR, student)));
+ assertEquals("studentObjectId",
+ generator.objAttributeName(new DbAttribute("OBJECT_ID", Types.INTEGER, student)));
+ assertEquals("studentSnapshotVersion",
+ generator.objAttributeName(new DbAttribute("SNAPSHOT_VERSION", Types.INTEGER, student)));
+
+ // ordinary names are unaffected
+ assertEquals("classRoom", generator.objAttributeName(new DbAttribute("CLASS_ROOM", Types.VARCHAR, student)));
+ }
+
+ @Test
+ public void dbRelationshipName_ToOne_PersistentBaseProperty() {
+
+ // an FK-derived name with a getter in Object or PersistentObject is qualified with the source entity name
+ DbRelationship r1 = makeRelationship("student", "class_id", "class", "id", false);
+ assertEquals("studentClass", dbRelationshipName(r1));
+
+ DbRelationship r2 = makeRelationship("AUDIT", "OBJECT_CONTEXT_ID", "CONTEXT", "ID", false);
+ assertEquals("auditObjectContext", dbRelationshipName(r2));
+ }
}
diff --git a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
index c46b345d7..427235815 100644
--- a/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
+++ b/cayenne-project/src/main/java/org/apache/cayenne/project/validation/NameValidationHelper.java
@@ -82,18 +82,11 @@ public class NameValidationHelper {
"package",
"synchronized");
- public boolean isReservedJavaKeyword(String word) {
- return RESERVED_JAVA_KEYWORDS.contains(word);
- }
-
- // a property is considered invalid if there is a getter or a setter for it in
- // java.lang.Object or PersistentObject
+ // property getter or setter would conflict with Object or Persistent
private static final Collection<String> PERSISTENT_BASE_PROPERTIES = List.of(
"class",
- "committedSnapshot",
- "currentSnapshot",
- "dataContext",
"objectId",
+ "objectContext",
"persistenceState",
"snapshotVersion");
@@ -106,6 +99,10 @@ public class NameValidationHelper {
return sharedInstance;
}
+ public boolean isReservedJavaKeyword(String word) {
+ return RESERVED_JAVA_KEYWORDS.contains(word);
+ }
+
/**
* This is more of a sanity check than a real validation. As different DBs allow
* different chars in identifiers, here we simply check for dots.
@@ -119,7 +116,7 @@ public class NameValidationHelper {
*/
public String invalidCharsInObjPathComponent(String objPathComponent) {
String invalidChars = validateJavaIdentifier(objPathComponent, "");
- return (invalidChars.length() > 0) ? invalidChars : null;
+ return (!invalidChars.isEmpty()) ? invalidChars : null;
}
public String invalidCharsInJavaClassName(String javaClassName) {
@@ -134,7 +131,7 @@ public class NameValidationHelper {
invalidChars = validateJavaIdentifier(toks.nextToken(), invalidChars);
}
- return (invalidChars.length() > 0) ? invalidChars : null;
+ return !invalidChars.isEmpty() ? invalidChars : null;
}
public boolean invalidPersistentObjectClass(String persistentObjectClassFQN) {
@@ -167,14 +164,16 @@ public class NameValidationHelper {
}
}
+ StringBuilder buf = new StringBuilder(invalidChars);
for (int i = 1; i < len; i++) {
if (!Character.isJavaIdentifierPart(id.charAt(i))) {
- if (invalidChars.indexOf(id.charAt(i)) < 0) {
- invalidChars = invalidChars + id.charAt(i);
+ if (buf.toString().indexOf(id.charAt(i)) < 0) {
+ buf.append(id.charAt(i));
}
}
}
+ invalidChars = buf.toString();
return invalidChars;
}