2022-10-17

flask

python

Posted by

applemango

最終更新: 2023-03-05

さて、今回はdbです

今回はsqlite3を使います

忘れないうちに設定をしておきましょう

やっと設定の出番が来ましたね

app.config.from_mapping( #アプリの設定
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
        'sqlite:///' + os.path.join(basedir, 'app.db')
    ,SQLALCHEMY_TRACK_MODIFICATIONS = False
)

SQLALCHEMY_DATABASE_URIはdbの位置を表しています

SQLALCHEMY_TRACK_MODIFICATIONSは無くても良いですが無いと警告が出でるためFalseを設定しています

デフォルトだとNullでTrue又はFalseに設定したら警告は消えます

dbは以下のようにして定義します

db = SQLAlchemy(app)

ですがこれだけだと意味が有りません

モデルを作成する必要が有ります

modelはclassとして作成できます

今回のmodelはこんな感じです

class Url(db.Model):  # type: ignore
    id = db.Column(db.Integer, primary_key=True)
    url = db.Column(db.String(4096), unique=True, nullable=False)

db.Columnで新しいColumnを作成します

db.Integerは整数の値をidに保存する事を表し

primary_keyは主キーである事を表します

db.Stringは文字列である事を表し

uniqueは重複する事が無い事

nullbleはNull値になる事が無い事を表します

したがって

idは整数であり主キーでもある

さてurlは次のうちどれがより適切に表現しているだろうか

info

urlを4096字に制限してるのはsafariの上限が4096字だからです

RFC7230なんて無かった

https://www.rfc-editor.org/rfc/rfc7230.txt (P21

今app.pyはこのような形になっていると思います

import os #pathの取得などに使う
import json #何かと使う
from flask import Flask # flaskを使うのに絶対必要
from flask import jsonify # jsonを送るのに使う
from flask import request # queryなどを取得するのに使う
from flask_cors import CORS, cross_origin#crosの設定
from flask_sqlalchemy import SQLAlchemy#databaseを使うために必要

basedir = os.path.abspath(os.path.dirname(__file__)) #実行folderのpathを取得、何かと使う

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') #dbの位置
    ,SQLALCHEMY_TRACK_MODIFICATIONS = False #警告よけ
)
cors = CORS(app, responses={r"/*": {"origins": "*"}}) # cors error対策
db = SQLAlchemy(app) # dbの作成

class Url(db.Model):  # type: ignore
    id = db.Column(db.Integer, primary_key=True)
    url = db.Column(db.String(4096), unique=True, nullable=False)

@app.route("/get/<id>")
def get_url(id):
    return jsonify({"url":"https://example.com"})

@app.route("/create", methods=["POST"])
@cross_origin()
def create_url():
    return jsonify({"url":"https://exmaple.com"})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000) # アプリの実行

ここで一回cmdでDBを操作してみましょう

まず仮想環境に入り

A:/abc>cd A:\abc\osaka\main\api

A:/abc/osaka/main/api>venv\Scripts\activate

(venv) A:\abc\osaka\main\api>
apple@apple ~ % cd /abc/osaka/main/api

apple@apple api % source venv/bin/activate 
(venv) apple@apple api % 

pythonを起動します

(venv) A:\abc\osaka\main\api>python
Python 3.9.4 ()
Type "help", "copyright", "credits" or "license" for more information.
>>>
(venv) apple@apple api % python
Python 3.11.2 (v3.11.2,) [Clang 13.0.0] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 

起動出来たらappからdbとmodel(Url)を読み込みましょう

>>> from app import db, Url

そして色々と試す前にまずはdbを作らないといけません

さっき作ったじゃないかと思うかもしれないがあれは定義しただけで作った訳では無いのです

>>> db.create_all()

で作る

warning

複数のErrorが環境によっては出ます

これはflask sqlalchemyのバージョンが原因である可能性が有ります

Flask-SQLAlchemy==3.0.2で出る事を確認しました

Flask-SQLAlchemy==2.5.1では出ませんでした

versionの確認は>pip freeze でできます

>>>db

で実行結果が

<SQLAlchemy> の場合出ます

<SQLAlchemy engine=sqlite:///A:\abc\osaka\main\api\app.db>

などのengineが指定されてる場合は出ませんでした

追記 2023-03-05

sqlalchemyバージョンが原因でエラーが出ることを確認しました

sqlalchemyのversionが2.0になり一部の互換性が失われたためだと思います

SQLAlchemy==2.0.4では出ました

SQLAlchemy==1.4.41では出ませんでした

その際下記のようなErrorが出るかもしれません

