📝

postgresql の insert on conflict で not null 制約エラーが起きる

2024/02/25に公開

概要

PostgreSQLのテーブルレコードを更新するために、INSERT ON CONFLICTを使おうと思った。
INSERT ON CONFLICTを使えばレコードがない場合でもある場合でも便利にデータが追加、更新できて良いと思った。
しかし、INSERT ON CONFLICTでは、更新の前にINSERTの制約チェックが入るため、NOT NULL制約を持つフィールドは必ず値を指定しないといけない事がわかった。
レコードがあればUPDATEを、ない場合はINSERTするしか無いっぽい。

少し詳細

以下のような仕様の関数を作ると便利かと考えた。

  • primary keyを指定すれば、そのprimary keyを持つレコードの値を更新できる。
    • この時レコードが存在する場合は任意のフィールドを更新することができる。
  • primary keyを指定しないか、なければレコードが追加できる。

素のSQLだとあまり有り難みがないが、ORMを使って任意のフィールドを動的に与えられる場合には便利だと考えた。
仕様はこんなイメージ

    def upsert_record_by_id(
        table: table_class, data: dict, id: Optional[UUID] = None
    ):
        """指定されたテーブルに対してupsertを実行します。
        idの設定があればそのレコードを、ない場合は新規にinsertします。

        Args:
            table (table_class): 操作するテーブルのクラス。
            data (dict): テーブルに設定するデータ。
            id (Optional[UUID]): レコードのid。更新したい場合は指定する。
        """
        ...

SQLで試すと以下のような感じ。

-- INIT database
DROP TABLE IF EXISTS Product;
CREATE TABLE Product (
  ProductID SERIAL PRIMARY KEY,
  Name VARCHAR(100) NOT NULL,
  Description VARCHAR(255)
);

-- Insert a first record.
INSERT INTO Product(Name, Description) VALUES ('book','a book for IT.');

-- Update a record. SUCCESS 
-- Descriptionだけ更新したいがNOT NULL制約を持つNameも指定しないといけない
INSERT INTO Product(ProductID, Name, Description) VALUES (1, 'book', 'a book for Cooking.') 
ON CONFLICT(ProductID) DO UPDATE SET Description=Excluded.Description;

-- Update a record. FAILURE
-- Nameを指定しない場合は、 「SQLエラー [23502]: ERROR: null value in column "name" of relation "product" violates not-null constraint」となる。
INSERT INTO Product(ProductID, Description) VALUES (1, 'a book for Cooking.') 
ON CONFLICT(ProductID) DO UPDATE SET (Name, Description)=(Excluded.Name, Excluded.Description);


-- QUERY database
SELECT * FROM Product;

参考
PostgreSQL: Re: BUG #16706: insert into on conflict(pk) do update error violates not-null constraint

I don't think that would work, because the not null constraint is checked before the INSERT, not the UPDATE.

Discussion