diff --git a/src/metadata/ColumnMetadata.ts b/src/metadata/ColumnMetadata.ts index 94d090bba5..d4e1d36b0c 100644 --- a/src/metadata/ColumnMetadata.ts +++ b/src/metadata/ColumnMetadata.ts @@ -662,7 +662,18 @@ export class ColumnMetadata { return extractEmbeddedColumnValue([...this.embeddedMetadata.embeddedMetadataTree], entity); } else { - entity[this.propertyName] = value; + // we write a deep object in this entity only if the column is virtual + // because if its not virtual it means the user defined a real column for this relation + // also we don't do it if column is inside a junction table + if (!this.entityMetadata.isJunction && this.isVirtual && this.referencedColumn && this.referencedColumn.propertyName !== this.propertyName) { + if (!(this.propertyName in entity)) { + entity[this.propertyName] = {}; + } + + entity[this.propertyName][this.referencedColumn.propertyName] = value; + } else { + entity[this.propertyName] = value; + } } } diff --git a/src/persistence/Subject.ts b/src/persistence/Subject.ts index 988509fbb7..80405b1ffe 100644 --- a/src/persistence/Subject.ts +++ b/src/persistence/Subject.ts @@ -289,7 +289,8 @@ export class Subject { if (this.parentSubject) { this.metadata.primaryColumns.forEach(primaryColumn => { if (primaryColumn.relationMetadata && primaryColumn.relationMetadata.inverseEntityMetadata === this.parentSubject!.metadata) { - primaryColumn.setEntityValue(this.entityWithFulfilledIds!, this.parentSubject!.entity); + const value = primaryColumn.referencedColumn!.getEntityValue(this.parentSubject!.entity!); + primaryColumn.setEntityValue(this.entityWithFulfilledIds!, value); } }); } diff --git a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts index e79fd24d63..05dc941ac7 100644 --- a/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts +++ b/src/query-builder/transformer/RawSqlResultsToEntityTransformer.ts @@ -237,7 +237,7 @@ export class RawSqlResultsToEntityTransformer { const idMap = columns.reduce((idMap, column) => { let value = result[column.databaseName]; if (relation.isOneToMany || relation.isOneToOneNotOwner) { - if (column.referencedColumn) // if column is a relation + if (column.isVirtual && column.referencedColumn && column.referencedColumn.propertyName !== column.propertyName) // if column is a relation value = column.referencedColumn.createValueMap(value); return OrmUtils.mergeDeep(idMap, column.createValueMap(value)); diff --git a/test/github-issues/6416/entity/Post.ts b/test/github-issues/6416/entity/Post.ts new file mode 100644 index 0000000000..d70667904e --- /dev/null +++ b/test/github-issues/6416/entity/Post.ts @@ -0,0 +1,52 @@ +import { EntitySchema } from "../../../../src"; + +import PostTag from "./PostTag"; +import PostAttachment from "./PostAttachment"; + +let id = 0; + +export default class Post { + postId: number; + + otherId: number; + + tags: PostTag[]; + + attachments: PostAttachment[]; + + constructor() { + this.postId = id++; + this.otherId = id++; + } +} + +export const PostSchema = new EntitySchema({ + name: "Post", + target: Post, + columns: { + otherId: { + type: Number, + primary: true, + nullable: false + }, + postId: { + type: Number, + primary: true, + nullable: false + } + }, + relations: { + tags: { + target: () => PostTag, + type: "one-to-many", + inverseSide: "post", + cascade: true + }, + attachments: { + target: () => PostAttachment, + type: "one-to-many", + inverseSide: "post", + cascade: true + } + } +}); diff --git a/test/github-issues/6416/entity/PostAttachment.ts b/test/github-issues/6416/entity/PostAttachment.ts new file mode 100644 index 0000000000..2408e5816e --- /dev/null +++ b/test/github-issues/6416/entity/PostAttachment.ts @@ -0,0 +1,35 @@ +import { EntitySchema } from "../../../../src"; + +import Post from "./Post"; + +let id = 0; + +export default class PostAttachment { + attachmentId: number; + + post: Post; + + constructor () { + this.attachmentId = id++; + } +} + +export const PostAttachmentSchema = new EntitySchema({ + name: "PostAttachment", + target: PostAttachment, + columns: { + attachmentId: { + type: Number, + primary: true, + nullable: false + } + }, + relations: { + post: { + primary: true, + nullable: false, + target: () => Post, + type: "many-to-one" + } + } +}); diff --git a/test/github-issues/6416/entity/PostTag.ts b/test/github-issues/6416/entity/PostTag.ts new file mode 100644 index 0000000000..1217085738 --- /dev/null +++ b/test/github-issues/6416/entity/PostTag.ts @@ -0,0 +1,51 @@ +import { EntitySchema } from "../../../../src"; + +import Post from "./Post"; + +let id = 0; + +export default class PostTag { + tagId: number; + + tagOtherId: string; + + tagPostId: string; + + post: Post; + + constructor () { + this.tagId = id++; + } +} + +export const PostTagSchema = new EntitySchema({ + name: "PostTag", + target: PostTag, + columns: { + tagOtherId: { + type: Number, + primary: true + }, + tagPostId: { + type: Number, + primary: true + }, + tagId: { + type: Number, + primary: true, + nullable: false + } + }, + relations: { + post: { + primary: true, + nullable: false, + target: () => Post, + type: "many-to-one", + joinColumn: [ + { name: "tagPostId", referencedColumnName: "postId" }, + { name: "tagOtherId", referencedColumnName: "otherId" } + ] + } + } +}); diff --git a/test/github-issues/6416/issue-6416.ts b/test/github-issues/6416/issue-6416.ts new file mode 100644 index 0000000000..f09ab22b21 --- /dev/null +++ b/test/github-issues/6416/issue-6416.ts @@ -0,0 +1,66 @@ +import {closeTestingConnections, createTestingConnections, reloadTestingDatabases} from "../../utils/test-utils"; +import {Connection} from "../../../src"; + +import { assert } from "chai"; + +import Post, { PostSchema } from "./entity/Post"; +import PostTag, { PostTagSchema } from "./entity/PostTag"; +import PostAttachment, { PostAttachmentSchema } from "./entity/PostAttachment"; + +describe("github issues > #6399 Combining ManyToOne, Cascade, & Composite Primary Key causes Unique Constraint issues", () => { + + let connections: Connection[]; + before(async () => connections = await createTestingConnections({ + entities: [PostSchema, PostTagSchema, PostAttachmentSchema], + enabledDrivers: ["sqlite"], + })); + beforeEach(() => reloadTestingDatabases(connections)); + after(() => closeTestingConnections(connections)); + + it("persisting the cascading entities should succeed", () => Promise.all(connections.map(async connection => { + + const post = new Post(); + const postTag = new PostTag(); + post.tags = [postTag]; + + await connection.manager.save(post, { reload: true }); + + try { + await connection.manager.save(post); + } catch (e) { + assert.fail(e.toString(), null, "Second save had an exception"); + } + }))); + + it("persisting the cascading entities without JoinColumn should succeed", () => Promise.all(connections.map(async connection => { + + const post = new Post(); + const postAttachment = new PostAttachment(); + post.attachments = [postAttachment]; + + await connection.manager.save(post, { reload: true }); + + try { + await connection.manager.save(post); + } catch (e) { + assert.fail(e.toString(), null, "Second save had an exception"); + } + }))); + + it("persisting the child entity should succeed", () => Promise.all(connections.map(async connection => { + const post = new Post(); + + await connection.manager.save(post); + + const postTag = new PostTag(); + postTag.post = post; + + await connection.manager.save(postTag, { reload: true }); + + try { + await connection.manager.save(postTag); + } catch (e) { + assert.fail(e.toString(), null, "Second save had an exception"); + } + }))); +});