フラスコメガチュートリアル、パート4:データベース(2018年版)

blog.miguelgrinberg.com







ミゲル・グリンバーグ






<<<前 次>>>







この記事は、ミゲルグリーンバーグの教科書の新版の第4部の翻訳です。 以前の翻訳は、その関連性を長く失いました。







これは、Flask Mega-Tutorialシリーズの第4版で、データベースの操作方法を説明します。







参考までに、以下はこのシリーズの記事のリストです。









注1:このコースの古いバージョンをお探しの場合は、こちらをご覧ください







注2:突然、このブログの私の(ミゲル)の仕事を支持して声を上げたい場合、または単に記事を1週間待つ忍耐がない場合、私(ミゲルグリーンバーグ)はこのガイドの完全版にパッケージ化された電子書籍またはビデオを提供します。 詳細については、 learn.miguelgrinberg.comをご覧ください







この章のトピックは非常に重要です。 ほとんどのアプリケーションでは、効率的に取得できる永続データを維持する必要があり、これがまさにデータベースの作成目的です。







この章のGitHubリンク: BrowseZipDiff







Flaskのデータベース



既に聞いたことがあると思うので、Flaskはデータベースをネイティブにサポートしていません。 これは、Flaskが意図的に自給自足的でない多くの分野の1つです。これは、1つに適応するのではなく、アプリケーションに最適なデータベースを自由に選択できるため素晴らしいことです。







Pythonには多数のデータベースがあり、その多くはFlaskアプリケーションと統合されています。 データベースは、リレーショナルモデルに対応するグループとそうでないグループの2つの大きなグループに分けることができます。 後者のグループはしばしばNoSQLと呼ばれ、一般的なSQLリレーショナルクエリ言語を実装していないことを示しています。 どちらのグループにも優れたデータベース製品がありますが、リレーショナルデータベースは、ユーザーリスト、ブログ投稿などの構造化データを持つアプリケーションに適していると思います。構造が定義されていないデータの方が良い傾向。 このアプリケーションは、他のほとんどのアプリケーションと同様に、任意のタイプのデータベースを使用して実装できますが、上記の理由から、リレーショナルデータベースを使用します。







第3章では、最初のFlask拡張機能を紹介しました。 この章では、さらに2つ使用します。 1つはFlask-SQLAlchemyです。これは、人気のSQLAlchemyパッケージ( Object Relational MapperまたはORM)のFlaskフレンドリーラッパーを提供する拡張機能です。 ORMを使用すると、アプリケーションは、テーブルやSQLではなく、クラス、オブジェクト、メソッドなどの高レベルオブジェクトを使用してデータベースを管理できます。 ORMの目標は、高レベルの操作をデータベースコマンドに変換することです。







SQLAlchemyの最良の部分は、このORMは1つではなく、多くのリレーショナルデータベース用であることです。 SQLAlchemyは、一般的なMySQLPostgreSQL 、およびSQLiteを含むデータベースエンジンの長いリストをサポートしています。 サーバーを必要としない単純なSQLiteデータベースで開発を行うことができるため、これは非常に強力です。そして、実稼働サーバーにアプリケーションをデプロイするときが来ると、アプリケーションを変更せずに、より信頼性の高いMySQLまたはPostgreSQLサーバーを選択できます。







Flask-SQLAlchemyを仮想環境にインストールするには、必ずそれをアクティブにしてから実行してください:







(venv) $ pip install flask-sqlalchemy
      
      





データベースの移行



私が見たデータベースチュートリアルのほとんどは、データベースの作成と使用を扱っていますが、アプリケーションを変更または増加する必要があるため、既存のデータベースの更新を作成する問題を適切に解決しません。 リレーショナルデータベースは構造化されたデータに集中しているため、これは困難です。そのため、既にデータベースにあるデータの構造を変更する場合は、変更された構造に転送する必要があります。







この章で紹介する2番目の拡張機能はFlask-Migrateです 。これは実際には謙虚な使用人によって作成されます。 この拡張機能は、SQLAlchemyデータベース移行の基盤であるAlembicのFlaskラッパーです。 データベースの移行に取り組むと、最初は少し手間がかかりますが、将来的にデータベースに変更を加えるための信頼できる方法にお金を払うのは少額です。







