サンプルアプリケーション

シンプルな *Twitter* のようなサイトを構築します。サンプルのソースコードは、examples/twitter ディレクトリにあります。 githubでソースコードを参照することもできます。また、お好みであればブログアプリの例もありますが、このガイドでは扱いません。

サンプルアプリは、簡単に始められるflaskウェブフレームワークを使用しています。flaskがまだインストールされていない場合は、サンプルを実行するためにインストールする必要があります。

pip install flask

サンプルの実行

../_images/tweepee.jpg

flaskがインストールされていることを確認した後、twitterのサンプルディレクトリにcdで移動し、run_example.pyスクリプトを実行します。

python run_example.py

サンプルアプリは、http://localhost:5000/でアクセスできます。

コードの詳細

簡潔にするために、すべてのサンプルコードは単一のモジュールexamples/twitter/app.pyに含まれています。peeweeを使用した大規模なFlaskアプリの構築方法については、Flaskアプリの構築を参照してください。

モデル

人気のあるWebフレームワークDjangoの精神に基づき、peeweeは宣言型のモデル定義を使用します。Djangoに慣れていない方のために説明すると、各テーブルに対してモデルクラスを宣言するという考え方です。次に、モデルクラスは、テーブルの列に対応する1つ以上のフィールド属性を定義します。Twitterクローンには、3つのモデルしかありません。

ユーザー:

ユーザーアカウントを表し、ユーザー名とパスワード、*gravatar*を使用してアバターを生成するためのメールアドレス、およびそのアカウントが作成された日時を示す日時フィールドを格納します。

リレーションシップ:

これは、*User*モデルへの2つの外部キーを含むユーティリティモデルであり、どのユーザーが互いにフォローしているかを格納します。

メッセージ:

ツイートに類似しています。Messageモデルは、ツイートのテキストコンテンツ、作成日時、投稿者(ユーザーへの外部キー)を格納します。

UMLがお好きなら、これらがテーブルとリレーションシップです。

../_images/schema.jpg

これらのモデルを作成するには、SqliteDatabaseオブジェクトをインスタンス化する必要があります。次に、モデルクラスを定義し、列をクラスのFieldインスタンスとして指定します。

# create a peewee database instance -- our models will use this database to
# persist information
database = SqliteDatabase(DATABASE)

# model definitions -- the standard "pattern" is to define a base model class
# that specifies which database to use.  then, any subclasses will automatically
# use the correct storage.
class BaseModel(Model):
    class Meta:
        database = database

# the user model specifies its fields (or columns) declaratively, like django
class User(BaseModel):
    username = CharField(unique=True)
    password = CharField()
    email = CharField()
    join_date = DateTimeField()

# this model contains two foreign keys to user -- it essentially allows us to
# model a "many-to-many" relationship between users.  by querying and joining
# on different columns we can expose who a user is "related to" and who is
# "related to" a given user
class Relationship(BaseModel):
    from_user = ForeignKeyField(User, backref='relationships')
    to_user = ForeignKeyField(User, backref='related_to')

    class Meta:
        # `indexes` is a tuple of 2-tuples, where the 2-tuples are
        # a tuple of column names to index and a boolean indicating
        # whether the index is unique or not.
        indexes = (
            # Specify a unique multi-column index on from/to-user.
            (('from_user', 'to_user'), True),
        )

# a dead simple one-to-many relationship: one user has 0..n messages, exposed by
# the foreign key. a users messages will be accessible as a special attribute,
# User.messages.
class Message(BaseModel):
    user = ForeignKeyField(User, backref='messages')
    content = TextField()
    pub_date = DateTimeField()

注意

使用するデータベースを定義するだけの*BaseModel*クラスを作成することに注意してください。他のすべてのモデルはこのクラスを拡張し、正しいデータベース接続も使用します。

Peeweeは、データベースエンジンで一般的にサポートされているさまざまな列タイプに対応する、さまざまなフィールドタイプをサポートしています。Pythonの型とデータベースで使用される型の間の変換は透過的に処理されるため、アプリケーションで以下を使用できます。

  • 文字列(Unicodeまたはその他)

  • 整数、浮動小数点数、およびDecimal数値。

  • ブール値

  • 日付、時刻、および日時

  • None (NULL)

  • バイナリデータ

