Docker-ComposeでFlask + SQLAlchemy+ MySQLのローカル開発環境を作る

#前回のあらすじ
###FlaskローカルサーバでHello Worldした
前回記事：[Python初心者がFlaskでLINEbotを作ることになった（Flaskざっくり解説編）](https://qiita.com/suwa-andante/items/2e31e00f9b1ac7a4c375)

###オウム返しbotを作ってみた（割愛）
以下の記事を参考に、オウム返しbotをHerokuに作り、HerokuとLINE bot SDKは何となく使えるようになりました。
LINEbotの大枠は、オウム返しbotを作ってみると理解できると思います。
とてもわかりやすい記事が数多くあるので、この部分については省略します。

参考：　[Pythonでオウム返しをするLine botを作成](https://qiita.com/Dexctersu/items/2243db615a1be7186acc)


#次はDB接続だ！
ただ言われたことを返すだけでは、ちょっとつまらない。
特定のワードに、対応した返答を返してほしい。
プログラムの場合分けだけでは限界があるので、やはりデータベースは使いたい！

##botは「セッション」という考え方がない・・・？
例えば、
「今日の夕飯は？」→『カレーライスです』→「辛い？」→『激辛です』
みたいに、前の会話を元にして次の会話に進めたら、それっぽいですよね。

前のカレーライスの会話がなければ、急に「辛い？」なんて聞かれても『つらい、ですか？？？』とかなってしまいそうです。
Webであれば、ログイン情報や入力データはCookieやセッションなどに持たせることができますが、botはブラウザアクセスではないので、これができません。

##会話状況やユーザーを確認するには、DBが不可欠
LINEから送られてくるのは、ユーザIDとメッセージ、リプライトークンくらいなので、
ユーザIDをキーとして、「今、何について話しているのか」をサーバ側で保存しておかないといけないわけです。
どうやって現在の話題を記録するか、についてはまた後程考えることにして、
とにかくデータベースが使えないと話にならない！

##FlaskとMySQLをDockerに載せる
ということで、MySQLとFlaskの開発環境をDockerの上で構築することにしました。
オウム返しbotで使ったHerokuの環境はいったん置いておきます。。

FlaskからMySQLのDBを扱うために、SQLAlchemyというORM(オブジェクト・リレーショナル・マッパー)モジュールなるものを使うことにしました。
ドライバはPyMySQL(PythonのMySQLクライアントモジュール)を使います。
DBスキーマをマイグレーション管理するため、Flask-Migrateもインストールします。

#サンプル：Peterを増やそう大作戦
LINEを使ったアプリにする前に、データベースを使えるようにしましょう。
ということで、WebアクセスでUserテーブルのPeterさんが増えたり減ったりするプログラムを作ってみます。
ツリー構造はこんな感じ。
前提として、Docker,Docker-composeをインストールしています。

```
FLASK_SQLALCHEMY
│  docker-compose.yml
│
├─docker
│  │  Dockerfile
│  │  requirements.txt
│  │
│  └─db
│
└─server
    └─project
        │  run.py
        │
        └─Peter
           │  app.py
           │  config.py
           │  database.py
           │  __init__.py
           │
           └─models
              │  models.py
              └─ __init__.py
```

###Docker関係のファイル

####`docker-compose.yml`

```docker-compose.yml:
version: '3'

services:
  flask:
    container_name: linebot-python
    build: ./docker
    ports:
      - 5000:5000
    links:
      - mysql
    privileged: true
    volumes:
      - ./server/project:/project
    tty: true
    environment:
        TZ: Asia/Tokyo
        FLASK_ENV: 'development'    #デバッグモードON
        FLASK_APP: 'run.py'         #起動用アプリの設定
    command: flask run -h 0.0.0.0

  mysql:
    container_name: linebot-db
    image: mysql:5.5
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: peter
      MYSQL_USER: hoge
      MYSQL_PASSWORD: huga
      TZ: 'Asia/Tokyo'
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    volumes:
      - ./docker/db/data:/var/lib/mysql
      - ./docker/db/sql:/docker-entrypoint-initdb.d
    ports:
      - 3306:3306
```

flaskのコンテナは5000番ポート、mysqlは3306番ポートで接続しています。

flaskのcommandで`flask run -h 0.0.0.0`を記載しておくと、
コンテナ起動と同時にFlaskサーバが立ち上がり結果を確認することができます。
今回はデバッグモードでFlaskを起動したいので、environmentに`FLASK_ENV: 'development'`を追加しています。
デバッグモードが不要の場合はコメントアウトします。

環境変数`FLASK_APP: 'run.py'`についてですが、
今回起動用のファイルと主機能を載せたファイルを分離しているので、
起動用のファイルを指定しています。
これがないと、projectフォルダ直下でapp.py（デフォルトのアプリファイル名）が見つからずにエラーとなってしまいます。

MySQLは仕事上の制約で5.5にしてますが、これはお好みで。
MySQLの文字コード指定は、my.cnfを設定してもよいのですが、
コマンドで指定したほうが早そうなのでこちらにしました。

####`Dockerfile`

```Dockerfile:
FROM python:3.6

ARG project_dir=/project/

ADD ./requirements.txt $project_dir

WORKDIR $project_dir

RUN pip install -r requirements.txt
```

requirements.txtに一覧化されているモジュールを一括インストールしています。
いちいちpipしなくていいので便利です。


####`requirements.txt`
```requirements.txt:
Flask
sqlalchemy
flask-sqlalchemy
flask-migrate
pymysql
```

利用したいモジュールをリスト化しています。

####db
フォルダだけ作っておきます。
先に読ませたいSQLを置くこともできますが、今回は特にないので何もしません。
MySQLコンテナを立ち上げたときにvolumes指定したものを自動で作ってくれます。


###SQLAlchemyの設定

ここからは、Flaskのソースコードになります。
`server/project/`にソースコードを配置します。

####`config.py`

```config.py:
import os


class DevelopmentConfig:

    SQLALCHEMY_DATABASE_URI = 'mysql+pymysql://{user}:{password}@{host}/{database}?charset=utf8mb4'.format(**{
        'user': os.getenv('DB_USER', 'hoge'),
        'password': os.getenv('DB_PASSWORD', 'huga'),
        'host': os.getenv('DB_HOST', '[MySQLコンテナのIP]'),
        'database': os.getenv('DB_DATABASE', 'peter')
    })
    SQLALCHEMY_TRACK_MODIFICATIONS = False
    SQLALCHEMY_ECHO = False
    
Config = DevelopmentConfig
```

環境変数の設定ファイルです。
DBのhostは、dockerのネットワークを調べたうえで記入する必要があります。
なので、コンテナをbuild,upした後で記入します。

####`database.py`

```database.py:
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate


db = SQLAlchemy()

def init_db(app):
    db.init_app(app)
    Migrate(app, db)
```
SQLAlchemyの初期化用ファイルです。

####models

今回はPeterさんを増やすので、IDと名前だけ記録するUserテーブルを作ります。
作成日時・アップデート日時は実際必要になることが多いので、備忘録がてら取り入れています。

```models.py:
from datetime import datetime
from Peter.database import db


class User(db.Model):

    __tablename__ = 'users'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), nullable=False)
    created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
    updated_at = db.Column(db.DateTime, nullable=False, default=datetime.now, onupdate=datetime.now)
        
    def __repr__(self):
        return '<User id={id} name={name}>'.format(
                id=self.id, name=self.name)

```

```__init__.py:
from .models import User


__all__ = [
    User,
]
```


###Flaskアプリ動作の設定

ここまで作った枠組みを、実際に動かす機能を作ります。
先述の通り、主機能を持った`app.py`と起動のみの`run.py`に分けています。

####`app.py`

```app.py:
from flask import Flask
from Peter.database import init_db, db
from Peter.models import User


def create_app():
    app = Flask(__name__)
    app.config.from_object('Peter.config.Config')
    
    init_db(app)
    
    @app.route('/')
    def index():
        return 'Peterが増えるプログラムです'

    @app.route('/show')
    def show_users():
        all_peter = User.query.filter_by(name='peter').all()
        how_many_peter = len(all_peter)
        return '今Peterは{}人います'.format(how_many_peter)

    @app.route('/add')
    def add_user():
        peter = User(name='peter')
        db.session.add(peter)
        db.session.commit()
        return 'Peterを増やしました。'

    @app.route('/delete')
    def delete_user():
        peter = User.query.filter_by(name='peter').first()
        if peter is not None:
            db.session.delete(peter)
            db.session.commit()
            return 'Peterを減らしました。'
        else:
            return 'Peterはひとりもいません'

    return app


app = create_app()
```
`localhost:5000/`でこのサービスの概要説明、
`localhost:5000/add`でPeterが増える
`localhost:5000/delete`でPeterが減る
`localhost:5000/show`でPeterの人数がわかる、といった具合です。

####`run.py`

```run.py:
from Peter.app import app

if __name__ == '__main__':
    app.run()
```

####`__init__.py`
何も書かなくてよいですが、ファイルだけ作成しておきます。
（ないとエラーになります。）

##Dockerコンテナを起動して、トップページにアクセス
これで、DBを使わないトップページだけならアクセスできるはずです。
一度コンテナを起動します。

まず、
イメージをビルドします。

```
$ docker-compose build
```
処理が終わったら、コンテナを起動します。

```
$ docker-compose up
```
`-d`オプションを付けるとバックグラウンドで起動しますが、きちんとFlaskサーバが動いているかを確認したいので、最初はオプションなしで起動をお勧めします。

↓のように出力されればOKです。

```
linebot-python |  * Serving Flask app "run.py" (lazy loading)
linebot-python |  * Environment: development
linebot-python |  * Debug mode: on
linebot-python |  * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
linebot-python |  * Restarting with stat
linebot-python |  * Debugger is active!
linebot-python |  * Debugger PIN: 122-417-323
```
Webブラウザで、`localhost:5000`にアクセスしてみましょう。
「Peterが増えるプログラムです」と表示されればOK！
コンテナから抜けるときはCTRL+Cを押します。（コンテナが停止します）


##DBのホストを設定する

このまま`/add`などにアクセスしても、DBのホストを設定していませんし、
そもそもテーブルを作っていないのでエラーになります。

まずはホスト設定を行います。
dockerのネットワークを調べて、MySQLコンテナのIPアドレスを探します。

サンプルアプリが属しているネットワークを調べます。

```
 $ docker network ls
NETWORK ID          NAME                       DRIVER              SCOPE
573eb5f34bc9        bridge                     bridge              local
ebd6757e246e        flask_sqlalchemy_default   bridge              local
28b6ef9d5831        host                       host                local
b28c335f87bb        none                       null                local
```
`flask_sqlalchemy_default`っていうのが該当していますね。
このネットワークを詳しく見てみます。

```
$ docker network inspect flask_sqlalchemy_default

    ~~中略~~
        "Containers": {
            "8eba3e42ef3b89ba116e27a755abe135aae5fd5e2ff98382e6906e65cfe406b5": {
                "Name": "linebot-db",
                "EndpointID": "02cc7765950bb9bca313a0cd8eb0001b5417eaab1225b02220f9ad63e2d71cf6",
                "MacAddress": "02:42:ac:12:00:02",
                "IPv4Address": "172.18.0.2/16",
                "IPv6Address": ""
            },
            "9cd7f8c9eea6abb37662dc7d8f49eb19fa42735dc7df6348becbcc30fe315dd1": {
                "Name": "linebot-python",
                "EndpointID": "d440a077abbb83905ca290c80e2900f6e7da58990826fb2b6932e925728a4a45",
                "MacAddress": "02:42:ac:12:00:03",
                "IPv4Address": "172.18.0.3/16",
                "IPv6Address": ""
            }

    ~~中略~~
```

私の環境では、linebot-dbコンテナは172.18.0.2、linebot-pythonコンテナは172.18.0.3であることがわかります。
※内部で利用しているIPアドレスなので、外部からアクセスできません。
　Webブラウザでたたいてもエラーになるはずです。

このIPアドレスを、config.pyのhost部分に記入します。

##migrateする
マイグレーションを行って、データベースを作ります。

作業のために、コンテナ内に入りますので、コンテナをバックグラウンドで起動するか、コマンドラインを複数立ち上げます。

```
$ docker exec -it linebot-python bash
```

コンテナ内で以下の操作を行います。

```
root@[コンテナID]:/project#   flask db init
root@[コンテナID]:/project#   flask db migrate
root@[コンテナID]:/project#   flask db upgrade
```

これでFlaskコンテナを経由して、MySQLコンテナにUserテーブルが作られたはずです。
（データベースそのものはdocker-compose.ymlでpeterデータベースを作成済み）


MySQLコンテナに入って、DBの様子を確認できます。

```
$ docker exec -it linebot-db bin/bash
```

```
root@[コンテナID]:/#   mysql -u hoge -phuga peter
```

```
mysql>show tables;

+-----------------+
| Tables_in_peter |
+-----------------+
| alembic_version |
| users           |
+-----------------+
2 rows in set (0.01 sec)
```

##実際にアクセスしてみる

`localhost:5000/add`にアクセスしてみましょう。
Peterが増えたら、`localhost:5000/show`で人数を確認。
deleteも試してみます。

#次回はLINEbotアプリとDB接続の組み合わせ

オウム返しbot作成で学んだbotの作り方を、今回のサンプルアプリに組み合わせてみようと思います。
前回も今回も、LINEbotと直接関わりない内容だったので、タイトル詐欺にならないよう気を付けます。

#参考記事一覧


・[PythonのFlaskでMySQLを利用したRESTfulなAPIをDocker環境で実装する](https://qiita.com/kai_kou/items/5d73de21818d1d582f00)
・[Flask + SQLAlchemyプロジェクトを始める手順](https://qiita.com/shirakiya/items/0114d51e9c189658002e)
・[Flask-SQLAlchemyの使い方](https://qiita.com/msrks/items/673c083ca91f000d3ed1)
・[Docker公式イメージのMySQLで文字コードを指定する](https://qiita.com/neko-neko/items/de8ea13bbad32140de87)
・[SQLAlchemy で Update するには？](https://qiita.com/nskydiving/items/745f985a42da89a16934)
・[SQLAlchemyについて](https://study-flask.readthedocs.io/ja/latest/flask_sqlalchemy.html)（公式）


大変勉強になりました！ありがとうございました！！