僕らは偶然性を大切にするためにクソゲーを作り続ける。

なんか気ままにクソゲームを作っています。

FlaskでWebアプリを作る。その2:Flask-Migrationでデータベースを作る

こんにちは、むさしです。 引き続きflaskでwebアプリを作っていきましょう。

前の記事: playwao.hatenablog.com

データベースを利用したWebアプリを作る

これからデータベースを使ったアプリを作っていきましょう。 はじめに話したようにデータベースはSQLiteを使います。

今回は読書用の日記アプリを作ります。 作る必要があるであろうテーブルは以下の2つになります。

  • 書籍
    • 書籍名
    • 作者・著者
    • 出版社
  • 読んだ記録
    • 日付(自動)
    • 読んだ本
    • 感想

また、ファイル構造を以下のように構成します。

.
├── manage.py
├── models.py
├── form.py
├── view.py
├── static
|   └── app.css
└── templates
    ├── index.html
    └── base.html

manage.pyは名前こそ変えましたがその1で作ったやつのapp.pyと同じです。 model.pyはfalskでデータベースのデータを扱い際の???いい言葉が思いつかない???になります。 view.pyはブラウザなどとやりとりするのに相手のレスポンスに対して特定の関数を呼び出し、適切なレスポンスを返す機能を実装します。 form.pyは書籍や記録などを入力する際のフォーム用のクラスを設定します。 また、header.htmlbase.htmlと名前を変えます。変えなくても問題はありません。 staticフォルダにはapp.cssというファイルを入れておきます。

データベース関係の準備

SQLAchemyのインストール

データベースはSQLiteを使います。macにはデフォルトで入いるのでそのままで大丈夫です。 mySQLなど好みのデータベースを使ってください。

ORマッパとしてSQL-Alchemyを使います。ORマッパとはwikipediaによると、

オブジェクト関係マッピング(英: Object-relational mapping、O/RM、ORM)とは、データベースとオブジェクト指向プログラミング言語の間の非互換なデータを変換するプログラミング技法である。オブジェクト関連マッピングとも呼ぶ。実際には、オブジェクト指向言語から使える「仮想」オブジェクトデータベースを構築する手法である。

とのことです。 僕の中ではpythonでデータベースにアクセスするのにSQLで記述しなくて良くなる便利なもの程度の認識です^^;

とにかくインストールしましょう。またこの時一緒にFlask-SQLAchemyもインストールしましょう。SQLAchemyのセッションなど管理できるので自前で作る必要がなく楽できます。

$ pip install sqlalchemy
$ pip install flask-sqlalchemy

Flask-Migrateのインストール

データベースを使うのに直接SQLiteを操作してテーブル作るのもいのですが、使うテーブルを変えたい時、新しいテーブルを追加したい時などにサーバーとデータベースをどっちも修正するのは面倒です。 また、ローカル環境で開発して本番のサーバーにあげる時初めからデータベースにテーブルを打ち込まなくていいというメリットもあります。 そこでFlask-Migrateを使います。これを使えばテーブルの修正や追加などをサポートしてくれます。

まずはpipでインストールします。

$ pip insatll flask-migrate

するとダウンロード完了です。

pip freezeで確認してみてください。

$ pip freeze
alembic==0.7.6
Flask==0.10.1
Flask-Migrate==1.4.0
Flask-Script==2.0.5
Flask-SQLAlchemy==2.0
itsdangerous==0.24
Jinja2==2.7.3
Mako==1.0.1
MarkupSafe==0.23
SQLAlchemy==1.0.6
Werkzeug==0.10.4
wheel==0.24.0

色々増えていますが、Flask-ScriptやMakoなどはFlask-Migrateをインストールした際に一緒にダウンロードされただけです。

ちなみにこのflask-migrateはflaskのオイラリー著者が作ったそうです。

モデルとマイグレーションの設定

モデルを作る準備ができたので早速作っていきましょう。

