IntegrityError в pysqlite: отличить "NOT NULL" от нарушения "UNIQUE"

В писклите, нарушая NOT NULL или UNIQUE ограничение также вызывает ошибку IntegrityError. К сожалению, этот тип исключения не предоставляет код ошибки, а только сообщение.

Итак, допустим, я хочу игнорировать нарушения уникальных ограничений, потому что я знаю, что это безопасно для данных, но следует сообщать о пустых значениях в ключевых столбцах.

Я придумал следующее решение:

con = sqlite3.connect(':MEMORY:')
con.execute('''CREATE TABLE ABCD (A TEXT NOT NULL,
                                  B TEXT NOT NULL,
                                  C TEXT NOT NULL,
                                  D TEXT NOT NULL,
                                  PRIMARY KEY (A, B))''')
with con:
    for a, b, c, d in inputs:
        try:
            con.execute('INSERT INTO ABCD VALUES (?, ?, ?, ?)',
                        (a, b, c, d))
        except sqlite3.IntegrityError as e:
            # Skip 'not unique' errors, but raise others.
            if not e.message.endswith('not unique'):
                raise
con.close()

Однако анализ сообщения об ошибке кажется неправильным и может быть ненадежным. Есть ли лучший способ сделать это, может быть, даже используя con.executemany()?

3 ответа

Решение

Это то, что я в итоге сделал:

con = sqlite3.connect(':MEMORY:')
con.execute('''CREATE TABLE ABCD (A TEXT NOT NULL,
                                  B TEXT NOT NULL,
                                  C TEXT NOT NULL,
                                  D TEXT NOT NULL,
                                  PRIMARY KEY (A, B))''')
with con:
    for a, b, c, d in inputs:
        if any(elem is None for elem in (a, b, c, d)):
            raise ValueError('Fields must not be empty.')
        con.execute('INSERT OR IGNORE INTO ABCD VALUES (?, ?, ?, ?)',
                    (a, b, c, d))
con.close()

Таким образом, пустые значения отлавливаются "вручную" перед выполнением операции с БД. Если какая-либо ошибка возникает во время execute (например, нарушение UNIQUE ограничение), запись пропускается. Обратите внимание, что INSERT OR IGNORE не означает игнорирование ограничения уникальности, а скорее игнорирование (то есть пропуск) входной строки.

Недостатком этого решения является то, что проверка пустых значений выполняется дважды. Я думаю, что это не так уж и плохо, так как это, вероятно, довольно дешевая операция. Я думаю, что это все же чище, чем анализ сообщения об ошибке, и, вероятно, более устойчивым к изменениям (например, обновление pysqlite, которое может изменить некоторые детали в сообщении об ошибке).

Кредиты: идея возникла из обсуждения с Лутцем. Это было также независимо предложено Мартийном.

Более элегантное решение - полностью полагаться на функциональность SQL(ite). Указав условие конфликта для первичного ключа (ON CONFLICT IGNORE), желаемое поведение уже достигнуто:

con = sqlite3.connect(':memory:')
con.execute('''CREATE TABLE ABCD (A TEXT NOT NULL,
                                  B TEXT NOT NULL,
                                  C TEXT NOT NULL,
                                  D TEXT NOT NULL,
                                  PRIMARY KEY (A, B) ON CONFLICT IGNORE)''')

Таким образом, повторяющиеся строки (которые нарушают ограничение уникальности первичного ключа) пропускаются без вывода сообщений, в то время как нулевые значения вызывают прерывание (что приводит к sqlite3 исключение). Все это достигается без предварительной фильтрации данных по значениям Null/None или с сообщениями об ошибках sqlite3 API. Теперь мы можем просто позвонить con.executemany(), без дальнейших церемоний:

with con:
    con.executemany('INSERT INTO ABCD VALUES (?, ?, ?, ?)', inputs)

Ниже приведен рабочий код:

import sqlite3

con = sqlite3.connect(':memory:')
con.execute('''CREATE TABLE ABCD (A TEXT NOT NULL,
                                  B TEXT NOT NULL,
                                  C TEXT NOT NULL,
                                  D TEXT NOT NULL,
                                  PRIMARY KEY (A, B));''')

inputs = [('cow', 'pig', 'cat', 'dog'), ('cow', 'pig', 'quail', 'turkey')]
with con:
    for a, b, c, d in inputs:
        try:
            con.execute('INSERT INTO ABCD VALUES (?, ?, ?, ?);',
                        (a, b, c, d))
        except sqlite3.IntegrityError as e:
            if 'not null' in e.args[0].lower():
                print('There is a NULL value')
            elif 'unique constraint' in e.args[0].lower():
                print('There is unique violation')
            else:
                raise

Тестовое задание:

>>> 
There is a NULL value
>>> 

Результат второго теста:

>>> 
There is unique violation
>>> 

Надежды, могут помочь вам.

Другие вопросы по тегам