>>> db
<SQLAlchemy>
>>> db.drop_all()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "A:\abc\osaka\main\api\venv\lib\site-packages\flask_sqlalchemy\extension.py", line 885, in drop_all
    self._call_for_binds(bind_key, "drop_all")
  File "A:\abc\osaka\main\api\venv\lib\site-packages\flask_sqlalchemy\extension.py", line 839, in _call_for_binds
    engine = self.engines[key]
  File "A:\abc\osaka\main\api\venv\lib\site-packages\flask_sqlalchemy\extension.py", line 628, in engines
    app = current_app._get_current_object()  # type: ignore[attr-defined]
  File "A:\abc\osaka\main\api\venv\lib\site-packages\werkzeug\local.py", line 513, in _get_current_object
    raise RuntimeError(unbound_message) from None
RuntimeError: Working outside of application context.

This typically means that you attempted to use functionality that needed
the current application. To solve this, set up an application context
with app.app_context(). See the documentation for more information.

errorが出た場合下記のコマンドを実行してみてください

pythonからはctrl + zで出てください

(venv) A:\abc\osaka\main\api>pip install Flask-SQLAlchemy==2.5.1
(venv) apple@var-apple api % pip install Flask-SQLAlchemy==2.5.1

時がたちsqlalchemyのversionが2.0になり、以下のようなエラーが出る可能性が出てきました

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/abc/osaka/main/api/app.py", line 18, in <module>
    db = SQLAlchemy(app) # dbの作成
         ^^^^^^^^^^^^^^^
  File "/abc/osaka/main/api/venv/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py", line 758, in __init__
    _include_sqlalchemy(self, query_class)
  File "/abc/osaka/main/api/venv/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py", line 112, in _include_sqlalchemy
    for key in module.__all__:
               ^^^^^^^^^^^^^^
AttributeError: module 'sqlalchemy' has no attribute '__all__'. Did you mean: '__file__'?

出た場合は下記のコマンドを実行してみてください

(venv) A:\abc\osaka\main\api>pip install SQLAlchemy==1.4.41
(venv) apple@apple api % pip install SQLAlchemy==1.4.41

それでまだ出た場合は同じようにバージョンを指定し

これを全てinstallしてみてください

click==8.1.3
colorama==0.4.5
Flask==2.2.2
Flask-Cors==3.0.10
Flask-SQLAlchemy==2.5.1
greenlet==1.1.3.post0
importlib-metadata==5.0.0
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.1
six==1.16.0
SQLAlchemy==1.4.41
Werkzeug==2.2.2
zipp==3.9.0

これで出たら原因はもはや分かりません

google itしてください(googleは動詞)

リセットする場合は

>>>db.drop_all()
#消したら再度使うには作り直す必要がある
>>>db.create_all()

これでできます

試しにurlを保存してみましょう

>>>u = Url(url="https://example.org") #uをUrlとして定義
>>>db.session.add(u) #uをdbに追加する
>>>db.session.commit() #変更を保存

取得するには

>>>u = Url.query.get(1) #主キー(id)で取得
>>>u.id, u.url

#又は

>>>u = Url.query.all() #全て取得あまり使わない

>>>u = Url.query.first() #最初のを取得

>>>u = Url.query.filter_by(url = "https://example.org").first() #検索.firstは複数あった場合最初の一つで十分だから

値の変更もできる

>>>u = Url.query.get(1)
>>>u.id, u.url
>>>u.url = "http://example.com/about"
>>>db.session.add(u)
>>>db.session.commit()
>>>u.id, u.url

消すにも簡単

>>>u = Url.query.get(1)
>>>db.session.delete(u)
>>>db.session.commit()

これで一通り基本的な操作はできました

ほんとはMigrateなどもありますが複雑化する為今回は使用しません

まあそんな話は置いといサックっとdbを実装しましょう

一応dbはリセットしておいてください

因みに今はこうなっているはずです

import os #pathの取得などに使う
import json #何かと使う
from flask import Flask # flaskを使うのに絶対必要
from flask import jsonify # jsonを送るのに使う
from flask import request # queryなどを取得するのに使う
from flask_cors import CORS, cross_origin#crosの設定
from flask_sqlalchemy import SQLAlchemy#databaseを使うために必要

basedir = os.path.abspath(os.path.dirname(__file__)) #実行folderのpathを取得、何かと使う

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') #dbの位置
    ,SQLALCHEMY_TRACK_MODIFICATIONS = False #警告よけ
)
cors = CORS(app, responses={r"/*": {"origins": "*"}}) # cors error対策
db = SQLAlchemy(app) # dbの作成

class Url(db.Model):  # type: ignore
    id = db.Column(db.Integer, primary_key=True)
    url = db.Column(db.String(4096), unique=True, nullable=False)

@app.route("/get/<id>")
def get_url(id):
    return jsonify({"url":"https://example.com"})

@app.route("/create", methods=["POST"])
@cross_origin()
def create_url():
    return jsonify({"url":"https://exmaple.com"})

if __name__ == '__main__':
    app.run(debug=True, host='0.0.0.0', port=5000) # アプリの実行

