2022-12-28

flask

python

Posted by

applemango

今回はflask-SQLAlchemysqlite3を用いてデータベースを実装していきます

requirements.txt

Flask==2.1.3
Flask-SQLAlchemy==2.5.1
(venv) A:\abc\osaka\main\api>pip install -r requirements.txt

今回の土台となるプログラム

configを適当に設定して12行目(db = SQLAlchemy(app))でdbを定義している以外特に変わったことはしていない、軽くconfigの解説をしておく

SQLALCHEMY_DATABASE_URIでdbの位置を設定している

SQLALCHEMY_TRACK_MODIFICATIONSを設定する必要性は無いが設定をせずにいると警告が出るため設定をしている

import os, json
from flask import Flask
from datetime import timedelta
from flask_sqlalchemy import SQLAlchemy

basedir = os.path.abspath(os.path.dirname(__file__))
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    ,SQLALCHEMY_TRACK_MODIFICATIONS = False
)
db = SQLAlchemy(app)

@app.route("/", methods=["GET"])
def index():
    return "Hello"

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000)

さて早速書いていく

今回は以下ようなテーブルを作る

(疑似コードです

// ユーザー
user {
    id: number
    name: string
    password: string
}

// ユーザが投稿した記事的なもの
post {
    id: number
    user_id: number
    title: string
    body: string
}

// postのlike(ハートやLGTM的なもの
like {
    id: number
    user_id: number
    post_id: number
    like: boolean
}

まずテーブルを定義しなければならない

とりあえずUserテーブルを定義しよう

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    name = db.Column(db.String, nullable=False, unique=True)
    password = db.Column(db.String, nullable=False)

このようにテーブルはclassとして定義する

ぱっと見複雑そうに見えるが大したことはしていない

db.Columnの一つ目の引数が型を表していて、次に続く引数がオプションである

今回はオプションとしてprimary_key(User.query.get(1)などと取得できる)とautoincrement(自動でidを入れてくれる)とnullable(常に何らかの値が入っている)とunique(値が重複しない)を使用している

頻繫に使う型一覧

説明
String文字列
Integer整数
Booleanブーリアン型
BigIntegerより大きな整数
DateTimedatetime.datetime()オブジェクトを入れる、オプションとしてserver_default=func.now()を指定すると追加された日時を保存できる
Float小数

その他の型

頻繫に使うオプション一覧

オプション説明
autoincrementbool自動で入れてくれる、idなどに使う、primary_keyと一緒に使う事が多い
defaultanyデフォルトの値を設定できる
nullableboolNoneなどを許せるか
primary_keyboolこの列を主キーとして利用するか
server_defaultanyデフォルトの値を設定する、defaultは値が無い場合sqlalchemyがデフォルト値を入れるがserver_defaultではテーブルを作成(CREATETABLE)する時にデフォルト文を入れる
uniquebool値が重複するか

その他の機能

その調子でほかのテーブルも定義していく

class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    title = db.Column(db.String())
    body = db.Column(db.String())

ここで新しいのが出たdb.ForeignKeyだ、これはuser_idUseridだという事を示している、それ以上でもそれ以下でもない

"user.id"となっている、それはsqlalchemyがクラスをsqlに変換して作成しているためだ

実際にどうなっているか見てみると以下のように変換されていた

sqlite> .schema
CREATE TABLE user (
        id INTEGER NOT NULL,
        name VARCHAR NOT NULL,
        password VARCHAR NOT NULL,
        PRIMARY KEY (id),
        UNIQUE (name)
);

そのためuser.idと指定していたのだ

因みにpostはこのようになっている

CREATE TABLE post (
        id INTEGER NOT NULL,
        user_id INTEGER,
        title VARCHAR,
        body VARCHAR,
        PRIMARY KEY (id),
        FOREIGN KEY(user_id) REFERENCES user (id)
);

ここまでくれば後は楽だろう

class Like(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
    like = db.Column(db.Boolean, default=True)

これで全てのテーブルを定義できた

いったんエディターから離れターミナルで作業をしよう

そうすれば簡単にdbを操作できる

(venv) A:\abc\osaka\main\api>python
Python 3.9.4 (tags/v3.9.4:*******, **  * ****, **:**:**) [MSC v.1928 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

まずはテーブルを作成しよう

>>> from app import *
>>> db.create_all()

User

idnamepassword

Post

iduser_idtitlebody

Like

iduser_idpost_idlike

これで三つのテーブルを作成できたが当然のことながらまだデータは入っていない

まずはユーザを二人ほど追加しよう

>>> apple = User(name="apple", password="42")
>>> user = User(name="osaka", password="24")
>>> db.session.add(apple)
>>> db.session.add(user)
>>> db.session.commit()

これでUserテーブルに以下のようなデータが入った

ついでにPostも追加しておこう

>>> post_0 = Post(user_id=1, title="なぜリンゴはニュートンの前で落ちたのか", body="調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね")
>>> post_1 = Post(user_id=1, title="神はなぜリンゴを知恵の実にしたのか", body="旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね")
>>> post_2 = Post(user_id=2, title="このタイトルに1時間くらい考えた", body="一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく")
>>> db.session.add(post_0)
>>> db.session.add(post_1)
>>> db.session.add(post_2)
>>> db.session.commit()

そうそう、好きな投稿にはLikeを付けておきましょう

>>> like_0 = Like(user_id=1, post_id=1, like=True)
>>> like_1 = Like(user_id=1, post_id=3, like=True)
>>> like_2 = Like(user_id=2, post_id=2, like=True)
>>> like_3 = Like(user_id=2, post_id=3, like=True)
>>> db.session.add(like_0)
>>> db.session.add(like_1)
>>> db.session.add(like_2)
>>> db.session.add(like_3)
>>> db.session.commit()

参考までに現在のデータは、下のような感じです

User

idnamepassword
1apple42
2osaka24

Post

iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね
32このタイトルに1時間くらい考えた一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく

Like

iduser_idpost_idlike
111true
213true
322true
423true

これくらいのデータがあれば試せるだろう

早速試してみよう

データを取得する

idから取得する

まずは簡単に主キーで取得しよう、今回は主キーにidを指定しているのでidを引数に置く

>>> user = User.query.get(1)
>>> user
idnamepassword
1apple42

値を取り出す

値の取得も簡単にできる

>>> user.name
apple

全てのデータを取得する

全てのユーザーを取得するには.all()を使う

>>> users = User.query.all()
>>> users
idnamepassword
1apple42
2osaka24

複数の場合はリスト形式なのでfor文などで取り出せる

.first().get()以外は基本リスト形式です

>>> for user in users:
...     print(user.name)
...
apple
osaka

最初のユーザーのみを取得するには.first()を使う

>>> user = User.query.first()
>>> user
idnamepassword
1apple42

特定の値を持つデータのみ取得する

特定の値を持つデータを取得するにはfilterを使います

例ではUser idが1の人(apple)が投稿したデータを取得しています

>>> posts = Post.query.filter(Post.user_id == 1).all()
>>> posts
iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね

検索

もし特定の文字列が含まれるものを取得したい場合は.contains()を使います

>>> posts = Post.query.filter(Post.body.contains("旧約聖書")).all()
>>> posts
iduser_idtitlebody
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね

今回の場合は意味は無いですが.filter()は重ねがけすることが可能です

>>> posts = Post.query.filter(Post.body.contains("旧約聖書")).filter(Post.user_id == 1).all()
>>> posts
iduser_idtitlebody
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね

filterでif文を使う

複数のfilterを使いたい場合、一つだけ適応したくない場合があるかもしれません、こういう場合はif文を使えます

>>> user = 1
>>> q = ""
>>> post = Post.query \
...         .filter(Post.user_id == user if user else True) \
...         .filter(Post.body.contains(q) if q else True)
iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
21神はなぜリンゴを知恵の実にしたのか旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね

自分の好きな投稿を取得

現在post、like、userは別々のテーブルにありますが、userがlikeした投稿を取得するにはこれをまとめる必要があります

まとめて取得するには以下のようにします

>>> post = Post.query.join(Like, (Like.post_id == Post.id)) \
...     .filter(Like.like, Like.user_id == 1) \
...     .all()

少し複雑なので解説しましょう

まず.joinを使ってLikePostを合体させています

この段階では以下のようになっています

Likeテーブルのpost_idを参考にLikeテーブルにPostテーブルを引っ付けた感じです

post.idpost.user_idpost.titlepost.bodylike.idlike.user_idlike.post_idlike.like
11なぜ..調べ..111true
32この..一応..213true
21神は...旧約..322true
32この..一応..423true

更にこれにfilterをかけると以下のようになります

post.idpost.user_idpost.titlepost.bodylike.idlike.user_idlike.post_idlike.like
11なぜ..調べ..111true
32この..一応..213true

最後に.all()を適応すると取得できます

iduser_idtitlebody
11なぜリンゴはニュートンの前で落ちたのか調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね
32このタイトルに1時間くらい考えた一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく

データを更新する

sqlalchemyでデータを更新するには代入を使用します

>>> user = User.query.get(1)
>>> user.name = "WorldWideWeb"
>>> db.session.commit()

一括でデータを更新する

一括でデータを変更する場合は代入できません

Update関数を使います

>>> user = User.query
>>> user.update({User.password:"BigAdminIsWatchingYou"})
>>> db.session.commit()
>>> user = User.query.filter(User.id == 1)
>>> user.update({User.password:"42"})
>>> db.session.commit()

データを削除する

削除にはdb.session.delete()を使う事が一般的です

>>> user = User.query.get(2)
>>> db.session.delete(user)
>>> db.session.commit()

一括でデータを削除する

.updateと同じように.deleteが使えます

>>> user = Post.query.filter(Post.user_id == 1).delete()
>>> db.session.commit()

全てを削除する

リセットする場合などはdb.drop_all()を使います

本当に全てが消えるので注意が必要です、再度使うには冒頭のdb.create_all()を実行する必要があります

>>> db.drop_all()

終わり

これで一通り出来ました、多分

このドキュメントどう?

emoji
emoji
emoji
emoji