【3ヶ月でFlask基礎固め】 3.アプリ開発でのTips

# 目的
初めて作成したSNSアプリケーション「TWITTY」のコードレビュー記事です。
振り返りも兼ねてTipsをまとめておこうと思います。

*間違った点があればご指摘いただけると幸甚です。


https://qiita.com/masaotomeza/items/77bd861fc273aec82bd7

# Tips
1. WEBAPPがユーザごとに違う画面を表示する仕組み
1. WSGI・Enginx・gunicorn
1. ログイン処理ロジック
1. アプリの起動の仕方
1. アプリ開発環境設定
1. Pythonスクリプト別の解説

# WEBAPPがユーザごとに違う画面を表示する仕組み

ブラウザアプリに「Cookie」というWEBの閲覧に関連する情報を一時保存をする機能があり、そのCookieの中にセッション情報(今回のアプリで言うところのログインしたユーザーID)を保存している。

このCookie情報は毎回のクライアント側からのRequestに含まれているため、サーバー側でどのユーザからのアクセスなのかを識別することができるため、個別のHTMLファイルを動的に表示することができる。


### セッションの種類(サーバーサイド / クライアントサイド)

Client Side Session : 
- クライアント側のRequestに含まれる、Cookieにセッション情報を含ませておく
- Flaskのsessionライブラリにおいては secret_keyを使って暗号化してCookieに保存させている

Server Side Session : 
- Cookieのセキュリティの懸念と保存可能メモリや有効期限といった制約から外れるための機能
- ユーザのセッション情報をサーバー側のデータベースに保存しておく
- Cookieに保存されたセッションIDのみを参照して個別の処理を実現する

### Cookieの補足
- SOP(Same-Origin Policy)：同一オリジンポリシーに基づき、ブラウザ側ではドメイン名単位でRequestに送るCookieを区別している
- オプションとしてドメイン名よりも小さな粒度でCookieを保存することも可能
- 別ドメインにCookieを保存することも可能(Cross-Origin Resource Sharing (CORS))

## WSGI・Enginx・gunicorn

これらはWEBスタック(*)の構成要素である
(*WEBアプリケーションに使われる技術のまとまり)

### WSGI（Web Server Gateway Interface）
- ソフトウェア名ではなく規格の名前
- PythonのAppとWEBサーバーを仲介し、Appがサーバーの違いに左右されずに動作できる様にする
- 具体的にはWEBサーバーが受け取ったHTTP RequestをPythonのAppに引き渡し、処理結果をHTTP ResponseとしてWEBサーバーに戻す役割がある

### Enginx(エンジンエックス)
- WEBサーバー用の有名なソフトウェア
- 主な機能
    - ロードバランサー : 
        - 大量のRequestを複数のサーバーに均等に分散させる機能
    - リバースプロキシ : 
        - 通常のプロキシ(フォワードプロキシ)
            - クライアントのRequestを集約してWEBサーバーと代理で通信する(VPNとは違う)
            - キャッシュを保存でき、リクエスト数を減らすことができる
            -　　認証機能を用いて社内外からの不正アクセスを防ぐことができる
        - リバースプロキシ
            - クライアントからのRequestと、WEBサーバーの中間に位置するサーバー
            - Requestに応じて適切なサーバーへ処理を振り分けることができる。(静的コンテンツだけ欲しい場合と、そうでない場合)
            - 複数の同じ処理ができるサーバーを配置しておけばRequestを分散させられる
            - キャッシュ機能もあり、注目されている静的コンテンツはWEBサーバではなくキャッシュを返してくれるので高速な表示&WEBサーバーへの負荷軽減につながる
        - 参考 : https://eset-info.canon-its.jp/malware_info/special/detail/201021.html
    - SSLの終端としての機能 : 
        - HTTPS(TSL/SSL)通信における内容の暗号化&複合化をプロキシサーバーが行うことで、WEBサーバの負担を軽減させられる。

### Gunicorn
- Python製のWSGIアプリケーションサーバー(ソフトウェア)
    - 主にPythonのDjangoやFlaskと一緒に使われることが多い
- アプリケーションサーバーの役割
    - WEBサーバーからの振り分けられた動的コンテンツ生成のRequestを受け取り、コードを実行してResponseを生成する
    - マルチワーカーモデルにより、同時に複数のリクエストを処理できる様になっている

