You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

TypeORM嵌套一对多自增ID级联插入失败,触发非空约束错误

Fixing TypeORM Two-Level Nested Association Insert Failure

The core issue here is that TypeORM isn't properly propagating the auto-generated item_id from the parent Item entity down to the nested ItemCustomizationOption entity during cascaded insert, leading to a null value in column "item_id" violation. Let's break down the problem and fix it step by step.

Root Cause Analysis

Your entity configuration uses composite primary keys for both ItemCustomization and ItemCustomizationOption, but the way you've defined the primary key fields and associations is preventing TypeORM from automatically populating the inherited item_id value across all nested levels. Specifically:

  • The itemId field in ItemCustomization was defined with @Column instead of @PrimaryColumn, which confuses TypeORM's association tracking.
  • The cascade configuration was limited to only insert operations, which might not cover all necessary steps to propagate IDs.

Step-by-Step Solution

1. Correct Entity Configurations

Update Item.ts

Expand the cascade option to cover all operations (insert, update, etc.) to ensure full propagation of changes:

import { Column, Entity, Index, OneToMany, PrimaryGeneratedColumn } from 'typeorm';
import { ItemCustomization } from "./ItemCustomization";

@Index("PK_item", ["itemId"], { unique: true })
@Entity("item", { schema: "foodspot" })
export class Item {
  @PrimaryGeneratedColumn({ type: "integer", name: "item_id" })
  itemId: number;

  @Column("text", { name: "name" })
  name: string;

  @Column("money", { name: "price" })
  price: string;

  // Use cascade: true to enable full cascaded operations
  @OneToMany(() => ItemCustomization, (customization) => customization.item, { cascade: true })
  itemCustomizations: ItemCustomization[];
}

Update ItemCustomization.ts

Use @PrimaryColumn for the composite primary key field itemId, and ensure the cascade option for itemCustomizationOptions is set to true:

import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm';
import { Item } from './Item';
import { ItemCustomizationOption } from './ItemCustomizationOption';

@Index('PK_item_customization', ['customizationId', 'itemId'], { unique: true })
@Index('fkIdx_item_customization_item', ['itemId'], {})
@Entity('item_customization', { schema: 'foodspot' })
export class ItemCustomization {
  // Mark itemId as part of the composite primary key
  @PrimaryColumn('integer', { name: 'item_id' })
  public itemId: number;

  @PrimaryGeneratedColumn({ type: 'integer', name: 'customization_id' })
  public customizationId: number;

  @Column('text', { name: 'title' })
  public title: string;

  @ManyToOne(() => Item, (item) => item.itemCustomizations)
  @JoinColumn([{ name: 'item_id', referencedColumnName: 'itemId' }])
  public item: Item;

  // Enable full cascading for nested options
  @OneToMany(() => ItemCustomizationOption, (option) => option.itemCustomization, { cascade: true })
  public itemCustomizationOptions: ItemCustomizationOption[];
}

Update ItemCustomizationOption.ts

Use @PrimaryColumn for all composite primary key fields, and confirm the join column mapping matches the parent entity's composite key:

import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm';
import { ItemCustomization } from './ItemCustomization';

@Index('PK_item_customization_option', ['customizationId', 'itemId', 'optionItemId'], { unique: true })
@Index('fkIdx_item_customization_option_item_customization', ['customizationId', 'itemId'], {})
@Index('fkIdx_item_customization_option_option_item', ['optionItemId'], {})
@Entity('item_customization_option', { schema: 'foodspot' })
export class ItemCustomizationOption {
  @PrimaryColumn('integer', { name: 'item_id' })
  public itemId: number;

  @Column('money', { name: 'price' })
  public price: string;

  @PrimaryColumn('integer', { name: 'customization_id' })
  public customizationId: number;

  @PrimaryColumn('integer', { name: 'option_item_id' })
  public optionItemId: number;

  @ManyToOne(() => ItemCustomization, (customization) => customization.itemCustomizationOptions)
  @JoinColumn([
    { name: 'item_id', referencedColumnName: 'itemId' },
    { name: 'customization_id', referencedColumnName: 'customizationId' },
  ])
  public itemCustomization: ItemCustomization;
}

2. Adjust Your Save Logic

Note: Your original input object was missing the price field required by the Item entity. Make sure to include it when saving:

const newItem = {
  name: "Cheese Burger",
  price: "10.99", // Add this required field
  itemCustomizations: [
    {
      title: "Sides",
      itemCustomizationOptions: [
        { optionItemId: 1, price: "3.00" }
      ]
    }
  ]
};

// Save the item with cascaded associations
await getRepository(Item).save(newItem);

3. Verify Database Constraints

Ensure your database has the correct foreign key constraints:

  • item_customization.item_id references item.item_id
  • item_customization_option.(item_id, customization_id) references item_customization.(item_id, customization_id)

This ensures data consistency and helps TypeORM correctly resolve association relationships.

Why This Works

By using @PrimaryColumn for composite key fields, we explicitly tell TypeORM which fields are part of the primary key. Combined with full cascade operations, TypeORM will now automatically propagate the auto-generated item_id from the parent Item down through ItemCustomization to ItemCustomizationOption during insert, eliminating the null value error.

内容的提问来源于stack exchange,提问作者Kapileshwar Mittapelly

火山引擎 最新活动