TypeORM - 重复键值违反唯一约束(可延迟 fk)

Posted

技术标签:

【中文标题】TypeORM - 重复键值违反唯一约束(可延迟 fk)【英文标题】:TypeORM - duplicate key value violates unique constraint (deferrable fk) 【发布时间】:2021-03-12 09:58:39 【问题描述】:

我有以下情况。

export default class BucketEntity extends Entity 
  @Column(
    type: "enum",
    enum: BucketType,
    default: BucketType.Notes,
  )
  type: BucketType;

  @Column()
  title: string;

  @OneToOne(() => BucketEntity, 
    nullable: true,
    onDelete: "NO ACTION",
    deferrable: "INITIALLY DEFERRED",
  )
  @JoinColumn()
  prev: Promise<BucketEntity | null>;

  @Column( nullable: true )
  prevId: number | null;

  @OneToOne(() => BucketEntity, 
    nullable: true,
    onDelete: "NO ACTION",
    deferrable: "INITIALLY DEFERRED",
  )
  @JoinColumn()
  next: Promise<BucketEntity | null>;

  @Column( nullable: true )
  nextId: number | null;

这是一个Bucket 实体,它与自身有两个一对一的关系。想想双向链表。每个Bucket 都有一个prev 和一个next 参考。这些关系具有deferrable: true 标志,这意味着我得到了一个可延迟的 FK 约束。

这是由该实体构建的实际表格:

                                         Table "public.dashboardBuckets"
  Column   |             Type             | Collation | Nullable |                    Default
-----------+------------------------------+-----------+----------+------------------------------------------------
 id        | integer                      |           | not null | nextval('"dashboardBuckets_id_seq"'::regclass)
 createdAt | timestamp without time zone  |           | not null | now()
 updatedAt | timestamp without time zone  |           | not null | now()
 type      | "dashboardBuckets_type_enum" |           | not null | 'Notes'::"dashboardBuckets_type_enum"
 title     | character varying            |           | not null |
 prevId    | integer                      |           |          |
 nextId    | integer                      |           |          |
Indexes:
    "PK_6823bada941d24553e040989058" PRIMARY KEY, btree (id)
    "REL_9fc9b030d495660236b8d00074" UNIQUE CONSTRAINT, btree ("nextId")
    "REL_ea16688a9b632de7758cf696b5" UNIQUE CONSTRAINT, btree ("prevId")
Foreign-key constraints:
    "FK_9fc9b030d495660236b8d00074a" FOREIGN KEY ("nextId") REFERENCES "dashboardBuckets"(id) DEFERRABLE INITIALLY DEFERRED
    "FK_ea16688a9b632de7758cf696b52" FOREIGN KEY ("prevId") REFERENCES "dashboardBuckets"(id) DEFERRABLE INITIALLY DEFERRED
Referenced by:
    TABLE ""dashboardBuckets"" CONSTRAINT "FK_9fc9b030d495660236b8d00074a" FOREIGN KEY ("nextId") REFERENCES "dashboardBuckets"(id) DEFERRABLE INITIALLY DEFERRED
    TABLE ""dashboardBuckets"" CONSTRAINT "FK_ea16688a9b632de7758cf696b52" FOREIGN KEY ("prevId") REFERENCES "dashboardBuckets"(id) DEFERRABLE INITIALLY DEFERRED