まずはモデルを作っていきます。 models.pyを以下のよう編集します。

models.py

#!/usr/bin/env python # coding: utf-8
from flask.ext.sqlalchemy import SQLAlchemy
from sqlalchemy import Column, Integer, Unicode, UnicodeText, ForeignKey
from sqlalchemy.orm import relationship, backref
from datetime import datetime

db = SQLAlchemy()

class Book(db.Model):
    """
    書籍モデル
    """
    __tablename__ = "books"
    id = Column(Integer, primary_key=True)
    title = Column(Unicode(255))
    auther = Column(Unicode(255))
    publisher = Column(Unicode(255))

    #初期化
    def __init__(title, auther, publisher):
        self.title = title.title()
        self.auther = auther.title()
        self.publisher = publisher.title()

class Diary(db.Model):
    """
    感想モデル
    """
    __tablename__ = "diaries"
    id = Column(Integer, primary_key=True)
    date = Column(Unicode(255))
    book_title = Column(Unicode(255), ForeignKey('books.title'))
    impression = Column(UnicodeText)

    #書籍とのリレーションを作成
    book = relationship("Book", backref=backref('diaries', order_by=id))

    #初期化
    def __init__(book_title, impression):
        self.date = datetime.utcnow().strftime( '%Y-%m-%d %H:%M:%S' )
        self.book_title = book_title.title()
        self.impression = impression.title()

まずは

from flask.ext.sqlalchemy import SQLAlchemy

db = SQLAlchemy()

として、SQLAchemyを使う準備をします。

次に書籍のモデルを作っていきます。

from sqlalchemy import Column, Integer, Unicode


class Book(db.Model):
    """
    書籍モデル
    """
    __tablename__ = "books"
    id = Column(Integer, primary_key=True)
    title = Column(Unicode(255))
    auther = Column(Unicode(255))
    publisher = Column(Unicode(255))

    #生成された時、呼び出される
    def __init__(title, auther, publisher):
        self.title = title.title()
        self.auther = auther.title()
        self.publisher = publisher.title()

__tablename__にはデータベースのテーブル名を入れてください。 これでどのテーブルを使うか指定します。 また、今回はmigrateもするのでその際のテーブル名になります。

その下はテーブルのカラムとその定義を書いていきます。 例えばid = Column(Integer, primary_key=True)ならidカラム名に、Integer, primary_key=Trueがカラムの定義にあたります。

Integerはこのカラムに整数が入ることを表しています。primary_key=Trueは入る値がカラムに唯一しか存在しないことを表しています。明記していませんがこの値は自動でナンバリングされます。なので、例えば新たにデータを入力すればidが自動で番号付けされます。

title = Column(Unicode(255))などにあるUnicode(255)は値にunicodeを取れることを表しています。文字数は255文字までとれます。Unicodeの代わりにStringを使うと日本語などを入力した時エラーがでるので注意が必要です。僕はめんどくさいので全部Unicodeにしています^^;

__init__とはBookクラスが生成された時呼び出される関数です。今回は引数にtitle, auther, publisherを取っています。この関数は生成時に呼び出されるものです。 self.title = title.title()title()はクラスが生成された時に取った引数を代入します。この例はtitle、titleでこれはわかりにくいですが^^; つまりnewBook = Book("坊ちゃん", "夏目漱石", "青空文庫")ならば、

title = "坊ちゃん"
auther = "夏目漱石"
publisher = "青空文庫"

と各変数に代入されます。

同様に感想についてもモデルを作っていきます。

from sqlalchemy import Column, Integer, Unicode, UnicodeText, ForeignKey
from sqlalchemy.orm import relationship, backref
from datetime import datetime