Flask-Migrateのインストールプロセスは、他の拡張機能と似ています。







 (venv) $ pip install flask-migrate
      
      





Flask-SQLAlchemyの構成



開発中に、SQLiteデータベースを使用します。 SQLiteデータベースは、各アプリケーションがディスク上の単一のファイルに保存され、MySQLやPostgreSQLなどのデータベースサーバーを起動する必要がないため、小さなアプリケーションを開発するのに最も便利な選択肢です。







構成ファイルに追加する2つの新しい構成項目があります。







config.py


 import os basedir = os.path.abspath(os.path.dirname(__file__)) class Config(object): # ... SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'app.db') SQLALCHEMY_TRACK_MODIFICATIONS = False
      
      





Flask-SQLAlchemy拡張機能は、 SQLALCHEMY_DATABASE_URI



構成SQLALCHEMY_DATABASE_URI



からアプリケーションデータベースの場所をSQLALCHEMY_DATABASE_URI



ます。 第3章から覚えているように、環境が変数を定義していない場合、環境変数から構成をセットアップし、フォールバック値を提供することをお勧めします。 この場合、 DATABASE_URL



環境変数からデータベースURLを取得し、定義されていない場合は、 ベースアプリケーション変数にあるapp.dbというデータベースを構成します。このデータベースは、 basedir



変数に格納されています。







SQLALCHEMY_TRACK_MODIFICATIONS



構成SQLALCHEMY_TRACK_MODIFICATIONS



をFalseに設定すると、Flask-SQLAlchemy関数が無効になります。これは、データベースに変更が加えられるたびにアプリケーションに通知する必要はありません。







データベースは、 データベースインスタンスとしてアプリケーションに表示されます 。 データベース移行エンジンにもインスタンスがあります。 これらは、 app/__ init__.py



のアプリケーションの後に作成する必要があるオブジェクトapp/__ init__.py









 from flask import Flask from config import Config from flask_sqlalchemy import SQLAlchemy from flask_migrate import Migrate app = Flask(__name__) app.config.from_object(Config) db = SQLAlchemy(app) migrate = Migrate(app, db) from app import routes, models
      
      





initスクリプトに3つの変更を加えました。 最初に、データベースを表すdb



オブジェクトを追加しました。 次に、移行メカニズムを表す別のオブジェクトを追加しました。 Flask拡張機能の使用例をご覧ください。 ほとんどの拡張機能は、この2つのように初期化されます。 最後に、以下のmodels



と呼ばれる新しいモジュールをインポートします。 このモジュールは、データベースの構造を決定します。







データベースモデル



データベースに保存されるデータは、一般にデータベースモデルと呼ばれるクラスのセットで表されます。 SQLAlchemyのORMレベルは、これらのクラスから作成されたオブジェクトを対応するデータベーステーブルの行にマッピングするために必要な変換を実行します。







ユーザーを表すモデルを作成することから始めましょう。 WWW SQL Designerツールを使用して、ユーザーテーブルで使用するデータを表す次の図を作成しました。













id



フィールドはすべてのモデルで一般的に使用され、主キーとして使用されます。 データベース内の各ユーザーには、このフィールドに格納されている一意の識別子値が割り当てられます。 ほとんどの場合、主キーはデータベースによって自動的に割り当てられるため、主キーとしてマークされたid



フィールドを指定するだけです。







username



email



およびpassword_hash



フィールドは文字列(またはデータベース用語ではVARCHAR



として定義され、データベースがスペースの使用を最適化できるように最大長が指定されています。 username



email



フィールドは一目瞭然ですが、 password_hash



フィールドは注目に値します。 作成するアプリケーションが最高のセキュリティ推奨事項を使用していることを確認したいため、データベースにユーザーパスワードを保存しません。 パスワードの保存に関する問題は、データベースが危険にさらされると、攻撃者がパスワードにアクセスできるようになり、これがユーザーに損害を与える可能性があることです。 パスワードを直接記述する代わりに、セキュリティを大幅に向上させるパスワードハッシュを記述します 。 これは別の章のトピックになるので、今は心配しないでください。







ユーザーテーブルに必要なものがわかったので、これを新しいapp / models.pyモジュールのコードに変換できます







 from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) password_hash = db.Column(db.String(128)) def __repr__(self): return '<User {}>'.format(self.username)
      
      





