typeorm: Cascade on save when it is not set

Issue type:

[ ] question [x] bug report [ ] feature request [ ] documentation issue

Database system/driver:

[ ] cordova [ ] mongodb [ ] mssql [x] mysql / mariadb [ ] oracle [ ] postgres [ ] sqlite [ ] sqljs [ ] react-native [ ] expo

TypeORM version:

[ ] latest [ ] @next [x] 0.2.7 (or put your version here)

Steps to reproduce or a small repository showing the problem:

Here are my entities:

export class Category {

  @PrimaryGeneratedColumn({unsigned: true})
  id: number;

  @Length(4, 45)
  @Column({type: 'varchar', length: 45})
  name: string;

  @CreateDateColumn({precision: null, type: 'timestamp', default: () => 'CURRENT_TIMESTAMP'})
  createdAt: Date;

  @IsInt()
  @Min(0)
  @Max(1)
  @Column({type: 'tinyint', width: 1})
  isLeaf: number;

  @ManyToOne(type => User, creator => creator.createdCategories, {nullable: false})
  creator: User;

  @ManyToOne(type => Category, parent => parent.children)
  @JoinColumn({name: 'parent_category_id'})
  parent: Category;

  @OneToOne(type => CategoryLeaf, category => category.category)
  categoryLeaf: CategoryLeaf;

  @OneToMany(type => Category, category => category.parent)
  children: Category[];
}
export class CategoryLeaf {

  @IsInt()
  @PrimaryColumn({unsigned: true})
  categoryId: number;

  @OneToOne(type => Category)
  @JoinColumn()
  category: Promise<Category>;

  @IsInt()
  @Min(0)
  @Max(9999)
  @Column({type: 'tinyint', width: 4})
  sla: number;

  @IsEnum(ApprovalMechanism)
  @Column({type: 'enum', enum: ApprovalMechanism, nullable: true})
  approvalMechanism: ApprovalMechanism;

  @IsJSON()  
  @Column({type: 'simple-json'})
  template: JSON;

  @OneToMany(type => Request, request => request.category)
  requests: Request[];

  @OneToMany(type => ApproverCategory, approvers => approvers.categoryData, {cascade: true})
  approvers: Promise<ApproverCategory[]>;

  @ManyToMany(type => Room, room => room.categories)
  @JoinTable({
    name: 'categories_rooms',
    joinColumns: [{
      name: 'category_id'
    }],
    inverseJoinColumns: [{
      name: 'room_id'
    }]
  })
  rooms: Promise<Room[]>;
}

When I try to make a save on Category this way:

const categoryRepository = await getRepository(Category);
let existingCategory = categoryRepository.findOne(1, ['categoryLeaf']);
existingCategory.name = 'New Name';
await categoryRepository.save(existingCategory);

