PythonでSQLiteを使ってみる（DB4S・SQLAlchemy）

本格的にはDB用意しなくていいんだけど、ちょこっと作業したり実験したりする際に少し使いたいケースが出てきたので、PythonでのSQLite周りを調べてみました。

以下のようなライトなユースケースです。

> 毎回最初から最後までスキャンしたり書き直したりするのは避けたかったのでCSVを使用しませんでした。MySQLを使用した方が良かったのかもしれませんが、厳しい期限があったので迅速にプログラムを完成するには、SQLサーバを追加設定する時間はありませんでした。この場合、SQLiteで十分でした。
> [Pythonや機械学習、そして言語の競争について – 極めて主観的な見地から](https://postd.cc/python-machine-learning-and-language-wars-a-highly-subjective-point-of-view/)

（他の言語では使っていたことがあるものの、Pythonでは初です）
なお、ベタのクエリよりもORMをメインに扱っていきます。

# 使うもの

- Python3.6
- Windows10
- [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
- [DB Browser for SQLite](https://github.com/sqlitebrowser/sqlitebrowser)
- Jupyter Notebook

# インストール

SQLite自体は最初からPythonで使えるようです。
また、SQLAlchemyに関しても、Anacondaであれば最初から入っている？ようです（今回使うPython環境ではv1.3.1が入っていました）。


```
$ pip install SQLAlchemy
```

```
Requirement already satisfied: SQLAlchemy in c:\users\****\anaconda3\envs\****\lib\site-packages (1.3.1)
```

また、内容の確認などのためのクライアントとしてDB Browser for SQLiteをインストールしておきます。Windows版は以下からインストーラーを落とせられます。

https://sqlitebrowser.org/dl/#windows

インストールしてPCを再起動（必要な場合）した後に、起動してみると以下のようなGUIが表示されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/a4596978-a727-06f4-3d29-908eb445b5f6.png)

# DB生成と接続

Jupyter上で、DB接続などを進めてみましょう。
また、その際にモデルを1つ試しに設定しておきます。

まずは必要なモジュールのimport。

```py
import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String
from sqlalchemy.orm import sessionmaker
```

DBファイルへの接続周りの処理を流します。
第一引数にファイルのパスを指定します。パスの先頭にsqlite:///という特殊な記述が必要になります。スラッシュが3本必要な点も注意してください。

第二引数にTrueを指定すると、モデルでの操作時のクエリのログなどがコンソール出力されるようになります。直接クエリを書いていないと、どんなクエリが流れているのか分かりづらい時があるため、今回はTrueで進めます。

```py
engine = sqlalchemy.create_engine('sqlite:///sample_db.sqlite3', echo=True)
```

サンプルにモデルを一つ用意します。基底クラスはdeclarative_base経由で用意するようです。

```py
Base = declarative_base()
```

先ほど用意した基底クラスを継承して、Fruitというモデルを用意しました。
Djangoとかだと、IntegerFieldといったように型に応じたクラスを指定しますが、SQLAlchemyだとColumnクラスを指定して第一引数に型（Integerなど）を指定する形式のようです。

主キー指定のprimary_keyなどはDjangoと同様、max_length的な最大文字数の指定などはlengthと引数が該当するようです。
また、DjangoでのMetaなどによるテーブル名などの指定は、アンダースコア2個の属性を指定する形で設定します。

```py
class Fruit(Base):
    
    id = Column(Integer, primary_key=True)
    name = Column(String(length=255))
    
    __tablename__ = 'fruit'
```

モデルを用意したら、基底クラスのmetadata.create_allメソッドを実行すると、まだ未生成のテーブルが生成されファイルに書き込まれます。

```py
Base.metadata.create_all(bind=engine)
```

今回は初回なので用意したモデルに対してCREATE文が走っていることが分かります。

![20190505_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/6328c7bb-5d59-7d4f-d869-bf6863e4f4d5.png)

一度カーネルを再起動してまた同じコードを流してみると、今度はすでにテーブルが生成されていないのでCREATE文は流れません。

![20190505_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/04b9c10f-bd48-065c-9308-857d1fe52781.png)

また、指定した名前でSQLite用のファイルが生成されていることが分かります。

![20190505_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/766b7806-da83-ccb1-4aa3-51b8794d2bba.png)

DB Browser for SQLiteのOpen Databaseを選択して内容を確認してみます。

fruitテーブルが正常に生成されているようです。

![20190505_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/2dad01f7-2814-436b-1178-b61c634ffbef.png)

# Insert

用意したモデルのクラスのオブジェクトを作って、値を設定して、addメソッドを呼んで、commitする、馴染みのある流れのようです。

```py
session = sessionmaker(bind=engine)()
apple = Fruit()
apple.id = 3
apple.name = 'apple'
session.add(instance=apple)
session.commit()
```

Browse Dataのタブを見てみると、正常に値が追加されていることが分かります。

![20190525_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/7f2cbfac-f308-e96a-efed-6b51ec769d97.png)


複数挿入する場合にはadd_allを使います。

```py
session.add_all(instances=[
    Fruit(id=4, name='orange'),
    Fruit(id=5, name='melon'),
])
session.commit()
```

![20190525_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/3a53f04c-cf87-85f7-05e9-1733d780cd7f.png)

# SELECT

queryメソッドで第一引数に対象のモデルを指定するとiterableオブジェクトが返ってくるので、それをループで回したりしてアクセスします。

```py
query_result = session.query(Fruit)
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
3 apple
4 orange
5 melon
```

## 条件指定

単純な一致による条件を指定する際にはfilter_byをメソッドチェーンで繋げていきます。

```py
apples = session.query(Fruit).filter_by(name='apple')
for apple in apples:
    print(apple.id, apple.name)
```

```
3 apple
```

Not equal的な指定をしたい際にはfilterメソッドで、引数に条件を書きます。

```py
query_result = session.query(Fruit).filter(Fruit.name != 'apple')
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
4 orange
5 melon
```

大なり小なり的な指定も同様にできます。

```py
query_result = session.query(Fruit).filter(Fruit.id >= 4)
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
4 orange
5 melon
```

複数の条件を指定する際には、条件単位で()の括弧で囲んで、ORの場合は|、ANDの場合は&を記載します。Pandasと同じ書き方ですね。

```py
query_result = session.query(Fruit).filter((Fruit.id == 3) | (Fruit.id == 4))
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
3 apple
4 orange
```

```py
query_result = session.query(Fruit).filter((Fruit.id == 3) & (Fruit.name == 'apple'))
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
3 apple
```

部分一致では、モデルのカラム部分にlikeの関数を挟みます。

```py
query_result = session.query(Fruit).filter(Fruit.name.like('%a%'))
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
3 apple
4 orange
```

## １件のみ取得

さらにoneメソッドを繋げると、1件のみの取得となり、ループを書かずに済みます。

```py
apple = session.query(Fruit).filter_by(name='apple').one()
print(apple.id, apple.name)
```

## 取得件数制限

1件ではなく、件数の制限にはlimitメソッドを加えていきます。

```py
query_result = session.query(Fruit).limit(2)
for fruit in query_result:
    print(fruit.id, fruit.name)
```

## 昇順・降順の指定

order_byメソッドを加えて、引数にモデルとそのカラムを指定します。カラムはさらに昇順のascや降順のdescなどの関数を持つので、必要なものを指定します。

```py
query_result = session.query(Fruit).order_by(Fruit.id.desc()).limit(2)
for fruit in query_result:
    print(fruit.id, fruit.name)
```

```
5 melon
4 orange
```

# Update

更新対象のオブジェクトをqueryで取得して、変更が必要な属性を更新してcommitします。どうやらupdate的な関数は実行しなくていい・・・模様です。

```py
apple = session.query(Fruit).filter_by(id=3).one()
apple.name = 'apple 2
session.commit()
```

![20190525_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/228778/a6c87e96-5b3d-9aec-6662-d8e9fd7ca16f.png)

## Delete

レコードを削除する際には、対象をqueryメソッドなどで選択してから、deleteを呼び出します。

```py
orange = session.query(Fruit).filter_by(id=4).one()
session.delete(orange)
session.commit()
```

※DB Browser for SQLiteで開いている状態では、ロックされていて削除ができませんでした、といったようなエラーが出てきました。アプリを閉じたら問題なく削除できました。
開きながら削除できないっぽい・・？

## ロールバック

なんらか処理に失敗すると、以下のようなエラーが出ることがありました。

```
InvalidRequestError: This Session's transaction has been rolled back due to a previous exception during flush. To begin a new transaction with this Session, first issue Session.rollback(). 
```

そういった場合は一度rollbackメソッドを流してあげると操作ができるようになります。

```py
session.rollback()
```

# 切断

処理が終わったらcloeseメソッドでセッションを閉じます。


```py
session.close()
```

---

とりあえず、基本的なところを動かしつつメモしてみましたが、馴染みのあるAPIで、DjangoやMySQLなどに慣れている方なら特に違和感なく使えるのでは・・・という印象です。

# 参考文献

- [DB Browser for SQLite](https://github.com/sqlitebrowser/sqlitebrowser)
- [Pythonや機械学習、そして言語の競争について – 極めて主観的な見地から](https://postd.cc/python-machine-learning-and-language-wars-a-highly-subjective-point-of-view/)
- [SQLAlchemy](https://github.com/sqlalchemy/sqlalchemy)
- [Python3 の 定番ORM 「 SQLAlchemy 」で MySQL ・ SQLite 等を操作 – 導入からサンプルコード](https://it-engineer-lab.com/archives/1183)