class Diary(db.Model):
    """
    感想モデル
    """
    __tablename__ = "diaries"
    id = Column(Integer, primary_key=True)
    date = Column(Unicode(255))
    book_title = Column(Unicode(255), ForeignKey('books.title'))
    impression = Column(UnicodeText)

    #書籍とのリレーションを作成
    book = relationship("Book", backref=backref('diaries', order_by=id))

    #生成された時、呼び出される
    def __init__(book_title, impression):
        self.date = datetime.utcnow().strftime( '%Y-%m-%d %H:%M:%S' )
        self.book_title = book_title.title()
        self.impression = impression.title()

先ほどと同じようなところは説明を省かせていただきます。

book_title = Column(Unicode(255), ForeignKey('books.title'))book = relationship("Book", backref=backref('diaries', order_by=id))はリレーションを定義しています。今回作成しているWebアプリは書籍一つに関していくつも感想や記録をつけることができます。このように書籍の下にいくつもの感想がくる構造を取ることを一対多の関係にあるといいます。これを関連付けているのがリレーションです。

book_title = Column(Unicode(255), ForeignKey('books.title'))ForeignKeyは外部キーといいます。このForeignKeyの引数としてリレーション先のテーブル名とそのカラム名(テーブル名).(カラム名)を指定します。 book = relationship("Book", backref=backref('diaries', order_by=id))BookDiaryという2つのクラスのリレーションをrelation()関数を使って別個に定義します。この時relation()関数をBookの方に

class Book(db.Model):

    ...

    diary = relation("Diary", order_by=Diary.id, backref="books")

と定義も出来ます。どちらで書くこともできますが僕は基本的に子要素のところで定義します。

また、__init__dateについてself.date = datetime.utcnow().strftime( '%Y-%m-%d %H:%M:%S' )としていますがこれはクラスを生成した時に時間を自動で入力してくれるようにしています。 これ以外にも例えばユーザー登録時にランダムidを発行したくなった時はuuidを使って

class User(db.Model):
    id = Column(Integer, primary_key=True)
    uuid = Column(Unicode(255))
    name = Column(Unicode(255))
    
    def __init__(name)
    self.uuid = uuid.uuid4()
    self.name = name.title()

とできます。

今度はmange.pyを編集していきます。

manage.py

#!/usr/bin/env python # coding: utf-8
import sys
reload(sys)
sys.setdefaultencoding('utf-8')

from flask import Flask, render_template
from model import db
from flask.ext.script import Manager, Server
from flask.ext.migrate import Migrate, MigrateCommand

app = Flask(__name__)
#デバッグ
app.config['DEBUG'] = True
#秘密キー
app.secret_key = 'development key'
#データベースを指定
app.config['SQLALCHEMY_DATABASE_URI'] = 'SQLite:///diary.db'
app.config['SQLALCHEMY_NATIVE_UNICODE'] = 'utf-8'
db.init_app(app)
db.app = app

@app.route("/")
def hello():
    return render_template("index.html")

migrate = Migrate(app, db)
manager = Manager(app)

manager.add_command('db', MigrateCommand)
manager.add_command('runserver', Server(host='localhost', port='8080'))

if __name__ == "__main__":
    manager.run()

まず、はマイグレーションに必要なものをインポートします。

from flask.ext.script import Manager, Server
from flask.ext.migrate import Migrate, MigrateCommand

そのあとデータベースを指定します。

app.config['SQLALCHEMY_DATABASE_URI'] = 'SQLite:///diary.sqlite3'
app.config['SQLALCHEMY_NATIVE_UNICODE'] = 'utf-8'
db.init_app(app)
db.app = app

その後、MigrateオブジェクトとManagerオブジェクトを生成します。 add_commandコマンドラインからマイグレーションの処理を行います。 このときmanager.add_command('runserver', Server(host='localhost', port='8080'))の部分はこのflask-migrateを適応するとなぜかホストやポート番号を設定できないのでこれで無理やり設定しています^^; もっといい方法があればいいのですが… ちなみにapp.config['SERVER_NAME'] = 'localhost'ではうまくいきませんでした。

