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 <[email protected]> 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>