# ログイン処理ロジック

デコレーターの @user.login_required を付与する事で、各関数の実行前にこのセッションにloginという項目があるのかどうかを確認するという仕組みを実装しています。

```python: app.py
# ホーム画面
@app.route('/home/<user_id>')
@user.login_required
def home(user_id):
    post_all=posts.get_posts(user_id)
    return render_template('home.html',posts=post_all)
```

#### What's デコレータ?
```
@user.login_requiredが指している、sns_user.pyのlogin_required関数がデコレータに該当します。
各関数を引数に入れて、ラッパー関数の中で処理を変更させます。

*ラッパー関数オブジェクトを生成するファクトリとしての機能
```

#### What's ラッパー関数?
```
login_required関数の中にある wrapper関数がラッパー関数に該当します。
デコレータから渡された関数を引数に入れて、少し処理を加えて関数を戻すという働きをしています。

(デコレータから渡された関数を、ラッパー関数で「wrap(包む)」している。)
```

##### デコレータの作成箇所
```python:sns_user.py : 
from functools import wraps

# デコレータ : 関数オブジェクトを引数にラッパー関数を戻している
def login_required(func): 
    @wraps(func)
    
    # ラッパー関数 : is_login関数によるログイン判定を追加して、関数オブジェクトfuncを戻している
    def wrapper(*args, **kwargs): 
        if not is_login():
            return redirect('/login')
        return func(*args, **kwargs)
        
    return wrapper
```
:::note info 
@wraps(func)は本来なくても実行できるものですが、デバッグに便利なライブラリです。通常、デコレーター(中身はラッパー関数)を実行した際にそのメタ情報(__doc__  , __name__)は、ラッパー関数のものとして上書きされますが、@wraps(func)があることで元々の関数のメタ情報を保持したまま実行することができます。
:::

参考 : https://zenn.dev/umeko/articles/8ef2df8be8b017


### アカウント作成 & ログイン処理

```python:app.py:secret_keyの設定
# Flaskインスタンスの作と暗号化キーの決定
app = Flask(__name__)
app.secret_key = "test_key"
```

:::note info
secret_keyは、Cookieに付与する情報を暗号化する際に用いられます。
:::

```python:sns_user.py : アカウントの作成処理
# アカウント作成処理
def try_create_account(form):
    user_id = form.get('user_id',"")
    password = form.get('password',"")
    if not user_id or not password:
        msg="Fill in Id&PASS both"
        return False,msg

    # ユーザーIDに重複がないかを確認する
    conn = sqlite3.connect(sqlite_func.DB_FILE)
    c = conn.cursor()
    c.execute('SELECT user_id FROM users where user_id=?',(user_id,))
    result = c.fetchone()
    if result is not None: # レコードがみつかった場合
        conn.close()
        msg="This Id is already used"
        return False,msg
    else:
        # パスワードをハッシュ化
        salt = bcrypt.gensalt()
        hashed_password = bcrypt.hashpw(password.encode('utf-8'), salt)
        # データベースにユーザを追加
        c.execute('INSERT INTO users (user_id, password) VALUES(?,?)',(user_id, hashed_password))
        conn.commit()
        conn.close()
        session['login']=user_id
        return True,"okey"
```

```python:sns_user.py : ログイン処理
# ログイン処理 - 入力されたIDとパスワードが一致しているかを確認する
def try_login(form):
    user_id = form.get('user_id')
    password=form.get('password')
    
    # ユーザー名に基づいてハッシュ化されたパスワードを取得
    conn = sqlite3.connect(sqlite_func.DB_FILE)
    c = conn.cursor()
    # user_idが文字列として提供されている場合、文字列内の各文字が個別のバインディングとして解釈される可能性があります。例えば、user_idが"abcd"であれば、4つのバインディングが提供されたと解釈されます。
    # 正しい方法は、user_idを単一の要素を持つタプルとして渡すことです。タプルを使う場合は、カンマを忘れないようにしてください（カンマがないと、それはタプルとは認識されません）。
    c.execute('SELECT password FROM Users where user_id=?',(user_id,))
    result = c.fetchone()
    conn.close()
    msg = ""
    if result is None:
        msg = "Id and Password is mandetory"
        return False, msg
    else:
        hashed_password = result[0]
        # 入力されたパスワードがデータベースのハッシュと一致するかを検証
        if bcrypt.checkpw(password.encode('utf-8'), hashed_password):
            session['login'] = user_id
            return True,msg
        # url_for関数は、Flaskのルート関数名を引数に取り、そのルートのURLを生成します。ルート関数名は、@app.routeデコレータで定義された関数の名前です。url_forを使用する際は、URLの一部を動的に追加するために文字列結合を直接使用するのではなく、キーワード引数を渡してその部分を指定します。
        else:
            msg="Password or Id is wrong"
            return False,msg

```