マイグレーションを行う

マイグレーションする準備できたので早速マイグレーションを行なっていきましょう。 まず、ちゃんと正しく動いているか確認するためにpython mange.pyを実行させてみましょう。すると以下のように説明が出てきます。これが出てきたら正しくでいている証拠です。

$ python manage.py
usage: manage.py [-?] {shell,db,runserver} ...

positional arguments:
  {shell,db,runserver}
    shell               Runs a Python shell inside Flask application context.
    db                  Perform database migrations
    runserver           Runs the Flask development server i.e. app.run()

optional arguments:
  -?, --help            show this help message and exit

また、python manage.py db --helpを実させましょう。

$ python manage.py db --help
usage: Perform database migrations

Perform database migrations

positional arguments:
  {upgrade,heads,show,migrate,stamp,current,merge,init,downgrade,branches,history,revision}
    upgrade             Upgrade to a later version
    heads               Show current available heads in the script directory
    show                Show the revision denoted by the given symbol.
    migrate             Alias for 'revision --autogenerate'
    stamp               'stamp' the revision table with the given revision;
                        don't run any migrations
    current             Display the current revision for each database.
    merge               Merge two revisions together. Creates a new migration
                        file
    init                Generates a new migration
    downgrade           Revert to a previous version
    branches            Show current branch points
    history             List changeset scripts in chronological order.
    revision            Create a new revision file.

optional arguments:
  -?, --help            show this help message and exit

この中のmigrateupgradeを主に使います。

まず、データベースを初期化します。

$ python manage.py db init
  Creating directory ~/flask_app/migrations ... done
  Creating directory ~/flask_app/migrations/versions ... done
  Generating ~/flask_app/migrations/alembic.ini ... done
  Generating ~/flask_app/migrations/env.py ... done
  Generating ~/flask_app/migrations/env.pyc ... done
  Generating ~/flask_app/migrations/README ... done
  Generating ~/flask_app/migrations/script.py.mako ... done
  Please edit configuration/connection/logging settings in
  '~/flask_app/migrations/alembic.ini' before proceeding.

こうするとmanage.pyと同じディレクトリにmaigrationというフォルダができています。 ここでマイグレーションを管理、実行します。

マイグレーションファイルを製作しましょう。

$ python manage.py db migrate
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.autogenerate.compare] Detected added table 'books'
INFO  [alembic.autogenerate.compare] Detected added table 'diaries'
  Generating ~/flask_app/migrations/versions/299d7e501a6e_.py ... done

するとmigration/versionsになんらかのpythonファイルが生成されています。

最後にこれをupgradeしてsqliteに適応させましょう。

$ python manage.py db upgrade
INFO  [alembic.migration] Context impl SQLiteImpl.
INFO  [alembic.migration] Will assume non-transactional DDL.
INFO  [alembic.migration] Running upgrade  -> 299d7e501a6e, empty message

これで完了です。daiary.dbというデータベースが生成されていると思うので中身を確認してみましょう。

$ sqlite3 diary.db
SQLite version 3.8.5 2014-08-15 22:37:57
Enter ".help" for usage hints.
sqlite> .table
alembic_version  books            diaries
sqlite> .schema
CREATE TABLE alembic_version (
    version_num VARCHAR(32) NOT NULL
);
CREATE TABLE books (
    id INTEGER NOT NULL, 
    title VARCHAR(255), 
    auther VARCHAR(255), 
    publisher VARCHAR(255), 
    PRIMARY KEY (id)
);
CREATE TABLE diaries (
    id INTEGER NOT NULL, 
    date VARCHAR(255), 
    book_title VARCHAR(255), 
    impression TEXT, 
    PRIMARY KEY (id), 
    FOREIGN KEY(book_title) REFERENCES books (title)
);

無事出来ているようです。

次回はテンプレートファイルとCRUDを実装していきたいと思います。

次の記事: playwao.hatenablog.com