2022-12-28
Posted by
今回はflask-SQLAlchemy
とsqlite3
を用いてデータベースを実装していきます
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 | より大きな整数 |
DateTime | datetime.datetime()オブジェクトを入れる、オプションとしてserver_default=func.now()を指定すると追加された日時を保存できる |
Float | 小数 |
その他の型
頻繫に使うオプション一覧
オプション | 型 | 説明 |
---|---|---|
autoincrement | bool | 自動で入れてくれる、idなどに使う、primary_keyと一緒に使う事が多い |
default | any | デフォルトの値を設定できる |
nullable | bool | Noneなどを許せるか |
primary_key | bool | この列を主キーとして利用するか |
server_default | any | デフォルトの値を設定する、defaultは値が無い場合sqlalchemyがデフォルト値を入れるがserver_defaultではテーブルを作成(CREATETABLE)する時にデフォルト文を入れる |
unique | bool | 値が重複するか |
その他の機能
その調子でほかのテーブルも定義していく
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_id
がUser
のid
だという事を示している、それ以上でもそれ以下でもない
"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
id | name | password |
---|
Post
id | user_id | title | body |
---|
Like
id | user_id | post_id | like |
---|
これで三つのテーブルを作成できたが当然のことながらまだデータは入っていない
まずはユーザを二人ほど追加しよう
>>> 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
id | name | password |
---|---|---|
1 | apple | 42 |
2 | osaka | 24 |
Post
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
3 | 2 | このタイトルに1時間くらい考えた | 一応これも考えたけどappleと違ってosakaはネタが少ないし十分なエビデンスが得られなかったからこれでいく |
Like
id | user_id | post_id | like |
---|---|---|---|
1 | 1 | 1 | true |
2 | 1 | 3 | true |
3 | 2 | 2 | true |
4 | 2 | 3 | true |
これくらいのデータがあれば試せるだろう
早速試してみよう
データを取得する
idから取得する
まずは簡単に主キーで取得しよう、今回は主キーにidを指定しているのでidを引数に置く
>>> user = User.query.get(1)
>>> user
id | name | password |
---|---|---|
1 | apple | 42 |
値を取り出す
値の取得も簡単にできる
>>> user.name
apple
全てのデータを取得する
全てのユーザーを取得するには.all()
を使う
>>> users = User.query.all()
>>> users
id | name | password |
---|---|---|
1 | apple | 42 |
2 | osaka | 24 |
複数の場合はリスト形式なのでfor文などで取り出せる
.first()
や.get()
以外は基本リスト形式です
>>> for user in users:
... print(user.name)
...
apple
osaka
最初のユーザーのみを取得するには.first()
を使う
>>> user = User.query.first()
>>> user
id | name | password |
---|---|---|
1 | apple | 42 |
特定の値を持つデータのみ取得する
特定の値を持つデータを取得するにはfilter
を使います
例ではUser idが1の人(apple)が投稿したデータを取得しています
>>> posts = Post.query.filter(Post.user_id == 1).all()
>>> posts
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
検索
もし特定の文字列が含まれるものを取得したい場合は.contains()
を使います
>>> posts = Post.query.filter(Post.body.contains("旧約聖書")).all()
>>> posts
id | user_id | title | body |
---|---|---|---|
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
今回の場合は意味は無いですが.filter()
は重ねがけすることが可能です
>>> posts = Post.query.filter(Post.body.contains("旧約聖書")).filter(Post.user_id == 1).all()
>>> posts
id | user_id | title | body |
---|---|---|---|
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
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)
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
2 | 1 | 神はなぜリンゴを知恵の実にしたのか | 旧約聖書にそうした記述はありませんでした、今後のリンゴの活躍に期待ですね |
自分の好きな投稿を取得
現在post、like、userは別々のテーブルにありますが、userがlikeした投稿を取得するにはこれをまとめる必要があります
まとめて取得するには以下のようにします
>>> post = Post.query.join(Like, (Like.post_id == Post.id)) \
... .filter(Like.like, Like.user_id == 1) \
... .all()
少し複雑なので解説しましょう
まず.join
を使ってLike
とPost
を合体させています
この段階では以下のようになっています
Like
テーブルのpost_id
を参考にLike
テーブルにPost
テーブルを引っ付けた感じです
post.id | post.user_id | post.title | post.body | like.id | like.user_id | like.post_id | like.like |
---|---|---|---|---|---|---|---|
1 | 1 | なぜ.. | 調べ.. | 1 | 1 | 1 | true |
3 | 2 | この.. | 一応.. | 2 | 1 | 3 | true |
2 | 1 | 神は... | 旧約.. | 3 | 2 | 2 | true |
3 | 2 | この.. | 一応.. | 4 | 2 | 3 | true |
更にこれにfilter
をかけると以下のようになります
post.id | post.user_id | post.title | post.body | like.id | like.user_id | like.post_id | like.like |
---|---|---|---|---|---|---|---|
1 | 1 | なぜ.. | 調べ.. | 1 | 1 | 1 | true |
3 | 2 | この.. | 一応.. | 2 | 1 | 3 | true |
最後に.all()
を適応すると取得できます
id | user_id | title | body |
---|---|---|---|
1 | 1 | なぜリンゴはニュートンの前で落ちたのか | 調べた結果分かりませんでした、今後のリンゴの活躍に期待ですね |
3 | 2 | このタイトルに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()
終わり
これで一通り出来ました、多分
このドキュメントどう?