```python:sns_user.py : ログアウト処理
# ログアウト
def try_logout():
    session.pop('login', None)
```

# アプリの起動の仕方
Flaskのアプリ実行コマンドは次の2種類ある。両方とも開発用のサーバーを建てているだけなので本番は別途WSGIサーバーを建てる必要があります(*Gunicorn)
1. ```flask run```
1. ```python app.py```

:::note warn
TODO : 本番環境(クラウド)でのサーバーを立てる設定方法について調査
:::

```cl:flask runで起動
~/De/develop/flask_x main !4 > flask run                                          py flask_env py base
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://127.0.0.1:5000
```
:::note 
環境変数から実行されるコマンド。DebugモードがデフォルトでONになっている。
URLは http://127.0.0.1:5000 だけで展開されている。

このURLは host = 127.0.0.1 いわゆる Localhostで自分自身のサーバーの所在を指しています。
Flaskはデフォルトでは、外部からアクセスできない様になっているのです。
:::
:::note warn
TODO : flask_dotenvライブラリについて調査
:::

```cl:python app.pyで起動
~/De/develop/flask_x main !4 ?1 > python app.py                               11s py flask_env py base
 * Serving Flask app 'app'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5001
 * Running on http://192.168.2.102:5001
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: XXX-XXX-XXX
```
:::note info
FlaskをPythonスクリプトとして実行した時の挙動です。スクリプトの中で設定した、app.runのパラメータ情報を適用したサーバーが建てられます。
:::
スクリプト内のサーバー設定  : 
```python: app.py
if __name__ == "__main__":
    app.run(host='0.0.0.0', port=5001, debug=True)
```
:::note info
host='0.0.0.0'とは、どのIPアドレスへのアクセスも受理するという全開の設定になっています。

これにより起動したマシンが存在している自分自身としてのhost='127.0.0.1'と、接続しているローカルのインターネット環境(家のwifi環境)におけるマシンのIPアドレス = '192.168.2.102'においてアクセスできるという設定になっています。
:::

:::note info
if \_\_name\_\_ == "\_\_main\_\_":　について。

Pythonスクリプトを実行するときに __name__という変数が自動で生成されます。
通常この変数は実行した関数が入っているファイル名が入ります。

しかし、そのファイルを python <~>.pyとして実行した場合は__main__という文字が入ります。つまりこの箇所では、python app.pyとして実行したときは app.runを実行してねという意味合いになります。
:::

# アプリ開発環境設定
今回の開発環境は以下の通りです。
```
~/De/develop/flask_x main !6 ?1 > ls                                              py flask_env py base
__pycache__    flask_env      relation.py    sns_user.py    static
app.py         migrations     sns_data.py    sqlite         templates
extension.py   models         sns_posts.py   sqlite_func.py test.sqlite3
```
# Pythonスクリプト別の解説
各スクリプトのコードはGithubに掲載しています。ここでは部分的な解説を行います。

Github : https://github.com/lovehakumai/TWITTY

<details><summary>app.py</summary>

## 概要
```
・flask runを実行した時に参照されるファイル
・本アプリにおいてもルーティングを捌く中心的なスクリプトとして開発しました
・サーバーに対して送られたRequestに入っているURLに応じた処理を行う。または別のプログラムにRequestを渡す役割をしています
```

### 解説ポイント　
#### ・ORMによるマイグレーション

##### ORM(Object Relational Migration)
Pythonの様なオブジェクト指向の言語と、データベースをリンクさせる考え方。クラスがテーブルで、インスタンスが行として紐づけられる。　flask_sqlalchemyインスタンス(dbという変数に入れられる)を用いて、フィルタを適用したり新しい行を追加したりとデータベースに対してCRUD操作をおこなう事ができる。
また、文字列を結合してSQL文を作らないので、SQLインジェクションの回避にも繋がる。
参考 => https://amzn.asia/d/buEekzD