上記で作成されたUser



クラスは、Flask-SQLAlchemyのすべてのモデルの基本クラスであるdb.Model



継承します。 このクラスは、いくつかのフィールドをクラス変数として定義します。 フィールドはdb.Column



クラスのインスタンスとして作成されます。このクラスは、フィールドタイプを引数として使用し、他のオプションの引数を使用します。たとえば、一意のインデックス付きフィールドを指定できます。これは、効率的なデータベース検索に重要​​です。







__repr__



メソッドは、このクラスのオブジェクトを印刷する方法をPythonに指示します。これはデバッグに役立ちます。 以下のPythonインタープリターセッションで、 __repr __()



メソッドの動作を確認できます。







 >>> from app.models import User >>> u = User(username='susan', email='susan@example.com') >>> u <User susan>
      
      





リポジトリの移行を作成する



前のセクションで作成されたモデルクラスは、このアプリケーションの初期データベース構造(またはスキーマ )を定義します。 しかし、アプリケーションの成長に伴い、構造を変更する必要があります。これにより、新しいエンティティが追加される可能性がありますが、要素を変更または削除することもできます。 Alembic(Flask-Migrateで使用される移行フレームワーク)はこれらのスキーマを変更するため、データベースを最初から再作成する必要はありません。







この一見複雑なタスクを達成するために、Alembicは移行リポジトリを維持します。これは、移行スクリプトが保存されるディレクトリです。 データベーススキーマが変更されるたびに、変更に関する詳細情報を含む移行スクリプトがリポジトリに追加されます。 データベースに移行を適用するために、これらの移行スクリプトは作成された順に実行されます。







Flask-Migrateは、 flask



コマンドを介してコマンドを発行します。 Flaskにネイティブな従属コマンドであるflask run



を既に見てきました。 Flask-Migrateにより、 flask db



サブコマンドが追加され、データベースの移行に関連するすべてが管理されます。 それでは、 flask db init



実行して、マイクロブログの移行リポジトリを作成しましょう。







 (venv) $ flask db init Creating directory /home/miguel/microblog/migrations ... done Creating directory /home/miguel/microblog/migrations/versions ... done Generating /home/miguel/microblog/migrations/alembic.ini ... done Generating /home/miguel/microblog/migrations/env.py ... done Generating /home/miguel/microblog/migrations/README ... done Generating /home/miguel/microblog/migrations/script.py.mako ... done Please edit configuration/connection/logging settings in '/home/miguel/microblog/migrations/alembic.ini' before proceeding.
      
      





Flaskコマンドは、Flaskアプリケーションの場所を知るためにFLASK_APP



環境変数に依存していることにFLASK_APP



してFLASK_APP



。 このアプリケーションでは、 第1章で説明されているように、 FLASK_APP=microblog.py



を設定します。







このコマンドを実行すると、新しい移行ディレクトリが見つかります。このディレクトリには、いくつかのファイルとバージョンサブディレクトリがあります。 これで、これらのファイルはすべてプロジェクトの一部と見なされ、バージョン管理システムに追加する必要があります。







最初のデータベース移行



移行リポジトリを使用して、最初のデータベース移行を作成します。これには、 Users



データベースモデルにマップされたUsers



テーブルが含まれます。 データベース移行を作成するには、手動または自動の2つの方法があります。 自動的に移行を作成するために、Alembicはデータベースモデルによって定義されたデータベーススキーマを、データベースで現在使用されている実際のデータベーススキーマと比較します。 次に、データベーススキーマがアプリケーションモデルと一致することを確認するために必要な変更を移行スクリプトに入力します。 この場合、以前のデータベースがないため、自動移行によりUser