我在这样的事务中更新nextId/prevId 字段:

      const qr = getConnection().createQueryRunner();
      await qr.connect();
      await qr.startTransaction();
      await qr.query("SET CONSTRAINTS ALL DEFERRED;");

      try 
        // Update bucket prev/next refs ------------------------------------ //
        // await getConnection().transaction(async (t) => 
        let next, prev, oldPrev, oldNext;
        if (prevId !== undefined && nextId !== undefined) 
          const repo = qr.manager.getRepository(BucketEntity);

          // olds
          const oldPrevId = bucket.prevId;
          const oldNextId = bucket.nextId;
          oldPrev =
            bucket.prevId === null
              ? null
              : await repo.findOne(bucket.prevId, 
                  relations: ["prev", "next"],
                );
          oldNext =
            bucket.nextId === null
              ? null
              : await repo.findOne(bucket.nextId, 
                  relations: ["prev", "next"],
                );

          // update
          bucket.prevId = prevId;
          bucket.nextId = nextId;

          if (prevId !== null) 
            prev =
              prevId === oldNextId
                ? oldNext
                : await repo.findOne(prevId, 
                    relations: ["prev", "next"],
                  );
            if (prev === undefined) throw new BucketDoesntExistError();
            if (prev !== null) prev.nextId = bucket.id;
          

          if (nextId !== null) 
            next =
              nextId === oldPrevId
                ? oldPrev
                : await repo.findOne(nextId, 
                    relations: ["prev", "next"],
                  );
            if (next === undefined) throw new BucketDoesntExistError();
            if (next !== null) next.prevId = bucket.id;
          

          // update oldPrev/oldNext
          if (oldPrev) 
            oldPrev.nextId = oldNextId;
          
          if (oldNext) 
            oldNext.prevId = oldPrevId;
          
        
        // ----------------------------------------------------------------- //
        if (oldNext) 
          await qr.manager.update(
            BucketEntity,
             id: oldNext.id ,
             prevId: oldNext.prevId, nextId: oldNext.nextId 
          );
        
        if (oldPrev) 
          await qr.manager.update(
            BucketEntity,
             id: oldPrev.id ,
             prevId: oldPrev.prevId, nextId: oldPrev.nextId 
          );
        
        if (next) 
          await qr.manager.update(
            BucketEntity,
             id: next.id ,
             prevId: next.prevId, nextId: next.nextId 
          );
        
        if (prev) 
          await qr.manager.update(
            BucketEntity,
             id: prev.id ,
             prevId: prev.prevId, nextId: prev.nextId 
          );
        

        await qr.manager.update(
          BucketEntity,
           id: bucket.id ,
           prevId: bucket.prevId, nextId: bucket.nextId 
        );

        await qr.commitTransaction();
       catch (error) 
        console.log("------------------------------------- error", error);
        await qr.rollbackTransaction();
       finally 
        await qr.release();
      

我收到以下错误:

error QueryFailedError: duplicate key value violates unique constraint "REL_ea16688a9b632de7758cf696b5"
    at new QueryFailedError (/Users/work/code/dashify-server/src/error/QueryFailedError.ts:9:9)
    at Query.callback (/Users/work/code/dashify-server/src/driver/postgres/PostgresQueryRunner.ts:220:30)
    at Query.handleError (/Users/work/code/dashify-server/node_modules/pg/lib/query.js:128:19)
    at Client._handleErrorMessage (/Users/work/code/dashify-server/node_modules/pg/lib/client.js:335:17)
    at Connection.emit (events.js:315:20)
    at Connection.EventEmitter.emit (domain.js:483:12)
    at /Users/work/code/dashify-server/node_modules/pg/lib/connection.js:115:12
    at Parser.parse (/Users/work/code/dashify-server/node_modules/pg-protocol/src/parser.ts:102:9)
    at Socket.<anonymous> (/Users/work/code/dashify-server/node_modules/pg-protocol/src/index.ts:7:48)
    at Socket.emit (events.js:315:20) 
  length: 237,
  severity: 'ERROR',
  code: '23505',
  detail: 'Key ("prevId")=(1) already exists.',
  hint: undefined,
  position: undefined,
  internalPosition: undefined,
  internalQuery: undefined,
  where: undefined,
  schema: 'public',
  table: 'dashboardBuckets',
  column: undefined,
  dataType: undefined,
  constraint: 'REL_ea16688a9b632de7758cf696b5',
  file: 'nbtinsert.c',
  line: '656',
  routine: '_bt_check_unique',
  query: 'UPDATE "dashboardBuckets" SET "prevId"=$1, "nextId"=$2 WHERE id=$3;',
  parameters: [ 1, null, 3 ]

这发生在具有延迟约束的事务中。难道不应该等到事务提交后再抛出错误,如果有任何重复?这发生在第一次更新查询之后。数据库是 postgresql。

关于我做错了什么有什么想法吗?

谢谢!

【问题讨论】:

【参考方案1】:

当我忘记在事务中运行我的命令之一时,我遇到了类似的情况,即在我应该使用的时候没有使用查询运行器管理器。

提示是在 SQL 日志中,我看到第二个 START TRANSACTION 命令嵌套在第一个命令中:

query: START TRANSACTION
query: INSERT INTO ...
query: INSERT INTO ...
query: START TRANSACTION
query: INSERT INTO ...
query: COMMIT
query failed: COMMIT

这导致初始事务中的记录在嵌套事务中不可用,从而导致 postgres 抛出约束错误。

【讨论】:

以上是关于TypeORM - 重复键值违反唯一约束(可延迟 fk)的主要内容,如果未能解决你的问题,请参考以下文章

IntegrityError:重复键值违反唯一约束

重复的键值违反了唯一约束,CakePHP

Golang:重复键值违反唯一约束

django:IntegrityError:重复键值违反唯一约束

Django 重复键值违反唯一约束错误模型表单

勺子插入 postgres 会产生“重复键值违反唯一约束”