##### マイグレーション
　ここでは新しいテーブル情報に古いテーブル情報を書き換えるという意味合いで用いています。今回はORMと、flask_migrateインスタンス(Migrate)を使って、Pythonスクリプトで定義したテーブルのクラス(Users, Post_contents, Relationクラス等)へ行われた変更を実際のSQLite内のテーブルにも反映をさせました。

:::note warn 
TODO マイグレーション後のテーブルでデータ型が変わった時の他の行の挙動について確認
TODO ORMのフィルタリングやCRUD操作を実装してみる
:::

#### ・redirect と url_for
flaskの標準ライブラリに入っている以下3つの関数は今回のアプリに必須のものでした。
- redirect
- url_for
- render_template

何度も引っかかってしまったのは url_for()関数です。これは引数にフォルダ名や関数名とパラメータを入れる事で適切なURLを動的に作ってくれる関数なのですが、URLを作るだけでリダイレクトしてくれるわけではありません。

なので、 ```redirect(url_for('home',user_id=user_id))``` の様に必ずredirectをつけてあげてResponseしてあげる様にしましょう。

#### ・ルーティングに<user_id>などの動的変数を引き渡す方法と注意点

例えば以下のコードではlogin_try関数の中で ```return redirect(url_for('home',user_id=user_id))``` という戻り値を設定した事で、home関数に対してパラメータuser_idを持つURLを生成し、 ```/home/<user_id>``` にリダイレクトする実装をしている。

```python: note
# ログインページ loginページから入力フォームが送られる
@app.route('/login/try', methods=["POST"])
def login_try():
    result, msg = user.try_login(request.form)
    if result :
        user_id = session['login']
        print("login_try result : ", result)
        return redirect(url_for('home',user_id=user_id))
    else:
        return render_template('login.html', msg=msg)

# ホーム画面
@app.route('/home/<user_id>')
@user.login_required
def home(user_id):
    post_all=posts.get_posts(user_id)
    return render_template('home.html',posts=post_all)
```

:::note info
URLに動的な変数(パラメータ)が存在しているとき、そのルーティング先の関数には引数が自動で入ってくる為、 ```def home()``` の様に位置引数を指定していない場合は、 ```cl: TypeError: home() takes 0 positional arguments but 1 was given``` の様なエラー(関数には位置引数が0個設定されているけど、1個引数が入っているよ。)が出ます。

パラメータをURLに使っている場合は、必ず関数の引数に位置引数を受け取る様に実装しましょう。
:::


#### ・werkzeug.utilsライブラリを使ったファイル保存方法
ユーザーからのファイル名をそのままスクリプト上で利用すると以下の様なセキュリティイシューに巻き込まれる可能性がある。

- Path Traversal(Directry Traversal) Attack
    - ファイル名が「../」を含んでいる場合、開発側の意図していない箇所へ保存されてしまう。
- DoS(Denial of Service) Attack
    - ファイル名の文字数に制限がない場合、長いファイル名を利用してサーバーに大きな負担をかけようとする攻撃

これらを回避する手段の一つに flaskの標準ライブラリの「Wergzeug」のsecure_filenameを利用します。
相対パスを示す可能性のある「../」のようなパターンを取り除くなどの処理がされます。

##### 挙動の確認

```python: werkzeug.utils.secure_filenameの挙動
>>> print(secure_filename("sample.jpg"))
sample.jpg
>>> print(secure_filename("/////..a"))
a
>>> print(secure_filename("あいうえお"))

```

:::note info
詳しい解説は公式DOCへ : https://flask.palletsprojects.com/en/3.0.x/patterns/fileuploads/
:::


##### 本アプリのファイル保存ロジック
ポイント : 
- ファイル拡張子を os.path.splitextで分ける(ex. sample.pngは sample, pngに分かれる)
- 日本語の様なASCIIではない文字はsecure_filenameで消されるのでutf-8でハッシュ化している
- ハッシュ化した上でsecure_filename関数を実行している。(拡張子は保持される)


