続 SQLite3 + JSON (+ Python)

# 続 SQLite3 + JSON

ちょっと実用的な情報が少ないと感じたのでせめて使えるレベルまでには持ってゆきたいと。
そう思いつつも、実際は情報をまとめている方々はちらほら見当たるのだけれどもその目的によってかなり使い方が異なるなーなどと、自分なりにまとめておかないとなと思った次第で。
（それくらいの努力はしたくなる程度に魅力的なのがJSON対応）

> フリーダムなEXCELシートをよく目にする機会があるとか、ペーパーレスって何ですか？おいしいものですか？　みたいな現場とか案件とか抱えていたり抱えそうになっていたりというあなたには絶対役立つソリューション。> **SQLite3 + JSON**



## プライマリーキー問題 +

これは設計というよりは運用の話。
以前のポストにも書いた通りでレコードのJSON文字列の中の一要素に絞ってキーを指定するなどという芸当は出来なさそうなのでプライマリキーだけは列を分けると便利。だけど、だからと言ってJSON文字列にそのキーが含まれていないとこれまたスーパー不便なのでプライマリキー列とJSON文字列両方にプライマリキーのデータが入っていると便利。

```sql
create table if not exists "{tablename}" 
(
    "id" INTEGER, 
    "jsondata" TEXT, 
    PRIMARY KEY("id" AUTOINCREMENT)
)
```

このようなテーブル設計でプライマリキーを分ける。

データ的に次のようにしたい場合、

| id   | jsondata                              |
| ---- | ------------------------------------- |
| 0    | '{"invoice": 123}, {"quantity": 100}' |

敢えて 以下の様に **id**を**jsondata**の中にも入れる。

| id   | jsondata                                        |
| ---- | ----------------------------------------------- |
| 0    | '{"id": 0}, "invoice": 123}, {"quantity": 100}' |

データが重複していてとてもキモい。キモいけどこれでものすごい楽になる。

でもあまりにもキモいのでせめて **jsondata** の中の **"id"** がプライマリキーに同期するようにしたい場合は以下のように参照する。

```sql
select 
	json_set(jsondata, "$.id", id) as row 
from {tablename}
```

※キモいだけではなくデータ量の話とかにもつながってくるので実運用ではJSON列に挿入したidがDB上のJSON列に残らないようにした方が良さげ。



## おさらいの JSON → 列

列はバラバラではなくレコード一行づつJSONとして返してくれたほうが楽なのだけど、どっちにしてもWhere句とかでは必要になるので **json_extract** は必須。

```sql
json_extract(<列名>, "$.<JSONのキー>")
```

具体的にはこんな風

```sql
select * from SHIPPING
where json_extract(jsondata, "$.invoice") = 123
```



### そして脇道

しかし、JSONの入っている列は、SQLite的にはただのテキストの列。つまり…。

```sql
select * from SHIPPING
where jsondata like '%invoice": 123%'
```

等ということが出来、例えばキーに対する値がしっかり意味を成している（かつ状態がユニークな）場合、列をまたいでデータを検索できることになるのでちょっと面白い。*（あまり褒められた使い方ではない）*

> 例えば「エラー」という文字がどこかに入っているレコードを探す。等。



## 部分更新

JSON列を更新したい場合、一度取り出して プライマリキーを指定して。という手順で更新。
で、JSONの一部項目だけを指定して更新したいなどの場合にどうすんだってとこ。
※さすがにselectしてupdateしてみたいなのを繰り返すとハチャメチャ遅い。

```sql
replace into {tablename}
select id, json_patch(jsondata, '{data}') 
from jsontable
where id={dataid}
```

要するに プライマリキーで引っ張りだしたJSONに新たに追加（または更新）したいJSON文字列をマージしてアップデートしてやるという感じ。

この手順を守っていればもともと入っていたデータがどこかに旅立ってしまった等の事故は防げるはず。



## Python向けクラスの修正

で、以上を踏まえて前回作ったデータベースクラスのメソッド(Python)の面倒も見ておく。

**旧**

```python
##    def get(self, tablename, where={}, condition="and", column="jsondata"):
##        sql = f"select * from {tablename}"
##        sql = self.queryString(sql, tablename, where, condition, column)
##        for row in self.fetch(sql):
##            yield {"id": row[0], "data": json.loads(row[1])}

##    def set(self, tablename, data={}, dataid="NULL"):
##        data = str(data).replace("'", '"')
##        sql = f"""replace into {tablename} values({dataid}, '{data}')"""
##        self.query(sql)
```

**新**

```python
    def get(self, tablename, where={}, condition="and", column="jsondata"):
        sql = f"""select json_set({column}, "$.id", id) from {tablename}"""
        sql = self.queryString(sql, tablename, where, condition, column)
        for row in self.fetch(sql):
            yield row[0]
            
    def set(self, tablename, data={}, where={}, dataid="NULL", condition="and", column="jsondata"):
        if "id" in data:
            del data["id"]
        data = str(data).replace("'", '"')
        if len(where) == 0 and dataid == "NULL":
            sql = f"""replace into {tablename} values({dataid}, '{data}')"""
        else:
            sql = f"""replace into {tablename} select id, json_patch({column}, '{data}') """
            sql += f"""from {tablename}"""
        if dataid != "NULL":
            sql = " where ".join((sql, f"id={dataid}"))
        else:
            sql = self.queryString(sql, tablename, where, condition, column)
        self.query(sql)
```



**set** はこのような感じで使える。

```python
with database.JsonDB("jsondb.db") as db:
	db.set("names", data={"name": "json", "age": 4}) # 新規で挿入
	db.set("names", data={"delete": "False", "name": "JSON"}, where={"name": "json"}) # 更新
```



条件を指定しない場合新規レコードに登録される手前、全件を更新するには別途 **update** のメソッドが必要かと思う。
けれど、思い返せど思い返せどDBに対してそんな更新をかけた記憶がさっぱり無いのでレアケースとして今のところは実装を見送っておく。