テーブルの作成

モデルを使用するには、テーブルを作成する必要があります。これは1回限りの操作であり、インタラクティブインタープリターを使用してすばやく実行できます。これを実現するための小さなヘルパー関数を作成できます。

def create_tables():
    with database:
        database.create_tables([User, Relationship, Message])

サンプルアプリと同じディレクトリにあるPythonシェルを開き、以下を実行します。

>>> from app import *
>>> create_tables()

注意

*ImportError*が発生した場合は、*flask*または*peewee*が見つからず、正しくインストールされていない可能性があります。peeweeのインストール手順については、インストールとテストドキュメントを参照してください。

すべてのモデルには、データベースでSQL *CREATE TABLE*ステートメントを実行するcreate_table()クラスメソッドがあります。このメソッドは、すべての列、外部キー制約、インデックス、およびシーケンスを含むテーブルを作成します。通常、これは新しいモデルが追加されるたびに1回だけ実行します。

Peeweeは、モデル間の依存関係を解決し、各モデルでcreate_table()を呼び出して、テーブルが順番に作成されるようにするヘルパーメソッドDatabase.create_tables()を提供します。

注意

テーブルの作成後にフィールドを追加するには、テーブルを削除して再作成するか、*ALTER TABLE*クエリを使用して手動で列を追加する必要があります。

または、スキーママイグレーション拡張機能を使用して、Pythonを使用してデータベーススキーマを変更することもできます。

データベース接続の確立

上記のモデルコードで、database属性を設定する*Meta*という名前のクラスが基本モデルに定義されていることに気付いたかもしれません。Peeweeでは、すべてのモデルが使用するデータベースを指定できます。モデルの動作を制御する多くのメタオプションを指定できます。

これはpeeweeのイディオムです。

DATABASE = 'tweepee.db'

# Create a database instance that will manage the connection and
# execute queries
database = SqliteDatabase(DATABASE)

# Create a base-class all our models will inherit, which defines
# the database we'll be using.
class BaseModel(Model):
    class Meta:
        database = database

Webアプリケーションを開発する場合、リクエストの開始時に接続を開き、レスポンスが返されたときに接続を閉じるのが一般的です。**接続は常に明示的に管理する必要があります**。たとえば、接続プールを使用している場合、connect()close()を呼び出した場合にのみ、接続が正しくリサイクルされます。

リクエスト/レスポンスサイクル中にデータベースへの接続を作成する必要があることをflaskに指示します。Flaskは、これを簡単にするための便利なデコレータを提供しています。

@app.before_request
def before_request():
    database.connect()

@app.after_request
def after_request(response):
    database.close()
    return response

注意

Peeweeはスレッドローカルストレージを使用して接続状態を管理するため、このパターンはマルチスレッドWSGIサーバーで使用できます。

クエリの作成

*User*モデルには、ユーザー固有の機能をカプセル化するインスタンスメソッドがいくつかあります。

  • following(): このユーザーは誰をフォローしていますか?

  • followers(): 誰がこのユーザーをフォローしていますか?

これらのメソッドは実装は似ていますが、SQLの*JOIN*句と*WHERE*句に重要な違いがあります。

def following(self):
    # query other users through the "relationship" table
    return (User
            .select()
            .join(Relationship, on=Relationship.to_user)
            .where(Relationship.from_user == self)
            .order_by(User.username))

def followers(self):
    return (User
            .select()
            .join(Relationship, on=Relationship.from_user)
            .where(Relationship.to_user == self)
            .order_by(User.username))

新しいオブジェクトの作成

新しいユーザーがサイトに参加したい場合、ユーザー名が使用可能かどうかを確認し、使用可能な場合は新しい*User*レコードを作成する必要があります。*join()*ビューを見ると、アプリケーションがModel.create()を使用してユーザーを作成しようとしていることがわかります。*User.username*フィールドを一意制約で定義したため、ユーザー名が使用されている場合、データベースはIntegrityErrorを発生させます。