```python:app.py
@user.login_required
@app.route('/myprofile/edit/try', methods=["POST"])
def myprofile_edit():
    user_id = user.session['login']
    name = request.form.get('nickname','')
    introduction = request.form.get('description','')
    thumbnail = request.files['thumbnail_img']
    if not name:
        return render_template("myprofile_edit.html",msg='Nickname is needed')

    if thumbnail:
        # ファイル拡張子を保持
        filename=thumbnail.filename
        _, ext = os.path.splitext(filename)
        # ファイル名をハッシュ化
        hash_name = hashlib.md5(filename.encode('utf-8')).hexdigest()
        print("hashed filename : ", hash_name)
        filename = f"{hash_name}{ext}"
        filename=secure_filename(filename)
 
        # 画像を保存するディレクトリのパスを指定
        save_user_dir = os.path.join(PROFILE_FILE,user_id)
        if not os.path.exists(save_user_dir):
            os.makedirs(save_user_dir)
        print("profile_file : ", PROFILE_FILE)
        print("save_path : ", save_user_dir)
        print("filename : ", filename)
        save_path = os.path.join(save_user_dir,filename)
        thumbnail.save(save_path)
        save_path_db = user_id+"/"+filename

    if sns_data.save_profile_try(user_id=user_id,nickname=name, description=introduction,thumbnail_url=save_path_db):
        return redirect(url_for('myprofile'))
    else:
        return render_template("myprofile_edit.html",msg='You couldn\'t save profile. Please Try again. ')
```

</details>

<details><summary>extension.py</summary>

## 概要

```
・flask_sqlalchemy(db) と flask_migrate(Migrate)のインスタンスを作成するコード
・app.pyとデータベース(db)のModelクラスを含むスクリプト(ex.User.py)を分けて管理する事が目的
・app.pyでModelクラスをimportする際に、Modelクラスでdbインスタンスをimportすると循環参照が生じてしまう為、extension.pyの様にインスタンスを別スクリプトで生成させる必要があった
```
### 解説ポイント
#### ・循環参照を回避するための extension.py

このスクリプトで使用している flask_migrate, flask_sqlalchemyに関しては app.pyの箇所で解説をしているのですが、このextension.pyというプログラムがどうして必要だったのかについてここでは説明します。

```python: extension.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

# SQLAlchemyインスタンスの作成
db = SQLAlchemy()

# Migrateインスタンスの作成
migrate = Migrate()
```

まず、flask_sqlalchemyは、ORM(Object Relational Mapping)という技術を用いて、スクリプトの実行によってデータベースの操作をできる様になるためのライブラリです。今回はflask_migrateライブラリと組み合わせてpythonスクリプトベースで定義したSQliteのテーブルにmigrate(古いスキーマから新しいスキーマに更新)できる様になる事が目的でした。

もし、ORMとMigrationをapp.pyに統一すると以下の様に設定ができます。

```python: app.py(一部)
from flask import Flask
from flask_sqlalchemy import flask_sqlalchemy
from flask_migrate import flask_migarate

app = Flask(__name__)
app.secret_key = "test_key"

app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=False
app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///path/to/database'

# flask_sqlalchemyのインスタンス化
db = flask_sqlalchemy()
db.init_app(app)

# flask_sqlmigrateのインスタンス化
migrate = flask_migrate()
migrate.init_app(app,db)

# テーブルのスキーマを設定
class Users(db.Model):
    id = db.Column(db.Integer, primary_key=True, index=True)
    user_id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(30))
    create_date = db.Column(db.DateTime, default=datetime.now())
    
    # __repr__(): デバッグで用いられるインスタンスの公的な名前、クラスのprint(インスタンス)で表示される
    def __repr__(self):
        return f '<Users %r>'% self.name

```

今回はスキーマ情報を持つUsers.pyをはじめ、複数のテーブルをmodelフォルダに別々に定義して個別で管理できる環境にしているので、extension.pyの様にインスタンス化を行う処理をapp.pyと違うスクリプトで実行させる必要がありました。

flaskでは、実行コマンドでファイルの指定をしない場合は、app.pyスクリプトを最初に実行してWEBサーバーを立ち上げます。この時、app.pyの中にテーブル設定を含んだクラスが定義されていれば、app.pyの中だけで完結するのですが、今回はテーブル情報をapp.pyと別の場所(Modelフォルダ)で管理しているので、Usersクラスをmigrateインスタンスのあるスクリプト(app.py)にimportしないといけないです。

しかし、Usersクラスでは、db.Modelクラスからの継承を使ってスキーマ情報を定義するので、flask_sqlalchemyのimportが必要となります。また、flask_migrateの初期化には ```migrate(app, db)``` という様にsqlalchemyをimportをする必要もあります。

