SQLite3 公開日時とURLで一意チェックする(表制約)
表制約で複数列の一意チェックをする
成果物
情報源
- https://www.sqlitetutorial.net/sqlite-unique-constraint/
- https://docs.python.org/ja/3.5/library/sqlite3.html#exceptions
- https://sqlite.org/lang_conflict.html
- https://www.sqlite.org/lang_insert.html
テーブル定義
表制約(unique
)
create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす );
UNIQUE(published,url)
を追記した。
制約に名前を付けることもできる。
create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト CONSTRAINT cstr_unique UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす );
だが、SQLite3ではalter table drop constraint
コマンドをサポートしていない。名前をつける意味はない。pragma index_info('制約名')
, pragma table_info('表名')
でも情報を出せない。
コンフリクト
UNIQUE(published,url) on conflict ...
を追記しても良かったが、どうすべきか迷ったのでやめた。
rollback
, abort
, fail
, ignore
, replace
の5つ。使うならignore
か。制約違反のステートメントをスキップして続行する。
create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) on conflict ignore -- 公開日時とURLの両方が被ると同一とみなす );
もし公開日時の降順にソートされてinsert
されるなら、DB内レコードと重複した時点で古いニュースと判断できる。よって、強制終了するのがよい。スキップして次に移っても、やはり重複してスキップ対象だろうから。
だが、これはソートされていることが前提。もしソートされていなければ1件ずつ対応すべきだろう。念の為、1件ずつが安全か。
また、ignore
では無視されてエラーが表示されない。一意制約違反があったことがわかったほうが良いときもあるだろう。そのときはfail
を使うことになる。問題は、エラー表示のON/OFFを切り替えられないことにある。
insert
時のコンフリクト
エラー表示のON/OFFを切り替えるには、insert
文を動的に生成し、on conflict
の対応をignore
/fail
で切り替えれば良いだろう。
class NewsDb: ... def __insert_sql(self, conflict=None): return ('insert into news(published,url,title,body) values(?,?,?,?)' + '' (conflict is None) 'on conflict ' + conflict) ...
conflict
がないときは無駄な結合が生じる。レコード数分もconflict
の値は確認すべき
例外
一意制約違反のときはSQLITE_CONSTRAINT
を返す。
conflict | 意味 | 戻り値 |
---|---|---|
rollback |
ロールバック | SQLITE_CONSTRAINT |
abort |
中断 | SQLITE_CONSTRAINT |
fail |
失敗 | SQLITE_CONSTRAINT |
ignore |
無視 | - |
replace |
上書き | - |
今回の一意制約違反のときは、ロールバックすべきでない。
insert ... on conflict ignore
にすることで例外発生を回避できるだろう。fail
だと例外発生するだろう。もし、例外時は無条件でrollback
しているなら、一意違反でないレコードまでロールバックされてしまう。
どんな例外が発生するのか? sqlite3.IntegrityError
だった。
コード
例外
test.py
import sqlite3 conn = sqlite3.connect(':memory:') cur = conn.cursor() sql = ''' create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす ); insert into news(published, url) values('0','A'); insert into news(published, url) values('0','B'); insert into news(published, url) values('0','A'); insert into news(published, url) values('1','A'); insert into news(published, url) values('1','B'); ''' cur.executescript(sql)
$ python3 test.py Traceback (most recent call last): File "test.py", line 19, in <module> cur.executescript(sql) sqlite3.IntegrityError: UNIQUE constraint failed: news.published, news.url
fail
import sqlite3 conn = sqlite3.connect(':memory:') cur = conn.cursor() sql = ''' create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす ); insert into news(published, url) values('0','A'); insert into news(published, url) values('0','B'); insert or fail into news(published, url) values('0','A'); insert into news(published, url) values('1','A'); insert into news(published, url) values('1','B'); ''' try: cur.executescript(sql) finally: print(cur.execute('select count(*) from news;').fetchone()[0])
2 Traceback (most recent call last): File "test0.py", line 19, in <module> try: cur.executescript(sql) sqlite3.IntegrityError: UNIQUE constraint failed: news.published, news.url
一意エラーが発生する前までのinsert
は挿入された。だが、一意エラー以降のinsert
はは実行されなかった。published
順にソートされた状態でinsert
されるなら、これでも問題ない。なにせ既存と重複したなら、それ以降は古いニュースだから不要。
ignore
test1.py
import sqlite3 conn = sqlite3.connect(':memory:') cur = conn.cursor() sql = ''' create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす ); insert into news(published, url) values('0','A'); insert into news(published, url) values('0','B'); insert or ignore into news(published, url) values('0','A'); insert into news(published, url) values('1','A'); insert into news(published, url) values('1','B'); ''' try: cur.executescript(sql) finally: print(cur.execute('select count(*) from news;').fetchone()[0])
python3 test1.py
4
一意エラー箇所だけ無視されて、他はすべてinsert
された。
一意制約エラーのとき以外はロールバックする
test2.py
import sqlite3 conn = sqlite3.connect(':memory:') cur = conn.cursor() sql = ''' create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす ); insert into news(published, url) values('0','A'); insert into news(published, url) values('0','B'); insert or fail into news(published, url) values('0','A'); insert into news(published, url) values('1','A'); insert into news(published, url) values('1','B'); ''' try: cur.executescript(sql) except sqlite3.IntegrityError: # おそらく制約違反 import traceback traceback.print_exc() except: # それ以外 import traceback traceback.print_exc() self.conn.rollback() # ロールバックする finally: print(cur.execute('select count(*) from news;').fetchone()[0])
Traceback (most recent call last): File "test2.py", line 19, in <module> try: cur.executescript(sql) sqlite3.IntegrityError: UNIQUE constraint failed: news.published, news.url 2
これで今回つくった一意制約エラーのときはロールバックせず、それ以外の例外のときはロールバックするようになった。
公式文書によると以下のようなエラーがある。
Error | 意味 |
---|---|
sqlite3.Warning |
警告 |
sqlite3.Error |
このモジュールにおける他の例外クラスの基底クラス |
sqlite3.DatabaseError |
データベースに関連するエラーに対して発生した例外 |
sqlite3.IntegrityError |
データベースのリレーショナル整合性が影響を受けるときに発生する例外 |
sqlite3.ProgrammingError |
プログラミングエラーに対して発生する例外(テーブルが既存・見つからない、SQL構文エラー、指定されたパラメーターの数が間違っている等) |
どうやら細かい切り分けはできていなさそう。たとえば外部エラーなど他の制約エラーが起こったときもsqlite3.IntegrityError
が発生する。今回の一意制約違反である場合だけにしたいのだが、どうすればいい?
一意制約エラーのとき以外はロールバックする2
test3.py
except sqlite3.IntegrityError as err_sql_integ: # おそらく制約違反 import traceback import sys msg = str(err_sql_integ.with_traceback(sys.exc_info()[2])) traceback.print_exc() if 'UNIQUE' in msg and 'published' in msg and 'url' in msg: pass else: self.conn.rollback() # ロールバックする
他のIntegrityError
のときはロールバックする。
でも、メッセージの内容ってSQLite3の実装や、テーブル定義によって変わってしまうのでは?
たとえば表制約に名前をつけたとき、メッセージ内容が変わってしまわないか?
create table if not exists news( id integer primary key, published text, url text, title text, body text, -- URL先から本文だけを抽出したプレーンテキスト CONSTRAINT cstr_unique UNIQUE(published,url) -- 公開日時とURLの両方が被ると同一とみなす );
sqlite3.IntegrityError: UNIQUE constraint failed: news.published, news.url
大丈夫だった。制約名の有無にかかわらず、エラーメッセージは同じ。
でも、バージョンによる違いとかあるかもしれない。でも、たぶんUNIQUE
,published
,url
の3単語に絞ったから大丈夫だろう。
結局どうする?
try: 挿入処理(`insert or fail ...`) except sqlite3.IntegrityError as err_sql_integ: # おそらく制約違反 import traceback import sys msg = str(err_sql_integ.with_traceback(sys.exc_info()[2])) traceback.print_exc() if 'UNIQUE' in msg and 'published' in msg and 'url' in msg: pass else: self.conn.rollback() # ロールバックする except: # それ以外 import traceback traceback.print_exc() self.conn.rollback() # ロールバックする
挿入データはpublished
の降順にソートされているものとする。
所感
すごい汚いコード。エラー条件分岐をメッセージのテキストで行うとか、クソすぎて不安しかない。あと、except
の内容かぶってる。
対象環境
- Raspbierry pi 3 Model B+
- Raspbian stretch 9.0 2018-11-13 ※
- bash 4.4.12(1)-release ※
- SQLite 3.29.0 ※
- MeCab 0.996ユーザ辞書
$ uname -a Linux raspberrypi 4.19.42-v7+ #1218 SMP Tue May 14 00:48:17 BST 2019 armv7l GNU/Linux
前回まで
- NewsApiを使ってみた
- NewsApiでカテゴリ別にニュースを取得する
- NewsApiで得たニュースを重複なく取り込む方法を考える
- NewsApiで得たニュースを保存するSQLite3テーブルを考える
- NewsApiのJSONからSQLite3DBファイルへ挿入する
- SQLite3に登録済みのNewsApiデータから最新を取得する
- NewsApiのJSONからSQLite3DBファイルへ挿入する(未登録のみ)
- HTMLから本文テキストだけを抽出したい(python-extractcontent)
- HTMLから本文を抽出してフォーマットする(改行+全角スペース)
- NewsApiのURLから本文を抽出してSQLite3に挿入する
- ニュースサイトを探す
- PythonでRSSを取得する(feedparser)
- PythonでのWebスクレイピング環境構築(chromium-driver,selenium,beautifulsoup4)
- PythonでRSSからHTMLの本文を抽出してSQLite3に挿入する(重複してしまう版)
- Pythonでソート(複数キーでdescとasc混在)
- Pythonで二分探索する
- PythonでRSSからHTMLの本文を抽出してSQLite3に挿入する(未登録のみ挿入する)