今回はdbのversion管理がめんどくさいので変更を加える度にdbをdropして作り直すようにします

if __name__ == '__main__':
    db.drop_all()
    db.create_all()
    db.session.commit()
    app.run(debug=True, host='0.0.0.0', port=5000) # アプリの実行

これで面倒な作業を致命的な欠陥と引き換えに得られました

(programを変更するたびにdataが飛ぶのでその都度対応してください

(次章ではちゃんとします

さぁ後は関数を二つ修正するだけです

get_urlはurlをdbから取得するように変更しましょう

簡単そうですね

作り終わったのが以下のものになります

@app.route("/get/<int:id>")
def get_url(id):
    u = Url.query.get(id)
    if not u:
        return jsonify({"url":"http://127.0.0.1:3000/"}), 200
    return jsonify({"url":u.url})

ここまで来たらもう理解できますね?

if not uはもし存在しない場合はNoneが返されるからです

無い場合は適当に返してもいいですが今回はurl作成pageに飛ぶようにしました

さぁ最後はcreate_urlを修正しましょう

まずは送られてきたurlを取得する必要が有ります

urlはrequest.dataにbyte形式で入っています

あまり美しくないコードですが動けば問題ありません以下のコードを使いましょう

url = json.loads(json.loads(request.data.decode('utf-8'))["body"])["url"]

後はdbに追加して返すだけです

@app.route("/create", methods=["POST"])
@cross_origin()
def create_url():
    url = json.loads(json.loads(request.data.decode('utf-8'))["body"])["url"]
    if not url:
        return jsonify({"url":"oh"})
    if Url.query.filter_by(url=url).first():
        return jsonify({"url": "127.0.0.1:3000/" + str(Url.query.filter_by(url=url).first().id)})
    u = Url(url=url)
    db.session.add(u)
    db.session.commit()
    return jsonify({"url":"127.0.0.1:3000/" + str(u.id)})

最終的にこのような形になりました

import os #pathの取得などに使う
import json #何かと使う
from flask import Flask # flaskを使うのに絶対必要
from flask import jsonify # jsonを送るのに使う
from flask import request # queryなどを取得するのに使う
from flask_cors import CORS, cross_origin#crosの設定
from flask_sqlalchemy import SQLAlchemy#databaseを使うために必要

basedir = os.path.abspath(os.path.dirname(__file__)) #実行folderのpathを取得、何かと使う

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') #dbの位置
    ,SQLALCHEMY_TRACK_MODIFICATIONS = False #警告よけ
)
cors = CORS(app, responses={r"/*": {"origins": "*"}}) # cors error対策
db = SQLAlchemy(app) # dbの作成

class Url(db.Model):  # type: ignore
    id = db.Column(db.Integer, primary_key=True)
    url = db.Column(db.String(4096), unique=True, nullable=False)

@app.route("/get/<int:id>")
def get_url(id):
    u = Url.query.get(id)
    if not u:
        return jsonify({"url":"http://127.0.0.1:3000/"}), 200
    return jsonify({"url":u.url})

@app.route("/create", methods=["POST"])
@cross_origin()
def create_url():
    url = json.loads(json.loads(request.data.decode('utf-8'))["body"])["url"]
    if not url:
        return jsonify({"url":"oh"})
    if Url.query.filter_by(url=url).first():
        return jsonify({"url": "127.0.0.1:3000/" + str(Url.query.filter_by(url=url).first().id)})
    u = Url(url=url)
    db.session.add(u)
    db.session.commit()
    return jsonify({"url":"127.0.0.1:3000/" + str(u.id)})

if __name__ == '__main__':
    db.drop_all()
    db.create_all()
    db.session.commit()
    app.run(debug=True, host='0.0.0.0', port=5000) # アプリの実行

これでできました早速使ってみましょう

ちゃんと動いているでしょうか

一応確認用のスクリプトを載せておく

info

urlに${id}がついてるのは内部的に...

MDを変換する限界かな

bodyとかheaderとかも同じだから気にしないで

多分動いているだろう

warning

ここまで飛ばしてきた人はいないと思うけど一応書いておこう

-

This typically means that you attempted to use functionality that needed

the current application. To solve this, set up an application context

with app.app_context(). See the documentation for more information.

-

もしこんな感じのErrorが出たら

以下のcommandを実行してみてください

>pip install Flask-SQLAlchemy==2.5.1

もしそれでも解決しなかった場合は一応

>python

>>>from app import db

>>>db.drop_all()

>>>db.create_all()

場合によってはdb.drop_all()でエラーが出るかもしれないが

多分大丈夫

後はお好みでcssを書いたり

今はidを直接10進数でやり取りしていますが効率を考え64進数に変換したり

アクセス数を取ったりしても良いかもしれません

最終的なコードはここに置いておきます

ではまた

このドキュメントどう?

emoji
emoji
emoji
emoji