I’m getting this error: query: SELECT categories_leaf.category_id AS category_id FROM categories_leaf categories_leaf WHERE ((categories_leaf.category_id = ?)) – PARAMETERS: [2] query: START TRANSACTION query: UPDATE categories SET name = ? WHERE id = ? – PARAMETERS: [“Cat222”,2] query: UPDATE categories_leaf SET category_id = ? WHERE category_id = ? – PARAMETERS: [null,2] query failed: UPDATE categories_leaf SET category_id = ? WHERE category_id = ? – PARAMETERS: [null,2] error: { Error: ER_BAD_NULL_ERROR: Column ‘category_id’ cannot be null

Why is the save action trying to update categoryLeaf if i didn’t configure cascade on true?

About this issue

  • Original URL
  • State: open
  • Created 6 years ago
  • Reactions: 10
  • Comments: 26 (11 by maintainers)

Most upvoted comments

well… that’s confusing judging by the age of this bug, I think we’ll have to live with it 😆

It’s not completely intuitive to me, but something that might help is if you set persistence: false in the relationship:

  @OneToOne(type => CategoryLeaf, category => category.category, {
    persistence: false
  })
  categoryLeaf: CategoryLeaf;

I’m not sure why lacking cascade: ["update"] doesn’t have the same effect. Would need @pleerock to weigh in on the intent here.

This is really frustrating.

Let’s say I have the following example:

User

@Entity()
export class User extends BaseEntity {
  @PrimaryGeneratedColumn()
  id!: string;

  @Column()
  name!: string;

  @OneToMany(() => UserThing, x => x.user)
  things?: UserThing[];
}

UserThing

@Entity()
export class UserThing extends BaseEntity {
  @PrimaryGeneratedColumn()
  userId!: string;

  @ManyToOne(() => User, x => x.things)
  user?: User;

  @Column()
  data!: string;
}

Now simply trying to load a user with it’s things and then saving that user causes TypeORM to try to update all of the user’s things. It actually even does the update twice, once with all the same values the thing already has, and a second time with a null userId which fails.

const user = await User.findOne(userId, {
  relations: ['things'],
});

await user.save();

This makes absolutely no sense. All I’ve done is just load the user and immediately save it without modifying anything and it still fails!

Setting persistence: false for the one-to-many relation somewhat fixes the issue. On save, TypeORM still re-selects all of the user’s things (which is pretty inefficient) and then I can’t make use of any nested update functionality if I wanted to.

Basically my only real solution is to not select related entities if I want to save the main entity.

@pleerock that’s exactly what I’d like to do - allow persistence of relations, but unfortunately only the persistence: false attribute fixes the original issue in this thread.

I originally had a many-to-many setup, however as TypeORM doesn’t allow for additional values on the joining table I created an intermediary entity and made it one-to-many, many-to-one, one-to-many between Site, SiteSpecie and Specie. Perhaps then my schema is incorrect (using Babel / JS):

Site

@Entity("sites")
export class Site extends BaseEntity {

    @PrimaryColumn("integer")
    id = undefined;

    @OneToMany(type => SiteSpecie, site_specie => site_specie.site, {
        eager: true, persistence: false // See https://github.com/typeorm/typeorm/issues/2859
    })
    site_species = undefined

    @Column("text")
    name = "";
}

SIteSpecie

@Entity("site_species")
export class SiteSpecie extends BaseEntity {

    @ManyToOne(type => Site, site => site.site_species, { primary: true, cascade: false })
    @JoinColumn({ name: "site_id" })
    site = undefined

    @ManyToOne(type => Specie, specie => specie.site_species, { primary: true, eager: true })
    @JoinColumn({ name: "specie_id" })
    specie = undefined

    @PrimaryColumn({
        type: "integer",
        default: "0"
    })
    is_rare = undefined;

}

Specie

@Entity("species")
export class Specie extends BaseEntity {

    @PrimaryColumn("integer")
    id = undefined;

    @OneToMany(type => SiteSpecie, site_specie => site_specie.specie)
    site_species = undefined

    @Column("varchar")
    common_name = "";
}

Without persistence: false whenever I run site->save() (after, say, changing the site name) TypeORM tries to update every entry in site_specie with null values, just like the original message in this thread. This obviously fails, and so nothing is updated and the transaction rolls back.

I have another example that resulted in a bug that we couldn’t find that easily…

Let’s say you have those two entities:

@Entity('pickup_points')
export class PickupPointEntity {
  @PrimaryGeneratedColumn({ unsigned: true })
  public id: number;

  @Column({ type: 'varchar', length: 255, nullable: false })
  public name: string;

  @OneToMany(() => ProductEntity, product => product.pickupPoint)
  public products: ProductEntity[];
}
@Entity('products')
export class ProductEntity {
  @PrimaryGeneratedColumn({ unsigned: true })
  public id: number;

  @Column({ type: 'varchar', length: 100, nullable: true })
  public name?: string = null;

  @Column({ type: 'varchar', length: 255, nullable: true })
  public description?: string = null;

  @ManyToOne(() => PickupPointEntity, pickupPoint => pickupPoint.products, { onDelete: 'SET NULL' })
  public pickupPoint: PickupPointEntity;
}

When you do something like this:

/**
  * Updates a given pickup point.
  *
  * @param id The ID of the pickup point.
  * @param updatePickupPointDto The DTO containing the new data.
  */
public async update(id: number, updatePickupPointDto: UpdatePickupPointDto): Promise<PickupPointEntity> {
  const pickupPoint = await this.pickupPointRepository.findOne({
    where: { id },
    relations: { products: true },
    select: { id: true, name: true, products: { id: true } },
  });

  pickupPoint.name = updatePickupPointDto.name ?? pickupPoint.name;
    
  await this.pickupPointRepository.save(pickupPoint);
  return pickupPoint;    
}

Then you will end up with all products of that pickup-point having everything set to their default value (null), because you could not tell typeorm to only save the entity without relations…

I can confirm I experience the same issue with MySQL and TypeORM v0.2.7. The relation is configured exactly the same way, it’s a OneToOne relationship and the entity I’m saving is the inverse side (the side that does not have the JoinColumn). doesn’t matter if cascade is omitted or explicitly set to false on the inverse relation, TypeORM will try to save the parent when the inverse is saved.

@MubashirMalik sadly no. We had to check for those occurrences and optimize them by for example using update instead of save and only passing the changed properties or even decrease performance by not just selecting specific columns and instead select everything…

@ORzazade Gotcha. I think the persistence option needs a complete overhaul.

@P4sca1 Oh yeah by no means a solution, but seeing as how old this issue is, I don’t foresee us getting a real one anytime soon unless @pleerock chimes back in.