RecycleView攻略 4日目 sqlite3との連携

# はじめに

RVは大量のデータを表示する為のwidgetです。となればそれにdatabase内のデータを映したいと考えるのは自然な事だと思います。実際にdatabaseとしてsqlite3を使ってみます。

```python
import sqlite3
conn = sqlite3.connect(...)
cur = conn.cursor()
rv = ...
rv.data = cur.execute("SELECT * FROM 何かの表")
```

```
AttributeError: 'tuple' object has no attribute 'get'
```

`cur.execute()`はdatabase検索結果を取り出すためのiteratorを返す[^1]のですが、そのiteratorから取り出せるのはtupleであるため、辞書のiterableを期待している`rv.data`へは代入できず上記のように怒られます。それ故に普通はRVに渡す際に以下のような変換が生じます。

```python
rv.data = ({'鍵1': 値1, '鍵2': 値2,} for 値1, 値2 in cur.execute("SELECT * FROM 何かの表"))
```

**実際に速度を測ったわけではないのですが(←重要)**このような変換が生じるのが気に入らない(?)ので、この投稿ではどうにかしてそれを無くせないか考えてみます。

# sqlite3.Rowの場合

sqlite3は検索結果の行を辞書のように扱えるようにしてくれる手段が存在します。それが[sqlite3.Row](https://docs.python.org/ja/3/library/sqlite3.html#row-objects)です。

```python
conn.row_factory = sqlite3.Row
cur = conn.cursor()
for row in cur.execute("SELECT * FROM 何かの表"):
   print(row['列名'])
```

このように表の列名を鍵として値を取り出せるようになります。しかしながら実際にこれを`rv.data`に代入すると

```
AttributeError: 'sqlite3.Row' object has no attribute 'get'
```

という風に怒られてしまいました。どうやら`sqlite3.Row`は辞書と同じmethodを全て備えているわけではなさそうです。なので次は派生classを用意して足りないmethodを補ってあげてみます。

# sqlite3.Rowを拡張した場合

最初に思い浮かぶのは次のような派生classです。

```python
class RVCompatibleRow(sqlite3.Row):
    def get(self, key, default_value):
        return self[key]

conn.row_factory = RVCompatibleRow
```

するとどうなったかというと

```
(tracebackも一部載せている)
   File "略/.venv/lib/python3.8/site-packages/kivy/uix/recyclelayout.py", line 154, in compute_sizes_from_data
     ph = item.get('pos_hint', ph)
   File "略/test_rv_sqlite3.py", line 41, in get
     return self[key]
 IndexError: No item with that key
```

という風に怒られました。実は`ph = item.get('pos_hint', ph)`は一番最初に`AttributeError`が出たときからあったtracebackなんですが、RVはこのように`pos_hint`や`size_hint`のようなwidgetの配置に関わる情報が辞書に含まれている場合に何らかの特別な処理を行っています。なので`RVCompatibleRow`を次のように修正しました。

```python
class RVCompatibleRow(sqlite3.Row):
    def get(self, key, default_value):
        return default_value if key not in self.keys() else self[key]
```

ただしdatabaseにwidgetの配置に関わる情報、つまりはデータの表示の仕方に関わる情報を含める状況は限られると思うので、もし自分自身で`.get()`を使わないのであればこれは

```python
class RVCompatibleRow(sqlite3.Row):
    def get(self, key, default_value):
        return default_value
```

としてしまっても良い気がします、少しでも速くするために。

次に出てきたerrorは

```
AttributeError: 'RVCompatibleRow' object has no attribute 'items'
```

です。というわけで`.items()`も実装してあげましょう。

```python
class RVCompatibleRow(sqlite3.Row):
    # 実装その一 : 列名による検索が無いので速いかもしれない(実際に測ってはいない)
    def items(self):
        return zip(self.keys(), self)

    # 実装そのニ
    # def items(self):
    #     for key in self.keys():
    #         yield (key, self[key])
```

**すると無事databaseの検索結果を映すことができました**。
![Screenshot at 2021-09-20 21-57-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144673/9c325742-33b3-be88-3589-d8de21aeaecf.png)

# 終わりに

いきなり上のscreenshotを見せられても意味不明ですね。databaseの中身が知りたい人は[ソースコード](https://gist.github.com/gottadiveintopython/e7d6159868b2874bec74a67245aca0ca)を覗いてください。とにかく今回の検証によってsqlite3の検索結果の列名と同じ名前のpropertyを持つviewclassを用意するだけで検索結果をRVに映せるとわかりました。

話は逸れますがSQLiteはC言語で書かれているのでPythonプログラムがCの計算能力を得られるというのは勿論なんですが

* コード中にSQLが現れることでdatabaseを触っている事が視覚的に分かりやすくなってコードが読みやすくなる
* アプリケーションの設定ファイルとして使うと自分で設計する手間が省ける

とかもかなり大きいですね。最初は嫌っていたのですが今は気に入っています。 m(_ _)m


[^1]: `cur`自体がiteratorで`cur.execute()`は`cur`その物を返す