Calling `create` with a loaded entity reloads relations in both directions
Issue description
Calling create with a loaded entity reloads relations in both directions
Expected Behavior
Calling create only uses the non-virtual columns from the given entity (or entities) to construct the new entity.
Actual Behavior
We sometimes call create with an existing entity, or a saved set of attributes. If we do this including relations (i.e. the default behaviour), TypeORM will reload all relations in both directions. That is, it will reload relations of the entity being loaded - and the inverse relations too.
Steps to reproduce
entity/Category.ts:
import { BaseEntity, Column, Entity, OneToMany, PrimaryColumn } from "../../../../src"
import { Post } from "./Post"
@Entity()
export class Category extends BaseEntity {
@PrimaryColumn()
id: number
@Column()
name: string
@OneToMany((type) => Post, post => post.category)
posts: Promise<Post[]>
}
entity/Post.ts:
import {
BaseEntity,
Column,
Entity,
ManyToOne,
PrimaryColumn,
} from "../../../../src"
import { Category } from "./Category"
@Entity()
export class Post extends BaseEntity {
@PrimaryColumn()
id: number
@Column({
nullable: true,
unique: true,
})
externalId?: string
@Column()
title: string
@Column({
default: "This is default text.",
})
text: string
@ManyToOne((type) => Category, category => category.posts)
category: Promise<Category>
@Column()
categoryId: number;
}
issue-new.ts:
import "reflect-metadata";
import { createTestingConnections, closeTestingConnections, reloadTestingDatabases } from "../../utils/test-utils";
import { DataSource } from "../../../src/data-source/DataSource"
import { Post } from "./entity/Post"
import { Category } from "./entity/Category"
describe("github issues > #new", () => {
let dataSources: DataSource[];
before(async () => dataSources = await createTestingConnections({
entities: [__dirname + "/entity/*{.js,.ts}"],
schemaCreate: true,
dropSchema: true,
}));
beforeEach(() => reloadTestingDatabases(dataSources));
after(() => closeTestingConnections(dataSources));
it.only("should not reload relations in create", () => Promise.all(dataSources.map(async dataSource => {
const category = Category.create({ id: 1, name: "Create" })
await category.save()
const existingPost = Post.create({
id: 1,
title: "First post",
categoryId: category.id,
})
await existingPost.save()
const newPost = Post.create({
id: 2,
title: "About ActiveRecord",
text: "Huge discussion how good or bad ActiveRecord is.",
categoryId: category.id,
})
console.log("Recreating", newPost)
const newPost2 = Post.create(newPost)
newPost2.title.should.be.eql(newPost.title)
})));
});
Run this with logging enabled. You will see:
# skip schema setup
query: SELECT "Category"."id" AS "Category_id", "Category"."name" AS "Category_name" FROM "category" "Category" WHERE "Category"."id" IN ($1) -- PARAMETERS: [1]
query: START TRANSACTION
query: INSERT INTO "category"("id", "name") VALUES ($1, $2) -- PARAMETERS: [1,"Create"]
query: COMMIT
query: SELECT "Post"."id" AS "Post_id", "Post"."externalId" AS "Post_externalId", "Post"."title" AS "Post_title", "Post"."text" AS "Post_text", "Post"."categoryId" AS "Post_categoryId" FROM "post" "Post" WHERE "Post"."id" IN ($1) -- PARAMETERS: [1]
query: START TRANSACTION
query: INSERT INTO "post"("id", "externalId", "title", "text", "categoryId") VALUES ($1, DEFAULT, $2, DEFAULT, $3) RETURNING "text" -- PARAMETERS: [1,"First post",1]
query: COMMIT
Recreating Post {
id: 2,
title: 'About ActiveRecord',
text: 'Huge discussion how good or bad ActiveRecord is.',
categoryId: 1
}
✔ should not reload relations in create (71ms)
query: SELECT "category"."id" AS "category_id", "category"."name" AS "category_name" FROM "category" "category" INNER JOIN "post" "Post" ON "Post"."categoryId" = "category"."id" WHERE "Post"."id" IN ($1) -- PARAMETERS: [2]
query failed: SELECT "category"."id" AS "category_id", "category"."name" AS "category_name" FROM "category" "category" INNER JOIN "post" "Post" ON "Post"."categoryId" = "category"."id" WHERE "Post"."id" IN ($1) -- PARAMETERS: [2]
error: Error: Connection terminated
1 passing (495ms)
Note how this runs the queries for Post.category and Category.posts. This can result in loading far more relations than expected.
My Environment
| Dependency | Version |
|---|---|
| TypeORM version | master branc |
Additional Context
We can work around this by filtering the column list to only non-virtual columns ourselves, but this is really surprising and hard to debug behaviour. We had this case with an inverse relation that loaded tens of thousands of rows, which was very bad for performance.
Relevant Database Driver(s)
- [ ] aurora-mysql
- [ ] aurora-postgres
- [ ] better-sqlite3
- [ ] cockroachdb
- [ ] cordova
- [ ] expo
- [ ] mongodb
- [ ] mysql
- [ ] nativescript
- [ ] oracle
- [X] postgres
- [ ] react-native
- [ ] sap
- [ ] spanner
- [ ] sqlite
- [ ] sqlite-abstract
- [ ] sqljs
- [ ] sqlserver
Are you willing to resolve this issue by submitting a Pull Request?
Yes, I have the time, but I don't know how to start. I would need guidance.