当初の勘違いとしては、インスタンス化と初期化を同時に行う必要があると思っていたので、
Users.pyの中でflask_sqlalchemyの初期化をするために appインスタンスをapp.pyからimportしていました。

これだと、app.pyの中からもUsers.pyをimportしているので、循環参照が生じてしまいました。

そのため以下の順番での処理をする様にextension.pyを設定しました。
```
1. extension.pyで flask_sqlalchemyとflask_migrateのインスタンスを作成
2. Users.pyで extension.pyから flask_sqlalchemyのインスタンスをimport
3. app.pyで extension.pyとUsers.pyから　それぞれflask_sqlalchemyインスタンスと、Usersクラスをimport
4. app.pyで flask_sqlalchemyとflask_migrateを初期化(~_init(app,db))
```

:::note info
公式からのTipsですが、この様にcreate_app関数を作れば実行タイミングを指定しやすいのでいいですね。

```python: sample.py
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

def create_app():
     """Application-factory pattern"""
     ...
     ...
     db.init_app(app)
     migrate.init_app(app, db)
     ...
     ...
     return app

```
:::

##### ・データベースのマイグレーション手順
flask_migrateのマイグレーションの手順を以下に記載します。

**初期化 - migrationsフォルダを生成**
```flask db init```　 

**マイグレーション** - pythonスクリプト内の定義を記載したテキストファイルが生成される
=>　差分を完全に感知できない(テーブル名の変更など)ため、手動で更新も必要
```flask db migrate -m "initial migration"```

**更新** - 生成したバージョンにSQliteを更新する
```flask db upgrade```

:::note info
コマンドラインはflaskの環境変数FLASK_APPに記載したファイル名(default=app.py)に基づいて実行されます。
このファイル内でFlaskのインスタンス化(app=Flask(__name__)))が行われている事が必要になります。
:::

**バージョン指定更新**
```flask db upgrade {バージョン名*{}は不要}```

</details>


<details><summary>sqlite_func.py</summary>

## 概要
```
・SQLiteのCRUD操作に用いる関数を用意したスクリプト
・戻り値をdict型で返す様にしている
```

### 解説ポイント
#### ・ Sqlite3の使い方
こちらの参考書のコードを参考に作成した

:::note info
Pythonではじめる Webサービス&スマホアプリの書きかた・作りかた

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/684559/b1372586-18fa-3d54-952a-7e3fd767b41a.png)
https://amzn.asia/d/1fa3Dbf
:::

##### ・SQLite3とは
- SQlite3は .sqlite3 拡張子ファイルの中にデータを格納するRDBMSです。
- サーバーレスアーキテクチャアプリケーションの中だけで完結します。
    - アプリを閉じればデータベースも閉じるような関係性
- トランザクション対応
    - ACID(Atomicity, Consistency, Isolation, Durabilty)トランザクションをサポートしている
        -  原子性(トランザクションは完了したか、無しか) : ロールバックの存在
        -  一貫性 : トランザクションが完了した後は、データベースの制約が満たされている(外部キー, ユニーク性)
        -  独立性 : 複数のトランザクションを順番に独立して実行する。排他制御。
        -  耐久性 : トランザクション後、システム障害があっても結果が失われることはない。トランザクションログ。
- ゼロ構成
    - インストールや設定が不要なのでダウンロード後すぐに利用できる。

:::note info
SQLiteの面白い記事 : 

利用者は数十億人！？ SQLiteはどこが凄いデータベース管理システムなのか調べてみた
https://qiita.com/ko1nksm/items/87d27a287e1b6005d11c

:::

### ・SQLite3の使い方
- 接続の仕方 : 

- **open_db関数**
    - sqlite3への完全PATHを使いデータベースへのアクセスをする
    - connectionオブジェクトのrow_factory属性にdict_factoryを代入する
        - カーソルオブジェクトによる戻り値を辞書型にするメソッド
        - 元々は、タプル型(順不同, 追加不能の固定されたリスト())で出力されるが、今回は辞書型で出力させるためにこの様な仕様にした
        - cursorオブジェクトで戻り値を設定する事が多いらしいが、今回はconnectionオブジェクトでグロバルに辞書型に設定した。
    - connection オブジェクト
        - データベースとの接続を表し、トランザクション管理(commit(), rollback())を行う
    - cursor　オブジェクト
        - connectionオブジェクトから作成され、SQLの実行をするためのインターフェース(fetchone(), fetchall())を持つ