モデル全体が移行スクリプトに追加されます。 flask db migrate



DB transferサブコマンドは、次の自動移行を生成します。







 (venv) $ flask db migrate -m "users table" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'user' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_email' on '['email']' INFO [alembic.autogenerate.compare] Detected added index 'ix_user_username' on '['username']' Generating /home/miguel/microblog/migrations/versions/e517276bb1c2_users_table.py ... done
      
      





コマンドの出力から、Alembicが移行に含まれていることがわかります。 最初の2行は参考情報であり、通常は無視できます。 それから彼は、テーブル'user'



と2つのインデックス'['email']'



および '[' username ']'を見つけたと言います。 次に、移行スクリプトを作成した場所を説明します。 e517276bb1c2コードは、移行用に自動生成された一意のコードです(異なる場合があります)。 -m



オプションで指定されたコメントはオプションであり、ハイフンに短い説明テキストを追加します。







生成された移行スクリプトはプロジェクトの一部であり、バージョン管理システムに含める必要があります。 スクリプトがどのように見えるか興味がある場合は、スクリプトを表示できます。 upgrade()



downgrade()



2つの機能があることがわかります。 upgrade()



機能は移行を適用し、 downgrade()



機能は移行を削除します。 これにより、Alembicは、ダウングレードパスを使用して、履歴内の任意の場所(古いバージョンでも)にデータベースを転送できます。







flask db migrate



コマンドはデータベースに変更を加えることはなく、単に移行スクリプトを作成します。 データベースに変更を適用するには、 flask db upgrade



コマンドを使用する必要があります。







 (venv) $ 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 -> e517276bb1c2, users table
      
      





このアプリケーションはSQLiteを使用しているため、Upgradeコマンドはデータベースが存在しないことを検出して作成します(このコマンドの完了後にapp.db



という名前のファイル、つまりSQLiteデータベースが追加されます)。 MySQLやPostgreSQLなどのデータベースサーバーを使用する場合、更新を開始する前にデータベースサーバーにデータベースを作成する必要があります。







データベースのアップグレードプロセスと、アップグレードおよびダウングレードの変更のロールバック



現時点では、アプリケーションはまだ初期段階にありますが、これは将来のデータベース移行戦略に何が含まれるかを議論するのに支障はありません。 開発マシンにアプリケーションがあり、オンラインで使用中の本番サーバーにコピーがデプロイされているとします。







アプリケーションの次のバージョンでは、モデルに変更を加える必要があるとします。たとえば、新しいテーブルを追加する必要があるとします。 移行を行わない場合、ローカルホストとサーバーの両方でデータベースのスキーマを変更する方法を理解する必要がありますが、これは大きな問題になる可能性があります。







ただし、データベース移行のサポートでは、アプリケーションのモデルを変更した後、新しい移行スクリプト(flask db migrate)を作成します。自動作成が正しいことを確認し、開発データベース(flask)に変更を適用することを確認します。 db upgrade)。 移行スクリプトをバージョン管理システムに追加してコミットします。







本番サーバーでアプリケーションの新しいバージョンをリリースする準備ができたら、必要な作業は、新しい移行スクリプトを含む更新されたバージョンのアプリケーションを取得し、 flask db upgrade



を実行することだけです。 Alembicは、データベースが最新版に更新されていないことを検出し、以前のリリース後に作成されたすべての新しい移行スクリプトを実行します。







前述したように、最後の移行をキャンセルするdowngrade flask db



コマンドもあります。 本番稼働時にこのオプションが必要になることはほとんどありませんが、開発中に非常に役立つことがあります。 移行スクリプトを生成して適用したのは、行った変更が必要なものではないことを確認するためだけです。 この場合、データベースの評価を下げ、移行スクリプトを削除してから、新しいスクリプトを作成して置き換えることができます。







データベース関係



リレーショナルデータベースは、データ項目間の関係を保存するのに適しています。 ユーザーがブログ投稿を書く場合を考えてください。 ユーザーはユーザーテーブルにエントリを持ち、メッセージはメッセージテーブルにエントリを持ちます。 この投稿を書いた人を記録する最も効果的な方法は、2つの記録をリンクすることです。







