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 14:01:55 UTC
(cayenne) 03/03: 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 5e9b009f0ad9ec02224db9f297c644448fb51d68
Author: Andrus Adamchik <an...@objectstyle.com>
AuthorDate: Mon Jul 20 15:52:21 2026 +0200
CAY-2980 Improve model name generation
name relashionships based on FK even when it does not end with _ID
---
ai-plugin/references/model-naming-conventions.md | 20 +++++----
ai-plugin/skills/cayenne-model-naming/SKILL.md | 13 +++---
.../dbsync/merge/context/EntityMergeSupport.java | 3 +-
.../dbsync/naming/BaseObjectNameGenerator.java | 21 +++++----
.../naming/DefaultObjectNameGeneratorTest.java | 50 ++++++++++++++++++----
.../tools/DbImporterMojoConfigurationTest.java | 2 +-
.../apache/cayenne/tools/config/pom-schema-2.xml | 2 +-
7 files changed, 73 insertions(+), 38 deletions(-)
diff --git a/ai-plugin/references/model-naming-conventions.md b/ai-plugin/references/model-naming-conventions.md
index 315a4b210..66a19e839 100644
--- a/ai-plugin/references/model-naming-conventions.md
+++ b/ai-plugin/references/model-naming-conventions.md
@@ -40,7 +40,7 @@ Concretely, the generator produces:
|---|---|---|
| `db-entity` name | stem, split on `_`, capitalize each token | `ARTIST_GROUP` → `ArtistGroup` |
| `db-attribute` name | split on `_`, camelCase | `FIRST_NAME` → `firstName` |
-| to-one relationship | FK column minus trailing `_ID`/`ID`; else target entity name | `MANAGER_ID` → `manager` |
+| to-one relationship | FK column, minus its trailing `_ID`/`ID` when present; target entity name for a compound (multi-column) FK or when there are no joins | `MANAGER_ID` → `manager`; `BIRTH_COUNTRY` → `birthCountry` |
| to-many relationship | English plural of the target entity name, prefixed with the FK role qualifier when the FK column embeds the source entity name | `PAINTING` → `paintings`; `HOME_TEAM_ID` → `homeGames` |
| name collision within an entity | append a numeric suffix | `team`, `team1`, `team2` … |
@@ -91,13 +91,15 @@ directions are not equally affected** — the to-one and to-many sides are named
`Employee`, because the role can't be mechanically tied to the entity. Name each collection by its
role yourself: `managedProjects` / `auditedProjects`.
-- **to-one side usually does NOT collide.** To-one naming is FK-column-based — it strips a trailing
- `_ID`/`ID`, so distinct `*_ID` FKs already yield distinct, good names (`HOME_TEAM_ID` → `homeTeam`,
- `AWAY_TEAM_ID` → `awayTeam`). Leave those alone. It **only** collides in the fallback path: when a
- FK column does *not* end in `ID`/`_ID` (or is null / has no joins), the generator drops to the
- target entity name, so two such FKs both become e.g. `employee` / `employee1`. There, derive the
- role from the FK column yourself even though it lacks the `_ID` suffix (`MANAGER` → `manager`,
- `SUPERVISOR` → `supervisor`).
+- **to-one side does NOT collide.** To-one naming is FK-column-based — the name is the FK column
+ with a trailing `_ID`/`ID` stripped when present (`HOME_TEAM_ID` → `homeTeam`, `BIRTH_COUNTRY` →
+ `birthCountry`), so distinct FK columns yield distinct, good names. Leave those alone. The
+ generator falls back to the target entity name only for a compound (multi-column) FK — whose
+ column names describe PK components, not a role — or when the relationship has no joins at all.
+ Models imported by **older Cayenne versions** used that fallback whenever the FK column lacked an
+ `ID`/`_ID` suffix, producing target-entity names and numbered collisions (`employee` /
+ `employee1`); when you see those, derive the role from the FK column yourself (`MANAGER` →
+ `manager`, `SUPERVISOR` → `supervisor`).
So in the typical "two `<ROLE>_<ENTITY>_ID` FKs" model **both directions are already fine**
(`homeTeam` ↔ `homeGames`, `awayTeam` ↔ `awayGames`) — nothing to rename. When you do rename, give
@@ -169,7 +171,7 @@ A DbRelationship `name` is **arbitrary** (not derived from a real table/column l
DbAttribute), but it is **not** exempt from cleanup — treat it as the first-class citizen here, since
every FK produces a DbRelationship whether or not an ObjRelationship was generated on it. Reverse
engineering names it with the **same generator** as ObjRelationships (to-one = FK column minus a
-trailing `_ID`/`ID`, else target entity name; to-many = English plural of the target entity with a
+trailing `_ID`/`ID` when present; to-many = English plural of the target entity with a
FK role qualifier when the FK embeds the source entity name, all `underscoredToJava`-cased), so it
inherits the same gaps — run-together names,
numbered collisions (`toArtist` / `toArtist1`), a leaked common prefix. Apply §1–§5 to DbRelationship
diff --git a/ai-plugin/skills/cayenne-model-naming/SKILL.md b/ai-plugin/skills/cayenne-model-naming/SKILL.md
index d4a85b2b3..edd50b59d 100644
--- a/ai-plugin/skills/cayenne-model-naming/SKILL.md
+++ b/ai-plugin/skills/cayenne-model-naming/SKILL.md
@@ -79,12 +79,13 @@ correct — leave them.** Flag only the cases the deterministic generator can't
Split on a real word boundary you're confident about; never invent one.
2. **Numbered collision names** (`projects` / `projects1`, `people` / `people1`) from more than one
relationship between the same two tables. The generator resolves the common cases itself: to-one
- names are FK-based (`HOME_TEAM_ID` → `homeTeam`), and to-many names pick up a FK role qualifier
- when the FK embeds the source entity name, even one sans a common table prefix (`HOME_TEAM_ID` →
- `homeGames`; `home_team_id` referencing `aa_team` → `homeAaGames`). What still collides:
- to-many collections whose FK role is unrelated to the entity name (`MANAGER_ID` / `AUDITOR_ID` →
- `projects` / `projects1` — rename by role: `managedProjects` / `auditedProjects`), and to-one
- ends whose FK columns lack an `_ID` suffix and collapse to `employee` / `employee1`.
+ names are FK-based even without an `_ID` suffix (`HOME_TEAM_ID` → `homeTeam`, `BIRTH_COUNTRY` →
+ `birthCountry`), and to-many names pick up a FK role qualifier when the FK embeds the source
+ entity name, even one sans a common table prefix (`HOME_TEAM_ID` → `homeGames`; `home_team_id`
+ referencing `aa_team` → `homeAaGames`). What still collides: to-many collections whose FK role is
+ unrelated to the entity name (`MANAGER_ID` / `AUDITOR_ID` → `projects` / `projects1` — rename by
+ role: `managedProjects` / `auditedProjects`), and — in models imported by older Cayenne versions —
+ to-one ends whose FK columns lack an `_ID` suffix and collapsed to `employee` / `employee1`.
3. **A common entity prefix leaking into relationship names** — when every entity shares a prefix
that was **kept** on the class names (`AaCustomer`, `AaOrder`), the relationship names inherit it
(`aaOrders`, `aaCustomer`). Strip the prefix from the relationship names (`orders`, `customer`);
diff --git a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
index 953672571..305e8e4cf 100644
--- a/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
+++ b/cayenne-dbsync/src/main/java/org/apache/cayenne/dbsync/merge/context/EntityMergeSupport.java
@@ -284,8 +284,7 @@ public class EntityMergeSupport {
}
// Check additional common DB types, like 'json' or 'geometry'
- if (dbAttribute instanceof DetectedDbAttribute) {
- DetectedDbAttribute detectedDbAttribute = (DetectedDbAttribute) dbAttribute;
+ if (dbAttribute instanceof DetectedDbAttribute detectedDbAttribute) {
String jdbcTypeName = detectedDbAttribute.getJdbcTypeName();
if (jdbcTypeName != null) {
String type = SQL_ADDITIONAL_TYPES_TO_JAVA_TYPE.get(jdbcTypeName.toLowerCase());
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 a818642d6..f1073b261 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
@@ -97,7 +97,8 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator {
private String toManyRoleQualifier(List<DbJoin> joins) {
- if (joins.isEmpty()) {
+ // a role is only derivable from a single-column FK; a compound FK's columns describe PK components
+ if (joins.size() != 1) {
return null;
}
@@ -115,7 +116,7 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator {
String roleUpper = role.toUpperCase();
// match the role against the source entity name, then against its shorter "_"-token suffixes,
- // as FK columns often drop a common table-name prefix ("home_team_id" referencing "nhl_team")
+ // as FK columns often drop a common table-name prefix ("home_team_id" referencing "acme_team")
String suffix = dbEntityBaseName(sourceEntity.getName()).toUpperCase();
while (true) {
if (roleUpper.equals(suffix)) {
@@ -137,24 +138,22 @@ public abstract class BaseObjectNameGenerator implements ObjectNameGenerator {
protected String toOneBase(List<DbJoin> joins, String targetEntityName) {
- if (joins.isEmpty()) {
- // In case, when uses EditRelationship button, relationship doesn't exist => it doesn't have joins
- // and just return targetName
+ // the FK column is a name source only for a single-join relationship: with no joins (e.g. the Modeler's
+ // EditRelationship dialog) there is no FK, and a compound FK's column names describe PK components,
+ // not the relationship role
+ if (joins.size() != 1) {
return dbEntityBaseName(targetEntityName);
}
- DbJoin join1 = joins.getFirst();
-
- // TODO: multi-join relationships
-
// return the name of the FK column sans ID
- String fkColName = join1.getSourceName();
+ String fkColName = joins.getFirst().getSourceName();
if (fkColName == null) {
return dbEntityBaseName(targetEntityName);
}
+ // an FK without an ID suffix ("birth_country" referencing "country") is still the best name source
String fkBase = stripIdSuffix(fkColName);
- return fkBase != null ? fkBase : dbEntityBaseName(targetEntityName);
+ return fkBase != null ? fkBase : fkColName;
}
private static String stripIdSuffix(String fkColName) {
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 102f6917c..5ec86462e 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
@@ -165,22 +165,56 @@ public class DefaultObjectNameGeneratorTest {
assertEquals("games", dbRelationshipName(r7));
}
+ @Test
+ public void dbRelationshipName_ToOne_FkWithoutIdSuffix() {
+
+ // an FK without an ID suffix still names the relationship after the column, not the target table
+ DbRelationship r1 = makeRelationship("employee", "birth_country", "country", "id", false);
+ assertEquals("birthCountry", dbRelationshipName(r1));
+
+ DbRelationship r2 = makeRelationship("PERSON", "SHIPPING_ADDRESS", "ADDRESS", "ID", false);
+ assertEquals("shippingAddress", dbRelationshipName(r2));
+
+ // an FK named after the bare target concept keeps the column name
+ DbRelationship r3 = makeRelationship("employee", "country", "acme_country", "id", false);
+ assertEquals("country", dbRelationshipName(r3));
+ }
+
@Test
public void dbRelationshipName_ToMany_RoleQualifiedFk_FkDropsTablePrefix() {
// FK columns matching a "_"-token suffix of the entity name still carry the role...
- DbRelationship r1 = makeRelationship("nhl_team", "id", "nhl_game", "home_team_id", true);
- assertEquals("homeNhlGames", dbRelationshipName(r1));
+ DbRelationship r1 = makeRelationship("acme_team", "id", "acme_game", "home_team_id", true);
+ assertEquals("homeAcmeGames", dbRelationshipName(r1));
- DbRelationship r2 = makeRelationship("nhl_team", "id", "nhl_game", "visiting_team_id", true);
- assertEquals("visitingNhlGames", dbRelationshipName(r2));
+ DbRelationship r2 = makeRelationship("acme_team", "id", "acme_game", "visiting_team_id", true);
+ assertEquals("visitingAcmeGames", dbRelationshipName(r2));
// ... and a plain suffix reference gets no qualifier
- DbRelationship r3 = makeRelationship("nhl_team", "id", "nhl_award", "team_id", true);
- assertEquals("nhlAwards", dbRelationshipName(r3));
+ DbRelationship r3 = makeRelationship("acme_team", "id", "acme_award", "team_id", true);
+ assertEquals("acmeAwards", dbRelationshipName(r3));
- DbRelationship r4 = makeRelationship("nhl_game_type", "id", "nhl_game", "type_id", true);
- assertEquals("nhlGames", dbRelationshipName(r4));
+ DbRelationship r4 = makeRelationship("acme_game_type", "id", "acme_game", "type_id", true);
+ assertEquals("acmeGames", dbRelationshipName(r4));
+ }
+
+ @Test
+ public void dbRelationshipName_MultiJoin() {
+
+ // a compound FK's columns describe PK components, so the to-one side is named after the target entity
+ DbRelationship r1 = makeRelationship("shipment", "order_id", "order_line", "order_id", false);
+ r1.addJoin(new DbJoin(r1, "line_num", "line_num"));
+ assertEquals("orderLine", dbRelationshipName(r1));
+
+ // ... and the to-many side gets an unqualified plural
+ DbRelationship r2 = makeRelationship("order_line", "order_id", "shipment", "order_id", true);
+ r2.addJoin(new DbJoin(r2, "line_num", "line_num"));
+ assertEquals("shipments", dbRelationshipName(r2));
+
+ // no role qualifier even when the first FK column alone would suggest one
+ DbRelationship r3 = makeRelationship("team", "team_id", "game", "home_team_id", true);
+ r3.addJoin(new DbJoin(r3, "season_id", "season_id"));
+ assertEquals("games", dbRelationshipName(r3));
}
@Test
diff --git a/cayenne-maven-plugin/src/test/java/org/apache/cayenne/tools/DbImporterMojoConfigurationTest.java b/cayenne-maven-plugin/src/test/java/org/apache/cayenne/tools/DbImporterMojoConfigurationTest.java
index 6bd13626d..00a6b1c58 100644
--- a/cayenne-maven-plugin/src/test/java/org/apache/cayenne/tools/DbImporterMojoConfigurationTest.java
+++ b/cayenne-maven-plugin/src/test/java/org/apache/cayenne/tools/DbImporterMojoConfigurationTest.java
@@ -90,7 +90,7 @@ public class DbImporterMojoConfigurationTest {
List<Pattern> excludes = new ArrayList<>();
excludes.add(PatternFilter.pattern("^ETL_.*"));
- assertEquals(filters.tableFilter(null, "NHL_STATS"),
+ assertEquals(filters.tableFilter(null, "GAME_STATS"),
new TableFilter(includes, excludes));
}
diff --git a/cayenne-maven-plugin/src/test/resources/org/apache/cayenne/tools/config/pom-schema-2.xml b/cayenne-maven-plugin/src/test/resources/org/apache/cayenne/tools/config/pom-schema-2.xml
index ceda9eaad..9ca13e33a 100644
--- a/cayenne-maven-plugin/src/test/resources/org/apache/cayenne/tools/config/pom-schema-2.xml
+++ b/cayenne-maven-plugin/src/test/resources/org/apache/cayenne/tools/config/pom-schema-2.xml
@@ -28,7 +28,7 @@
<configuration>
<dbimport>
<schema>
- <name>NHL_STATS</name>
+ <name>GAME_STATS</name>
<excludeTable>^ETL_.*</excludeTable>
<excludeColumn>^ETL_.*</excludeColumn>
</schema>