Flaskで作るSNS　Flask(Model)編

#TL;DL
Udemyの下記講座の受講記録

Python+FlaskでのWebアプリケーション開発講座！！～0からFlaskをマスターしてSNSを作成する～
https://www.udemy.com/course/flaskpythonweb/

この記事ではFlaskのView側(Model側)について記載する。

View(Template）側については以下の記事を参照
https://qiita.com/kekosh/items/e4b5d1e3272a20d1c966

##Model(モデル)
モデルとはデータベースのテーブルの内容を定義するもの。

**SQLAlchemy**
pythonで用いられるORマッパーのライブラリ。要はDBを操作するために利用するライブラリ

**マイグレーション**
Modelの変更をデータベースのテーブルに反映すること。


###１．テーブルの作成

**インストール**

```bash
pip install flask-sqlalchemy
pip install flask-migrate
```

**モデル**

```python
#model.py
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

base_dir = os.path.dirname(__file__)

app = Flask(__name__)

# sqliteのファイル保存先を指定
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
    os.path.join(base_dir, 'data.sqlite')

# モデルに変更があった場合にシグナル送出の有無を設定する
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# appに設定した内容でDBに接続する
db = SQLAlchemy(app)

# モデルクラスの定義にはdb.Modelクラスを継承する必要がある。
class Person(db.Model):
    # テーブル名を設定(テーブル名はクラス名の複数形が一般的)
    __tablename__ = 'persons'

    # 作成するテーブルのカラムを定義
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    age = db.Column(db.Integer)
```

**モデルからテーブルを作成し、データを追加する。**

```python
#crud.py
from model import db, Person

# モデルからテーブルを作成する(インポートされているモデルすべてが対象)
db.create_all()

* モデルで定義したデータクラスのインスタンスを作成する。
man1 = Person('Taro', 18)
man2 = Person('Jiro', 18)
man3 = Person('Saburo', 18)

# 作成したテーブルにを追加する(add:追加　add_all：リスト形式の一括追加)
db.session.add_all([man1, man2])
db.session.add(man3)

# テーブルへの変更をcommitで確定する。
db.session.commit()
print(man1, man2, man3)
```

**crud.pyファイルを実行後、Modelフォルダ内にsqliteのデータファイルが作成されていることを確認**
![スクリーンショット 2020-11-24 22.39.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600163/1456f406-d061-da0a-9fe5-576799ac2ddf.png)

###２．テーブルのマイグレーション
マイグレーションとは・・・
データベースにモデルファイルに定義したテーブルの更新情報を反映し、その情報を履歴として管理しておくこと。これにより、別環境などに同内容のDBを構築したい場合に履歴に従ってDBを更新していくことで上手く同じ内容のDBを作成することができる。

注意点は、前の項のようにコントロールファイル内で「db.create_all()」メソッドを実行してテーブルを作成した場合はマイグレーションを利用するのに適していないこと。

```python
from model import db, Person

# この場合はマイグレーションによる管理は適さない。
db.create_all()
```

**マイグレーション管理する場合のモデル**

```python
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

base_dir = os.path.dirname(__file__)

app = Flask(__name__)

# sqliteのファイル保存先を指定
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + \
    os.path.join(base_dir, 'migrate_data.sqlite')

# sqlalchemyのデータに変更があった場合にトレースする設定のONOFF
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# DBのインスタンス作成(接続)
db = SQLAlchemy(app)

# マイグレーションの準備（引数はFlaskAppとSQLAlchemyのインスタンス）
# 2021-01-10:
# 初期化はinit_appメソッドでも可能（Flask拡張機能標準メソッド)
# [Migrationインスタンス].init_app([flaskアプリ名],[db名]
# 例：
# migrate = Migrate()
# migrate.init_app(app, db)
Migrate(app, db)

# db.Modelクラスを継承することでテーブルを定義できるようにする
class Person(db.Model):
    # テーブル名を設定(テーブル名はクラス名の複数形が一般的)
    __tablename__ = 'persons'

    # 作成するテーブルのカラムを定義
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    gender = db.Column(db.Text)
    age = db.Column(db.Integer)

    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return "id={},name={},age={}".format(self.id, self.name, self.age)
```

**マイグレーションの実行**
1.環境変数を設定する
command : export FLASK_APP = [Flaskインスタンスを作成しているファイル]

**"Flaskインスタンスを作成しているファイル"とは、"app=Flask()"が実行されているファイルのこと。（書かれているところではなくて実行されているところ）**

```terminal
(flaskenv) (base) root@e8cf64ce12e9:/home/venv/flaskenv/Model# export FLASK_APP=migrate_model.py
```

2.DBを初期化する。これにより、マイグレーションの設定情報を保存するMigratinフォルダが作成される。
command : flask db init

```terminal
(flaskenv) (base) root@e8cf64ce12e9:/home/venv/flaskenv/Model# flask db init  Creating directory /home/venv/flaskenv/Model/migrations ...  done
  Creating directory /home/venv/flaskenv/Model/migrations/versions ...  done
  Generating /home/venv/flaskenv/Model/migrations/README ...  done
  Generating /home/venv/flaskenv/Model/migrations/alembic.ini ...  done
  Generating /home/venv/flaskenv/Model/migrations/env.py ...  done
  Generating /home/venv/flaskenv/Model/migrations/script.py.mako ...  done
  Please edit configuration/connection/logging settings in
  '/home/venv/flaskenv/Model/migrations/alembic.ini' before proceeding.
```

3.DBにモデルの変更内容を反映するためのマイグレーションファイルを作成する。
command : flask db migrate -m 'コメント'

```terminal
(flaskenv) (base) root@e8cf64ce12e9:/home/venv/flaskenv/Model# flask db migrate -m 'add Person'
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'persons'
  Generating /home/venv/flaskenv/Model/migrations/versions/aa0223688c58_add_person.py
  ...  done
```

4.マイグレーションファイルの内容をDBに反映する。
command : flask db upgrade

```terminal
(flaskenv) (base) root@e8cf64ce12e9:/home/venv/flaskenv/Model# flask db upgrade
INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade aa0223688c58 -> c881e626270e, persons db add new column "gender"
```

マイグレーションを実行することで、modelファイル上でのテーブルに対する変更内容がDBに反映される。
また、マイグレーションに伴い、DBに「alembic_version」テーブルが自動的に追加される。このテーブルは、マイグレーションのバージョンを管理するために追加されるテーブルで、右クリックのShow_Tableメニューから内容を確認すると、適用されたマイグレーションのバージョン情報が確認できる。
![スクリーンショット 2020-12-13 0.37.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600163/b9fa6818-26a9-9211-c626-c1146b033f41.png)


###３．制約の提示、インデックス作成

| オプション | 制約 | コード |
|:-:|:--|:--|
| primary_key  | 主キー制約  | db.Column(db.Integer, primary_key = True)  |
| unique | ユニーク制約  | db.Column(db.Integer,unique = True) |
| nullable  | NOT NULL制約  | db.Column(db.Integer,nullable = False) |
| index | インデックスを作成 | db.Column(db.Text,Index = True) | 
| db.Index | クラスの外でインデックスを定義することが可能。第1引数にインデックス名、第2引数にインデックスの関数を定義する| db.Index('some_index',func_lower(Person.name))|
| server_default | カラムのデフォルト値を設定 | db.Column(db.Text, server_default='xxxx') |
| CheckConstraint ※  | チェック条件を自由に設定 | \_\_table_args__=(CheckConstraint('update\_at > create\_at'),) |

※CheckConstraintモジュールををsqlalchemyからインポートする必要がある。

**db.config['SQLALCHEMY_ECHO'] = Trueの場合**
![スクリーンショット 2020-12-13 21.51.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600163/caa03567-352e-c92d-bada-5cf7b62c571c.png)
以下のように実行されたSQLの内容がコンソールに表示される。
![スクリーンショット 2020-12-13 21.51.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/600163/86bd5fac-8c54-f345-67c0-647b5ebebeb3.png)


###4.SQLAlchemyの基本操作

| コード | 概要 |
|:--|:--|
| db.session.add(object)  | レコードの挿入  |
| db.session.add_all([List])  | 複数レコード一括追加  |
| db.session.delete() 　Table.query.filter_by(条件).delete() | レコードの削除  |
| Table.query.get(primary_key)  | 主キーで抽出  |
| Table.query.all()  | データをリスト化してすべて抽出  |
| Table.query.first()  | データの最初の要素だけ取得  |
| Table.query.filter_by(name='A')  | 絞り込み  |
| Table.query.filter(Table.name.startswith('A'))  | 前方一致検索  |
| Table.query.filter(Table.name.endswith('z'))  | 後方一致検索  |
| Table.query.limit(1) | 件数を指定して抽出  |
| Table.query.update({'column': 'value'})  | アップデート |

- 複数レコードを取得した場合、個別のレコードを参照するためにはfor文で一つずつ取得する必要がある。

その他参考
https://docs.sqlalchemy.org/en/14/orm/query.html?highlight=query%20limit#sqlalchemy.orm.Query.limit

###5.外部キー
複数テーブルのカラム間で紐付けする。
外部参照キーを設定しているテーブルのデータをSELECTした場合、結果に参照先テーブルの紐づくデータも一緒に取得されるようになる。

| 概要 | テンプレート | サンプル |
|:--|:--|:--|
| 参照先設定 | db.relationship(参照先モデル名, backref='外部参照時テーブル名'  | projects = db.relationship('Project', backref='employees', [layzy=xxxx]) |
| 被参照設定  | db.Column(データ型, db.ForeignKey(キー項目)  | employee_id = db.Column(db.Integer, db.ForeignKey('employees.id'))|

○lazyオプション
テーブルを紐付けるときの処理方式を設定する。

| オプション | 概要 |
|:--|:--|
| select | デフォルトの設定。都度SELECT文を実施する。  |
| joined | JOIN句でテーブルを紐付ける  |
| subquery | サブクエリでテーブルを紐付ける  |
| dynamic | 紐付けたテーブルでクエリ実行用オブジェクトを作成する。  |



```python
import os
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

base_dir = os.path.abspath(os.path.dirname(__file__))

app = Flask(__name__)

app.config['SQLALCHEMY_DATABASE_URI'] = \
    'sqlite:///' + os.path.join(base_dir, 'data.sqlite')

app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['SQLALCHEMY_ECHO'] = True

db = SQLAlchemy(app)


class Employee(db.Model):
    __tablename__ = 'employees'

    """カラム定義"""
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    # 外部キーの宣言
    # One to Many
    projects = db.relationship('Project', backref='employees')
    # One to One
    company = db.relationship('Company', backref='emoployees', uselist=False)

    def __init__(self, name):
        self.name = name

    def __str__(self):
        if self.company:
            return f'Employee name {self.name} company is {self.company.name}'
        else:
            return f'Employee name = {self.name}, has no company'

    def show_projects(self):
        for project in self.projects:
            print(project)


class Project(db.Model):
    __tablename__ = 'project'

    """カラム定義"""
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    employee_id = db.Column(db.Integer, db.ForeignKey('employees.id'))

    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id


class Company(db.Model):
    __tabelname__ = 'companies'

    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.Text)
    employee_id = db.Column(db.Integer, db.ForeignKey('employees.id'))

    def __init__(self, name, employee_id):
        self.name = name
        self.employee_id = employee_id


# テーブル作成
db.create_all()
```

###6.トランザクション
- トランザクションとは、DBに対する処理を１つの単位としてまとめたもの。
- コミットするまでを１トランザクションとするため、トランザクション内でエラーが発生した場合は、トランザクション内の処理すべてがロールバックされる。
- 一連の処理結果がすべて完了するかエラーですべてなかったことになるかのどちらかになるので、トランザクションを利用することで、処理の原子性が保たれる。

```python
# 構文
with db.sessino.begin(subtransactions=True):
    DB処理
db.session.commit()
```

```python
# データ更新処理
# 対象レコード取得
record = Model.query.get(1)
# 更新したい項目に代入
record.name = '更新'
# db.sessionに更新後のレコードオブジェクトを追加
db.session.add(record)
# コミット処理
db.session.commit()
```