ユーザーと投稿の間の接続が確立された後、必要になる可能性のある2種類の要求があります。 投稿があり、どのユーザーがそれを書いたかを知る必要がある場合に最も簡単です。 もう少し複雑な質問は、この逆です。 ユーザーがいる場合は、そのユーザーが作成したすべてのエントリを取得する必要があります。 Flask-SQLAlchemyは、両方のタイプのクエリで役立ちます。







投稿を保存するためのベースを拡張して、実行中の接続を確認できるようにします。 これを行うには、データベース設計ツールに戻り、レコードのテーブルを作成します。













メッセージテーブルには、必要な識別子、メッセージテキスト、およびタイムスタンプが含まれます。 しかし、これらの予想されるフィールドに加えて、メッセージをその作成者にバインドするuser_id



フィールドを追加しuser_id



。 すべてのユーザーが一意の主キーid



持っていることがわかりました。 ブログエントリを作成したユーザーに関連付ける方法は、ユーザーIDへのリンクを追加することuser_id



。これはuser_id



フィールドuser_id



まったく同じuser_id



。 このuser_id



フィールドは外部キーと呼ばれます。 上記のデータベースダイアグラムでは、外部キーは、フィールドとそれが参照するテーブルのid



フィールドとの関係として表示されます。 「1人の」ユーザーが「多くの」メッセージを書き込むため、この種の関係は1対多と呼ばれます。







変更されたapp / models.pyを以下に示します:







 from datetime import datetime from app import db class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), index=True, unique=True) email = db.Column(db.String(120), index=True, unique=True) password_hash = db.Column(db.String(128)) posts = db.relationship('Post', backref='author', lazy='dynamic') def __repr__(self): return '<User {}>'.format(self.username) class Post(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String(140)) timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) def __repr__(self): return '<Post {}>'.format(self.body)
      
      





新しいPost



クラスには、ユーザーが作成したブログ投稿が含まれます。 timestamp



フィールドにはインデックスが付けられます。これは、メッセージを時系列で受信する場合に便利です。 また、デフォルトの引数を追加し、関数datetime.utcnow



を渡しました。 デフォルト関数を渡すと、SQLAlchemyはこの関数を呼び出すようにフィールドを設定します()



utcnow



後に()



utcnow



なかったため、呼び出しの結果ではなく、この関数を自分で渡します)。 通常、これにより、サーバーアプリケーションでUTCの日付と時刻を操作できます。 これにより、ユーザーがどこにいても単一のタイムスタンプを使用できます。 これらのタイムスタンプは、表示されるときにユーザーの現地時間に変換されます。







user_id



フィールドはuser.id



外部キーとして初期化されました。つまり、 users



テーブルのid



値を参照します。 このリンクでは、userは、Flask-SQLAlchemyが小文字に変換されたモデルクラスの名前として自動的に設定するデータベーステーブルの名前です。 Userクラスには、 db.relationship



によって初期化される新しいメッセージフィールドがあります。 これは実際のデータベースフィールドではなく、ユーザーと投稿の関係の高レベルのビューであり、このため、データベースダイアグラムにはありません。 1対多の関係の場合、db.relationshipフィールドは通常、片側で定義され、多くにアクセスする便利な方法として使用されます。 したがって、たとえば、ユーザーがu



に格納されている場合、式u.posts



は、そのユーザーによって書き込まれたすべてのレコードを返すデータベースクエリをトリガーします。 db.relationship



の最初の引数は、多リレーションシップのサイドを表すクラスを指定します。 backref



引数は、1つのオブジェクトを指すmanyクラスのオブジェクトに追加されるフィールドの名前を指定します。 これにより、投稿の著者を返すpost.author



式が追加されます。 遅延引数は、通信用のデータベースクエリの実行方法を決定します。これについては後で説明します。 これらの詳細が意味をなさない場合でも心配しないでください。この記事の最後に例を示します。