try:
    with database.atomic():
        # Attempt to create the user. If the username is taken, due to the
        # unique constraint, the database will raise an IntegrityError.
        user = User.create(
            username=request.form['username'],
            password=md5(request.form['password']).hexdigest(),
            email=request.form['email'],
            join_date=datetime.datetime.now())

    # mark the user as being 'authenticated' by setting the session vars
    auth_user(user)
    return redirect(url_for('homepage'))

except IntegrityError:
    flash('That username is already taken')

ユーザーが誰かをフォローしたい場合も同様のアプローチを使用します。フォロー関係を示すために、*Relationship*テーブルに行を作成し、あるユーザーから別のユーザーを指します。from_userto_userの一意のインデックスにより、重複行が発生しないようにします。

user = get_object_or_404(User, username=username)
try:
    with database.atomic():
        Relationship.create(
            from_user=get_current_user(),
            to_user=user)
except IntegrityError:
    pass

サブクエリの遂行

ログインしてTwitterのホームページにアクセスすると、フォローしているユーザーのツイートが表示されます。これをきれいに実装するために、サブクエリを使用できます。

注意

サブクエリuser.following()は、デフォルトでは通常Userモデルのすべての列を選択します。サブクエリとして使用しているため、peeweeは主キーのみを選択します。

# python code
user = get_current_user()
messages = (Message
            .select()
            .where(Message.user.in_(user.following()))
            .order_by(Message.pub_date.desc()))

このコードは、次のSQLクエリに対応します。

SELECT t1."id", t1."user_id", t1."content", t1."pub_date"
FROM "message" AS t1
WHERE t1."user_id" IN (
    SELECT t2."id"
    FROM "user" AS t2
    INNER JOIN "relationship" AS t3
        ON t2."id" = t3."to_user_id"
    WHERE t3."from_user_id" = ?
)

その他の注目すべきトピック

サンプルアプリには、簡単に説明する価値のある、その他の優れた点がいくつかあります。

  • 結果リストのページネーションのサポートは、object_listと呼ばれる単純な関数(Djangoのそれにちなんで名付けられました)に実装されています。この関数は、オブジェクトのリストを返すすべてのビューで使用されます。

    def object_list(template_name, qr, var_name='object_list', **kwargs):
        kwargs.update(
            page=int(request.args.get('page', 1)),
            pages=qr.count() / 20 + 1)
        kwargs[var_name] = qr.paginate(kwargs['page'])
        return render_template(template_name, **kwargs)
    
  • login_required デコレータを使用したシンプルな認証システム。最初の関数は、ユーザーが正常にログインしたときに現在のセッションにユーザーデータを追加するだけです。デコレータ login_required は、ビュー関数をラップするために使用でき、セッションが認証されているかどうかを確認し、認証されていない場合はログインページにリダイレクトします。

    def auth_user(user):
        session['logged_in'] = True
        session['user'] = user
        session['username'] = user.username
        flash('You are logged in as %s' % (user.username))
    
    def login_required(f):
        @wraps(f)
        def inner(*args, **kwargs):
            if not session.get('logged_in'):
                return redirect(url_for('login'))
            return f(*args, **kwargs)
        return inner
    
  • データベースにオブジェクトが見つからない場合は、例外をスローする代わりに 404 レスポンスを返します。

    def get_object_or_404(model, *expressions):
        try:
            return model.get(*expressions)
        except model.DoesNotExist:
            abort(404)
    

注意

object_list()get_object_or_404() を頻繁にコピー/ペーストする必要がないように、これらの関数は playhouse の Flask 拡張モジュール の一部として含まれています。

from playhouse.flask_utils import get_object_or_404, object_list

その他の例

peewee の examples ディレクトリ には、以下を含むより多くの例が含まれています。

注意

これらのスニペットが気に入って、もっと知りたいですか? flask-peewee をチェックしてみてください。これは、Django のような管理インターフェース、RESTful API、認証などを peewee モデルに提供する Flask プラグインです。