TypeORM嵌套一对多自增ID级联插入失败,触发非空约束错误
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
itemIdfield inItemCustomizationwas defined with@Columninstead of@PrimaryColumn, which confuses TypeORM's association tracking. - The cascade configuration was limited to only
insertoperations, 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_idreferencesitem.item_iditem_customization_option.(item_id, customization_id)referencesitem_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