```python: sqlite_func.py
import os, sqlite3

# パスの指定
BASE_DIR = os.path.dirname(__file__) # ファイルへの完全PATHを取得
DB_FILE = BASE_DIR + '/sqlite/db.sqlite3' # sqlite3ファイルに対しては完全PATHを指定する必要がある


def open_db():
    conn = sqlite3.connect(DB_FILE)
    conn.row_factory = dict_factory
    return conn

# SWLWCT句の結果を辞書で得られるようにする
def dict_factory(cursor, row):
    d = {}
    for idx, col in enumerate(cursor.description):
        d[col[0]] = row[idx]
    return d
```

**・sqlの実行メソッド**
```python: sqlite_func.py
# SQLを実行する時
def exec(sql, *args):
    db = open_db()
    c = db.cursor()
    c.execute(sql, *args)
    db.commit()
    return c.lastrowid

# SQLを実行して結果を得る
def select(sql, *args):
    db = open_db()
    c = db.cursor()
    c.execute(sql, args)
    return c.fetchall()
```

##### ・引数の *argsとは?
- pythonの引数のルールとして、位置引数とキーワード引数という渡し方があります。
- *argsは、位置引数を受け取る事ができる
- **kwargsは、キーワード引数を受け取る事ができる

- argsは関数に渡された位置引数をタプル型として扱う
- kwargsは関数に渡されるキーワード引数を、辞書型として扱う

- **kwargsはprint()関数で使えないのは、**kwargsを渡された関数内部でどの変数に引数を代入するかという処理が走るためである。kwargsとしてであれば辞書型オブジェクトとして扱ってくれるので、print()関数では使える。

```python:sample.py
def sample(*args, **kwargs):
    print('*args : ', *args)
    print('args : ', args)
    print('type args : ', type(args))
    # print('**kwargs : ', **kwargs) 
    # **kwargsはキーワード引数を辞書型に展開して受け取る。
    # print()関数の中には id や nameというキーワードを持っていない
    # キーワードを入れる所がないとTypeerrorになる。
    print('kwargs : ', kwargs)
    print('type kwargs : ', type(kwargs))

if __name__=='__main__':
    sample_list = [1,2,3]
    sample_tuple = (1,2,3)
    sample_dict = {'id':1, 'name':'john'}
    sample(sample_list)
    print('------------------------------')
    sample(sample_tuple)
    print('------------------------------')
    sample(sample_dict)
    print('------------------------------')
    sample(sample_list,**sample_dict)
```
```cl: 戻り値
~/De/develop/flask_x main !7 ?2 > python sample.py
*args :  [1, 2, 3]
args :  ([1, 2, 3],)
type args :  <class 'tuple'>
kwargs :  {}
type kwargs :  <class 'dict'>
------------------------------
*args :  (1, 2, 3)
args :  ((1, 2, 3),)
type args :  <class 'tuple'>
kwargs :  {}
type kwargs :  <class 'dict'>
------------------------------
*args :  {'id': 1, 'name': 'john'}
args :  ({'id': 1, 'name': 'john'},)
type args :  <class 'tuple'>
kwargs :  {}
type kwargs :  <class 'dict'>
------------------------------
*args :  [1, 2, 3]
args :  ([1, 2, 3],)
type args :  <class 'tuple'>
kwargs :  {'id': 1, 'name': 'john'}
type kwargs :  <class 'dict'>
```



</details>

-------------

以下時間切れ。また機会があれば書きますm(_ _ )m....

<details><summary>sns_user.py</summary>

```
・新規アカウントの作成処理と、ログイン処理を担う
・ユーザ検索結果の取得など、Usersテーブルに関する処理を行っている
```
</details>

<details><summary>relation.py</summary>

```
・ユーザのフォロー関係を司るデータベースのCRUD操作を行う処理を含むスクリプト
```
</details>

<details><summary>sns_data.py/sns_posts.py </summary>

```
・投稿内容に関するテーブルPost_contentsテーブルに関連する処理を司る
・get_app()を持っており、循環参照を回避している
・設計時は分けて作成していたが、統合しても良い
```
</details>