アプリケーションモデルの更新があるため、新しいデータベース移行を作成する必要があります。







 (venv) $ flask db migrate -m "posts table" INFO [alembic.runtime.migration] Context impl SQLiteImpl. INFO [alembic.runtime.migration] Will assume non-transactional DDL. INFO [alembic.autogenerate.compare] Detected added table 'post' INFO [alembic.autogenerate.compare] Detected added index 'ix_post_timestamp' on '['timestamp']' Generating /home/miguel/microblog/migrations/versions/780739b227a7_posts_table.py ... done
      
      





そして、データベースに移行を適用する必要があります。







 (venv) $ 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 e517276bb1c2 -> 780739b227a7, posts table
      
      





プロジェクトがバージョン管理に保存されている場合は、新しい移行スクリプトを忘れずに追加してください。







打ち上げ時間



長いデータベース作成プロセスを実行させましたが、すべてがどのように機能するかについてはまだ説明していません。 アプリケーションにはまだデータベースロジックがないため、Pythonインタープリターでデータベースを操作して、データベースに慣れてみましょう。 それでは、Pythonを実行してください。 インタープリターを開始する前に、仮想環境がアクティブになっていることを確認してください。







Pythonコマンドラインで、データベースインスタンスとモデルをインポートしましょう。







 >>> from app import db >>> from app.models import User, Post
      
      





:







 >>> u = User(username='john', email='john@example.com') >>> db.session.add(u) >>> db.session.commit()
      
      





, db.session



. , , db.session.commit()



, . , db.session.rollback()



, . , db.session.commit()



. , .







:







 >>> u = User(username='susan', email='susan@example.com') >>> db.session.add(u) >>> db.session.commit()
      
      





, :







 >>> users = User.query.all() >>> users [<User john>, <User susan>] >>> for u in users: ... print(u.id, u.username) ... 1 john 2 susan
      
      





, . — , , all()



. , id



1 2.







. , :







 >>> u = User.query.get(1) >>> u <User john>
      
      





:







 >>> u = User.query.get(1) >>> p = Post(body='my first post!', author=u) >>> db.session.add(p) >>> db.session.commit()
      
      





timestamp



, , . user_id? , db.relationship



, User



, posts



, . , , , . SQLAlchemy , .







, :







 >>> # get all posts written by a user >>> u = User.query.get(1) >>> u <User john> >>> posts = u.posts.all() >>> posts [<Post my first post!>] >>> # same, but with a user that has no posts >>> u = User.query.get(2) >>> u <User susan> >>> u.posts.all() [] >>> # print post author and body for all posts >>> posts = Post.query.all() >>> for p in posts: ... print(p.id, p.author.username, p.body) ... 1 john my first post! # get all users in reverse alphabetical order >>> User.query.order_by(User.username.desc()).all() [<User susan>, <User john>]
      
      





Flask-SQLAlchemy- , , .







, , , :







 >>> users = User.query.all() >>> for u in users: ... db.session.delete(u) ... >>> posts = Post.query.all() >>> for p in posts: ... db.session.delete(p) ... >>> db.session.commit()
      
      





shell context



, , Python? , , :







 >>> from app import db >>> from app.models import User, Post
      
      





, Python, . flask shell



— . shell



— "", flask



, . - Python . これはどういう意味ですか? 次の例を考えてみましょう。







 (venv) $ python >>> app Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'app' is not defined >>> (venv) $ flask shell >>> app <Flask 'app'>
      
      





app



, , flask shell



. flask shell



, , , «shell context», .







microblog.py , :







 from app import app, db from app.models import User, Post @app.shell_context_processor def make_shell_context(): return {'db': db, 'User': User, 'Post': Post}
      
      





app.shell_context_processor



. flask shell



, , . , , , , , , .







, flask shell



, , :







 (venv) $ flask shell >>> db <SQLAlchemy engine=sqlite:////Users/migu7781/Documents/dev/flask/microblog2/app.db> >>> User <class 'app.models.User'> >>> Post <class 'app.models.Post'>
      
      





<<<前 次>>>








All Articles