SQLALchemy seed を作った

お仕事で Django を使ってて便利だなーと思ったのが fixtures。
初期データを突っ込むのに便利。
Providing initial data for models | Django documentation | Django


yaml で初期データの定義を書いて、コマンド経由で DB にデータを入れられる。
Flask で似たようなことできないだろうかと探したら、Flask-Fixtures があったが、これはテストデータを入れるのを想定している模様。
Add load_fixtures_from_file by RoPP · Pull Request #21 · croach/Flask-Fixtures · GitHub という PR もあるが放置されている。


また Flask-SQLAlchemy に依存しているので、素の SQLALchemy を使ってる場合は使えない。
SQLAlchemy-Fixtures というものがあるが、Factory_boy を使えとある。


もう一つの方法としてはマイグレーションツールの Alembic の中で bulk_insert を使って初期データを入れる。
が、autogenreate を使う場合を想定すると、マイグレーションファイルとは別にしたい。


というわけで SQLAlchemy と PyYaml*1だけに依存しているものを作った。
GitHub - heavenshell/py-sqlalchemy_seed: Simple data seeder using SQLAlchemy.

こんな感じのディレクトリ構成。

.app
  /fixtures/pictures.yaml
  models
  /models.py
  /db.py
  views.py

Seed ファイルを書く。
書き方は Django と同じ(多分)。

- model: app.models.picture.Picture
  id: 1
  fields:
    name: spam.jpg
    title: Spam
    description: spam description
- model: app.models.picture.Picture
  id: 2
  fields:
    name: ham.jpg
    title: Ham
    description: ham description
- model: app.models.picture.Picture
  id: 3
  fields:
    name: beacon.jpg
    title: Beacon
    description: beacon description

こんな感じで、 yaml ファイルを書く。

from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import scoped_session, sessionmaker

engine = create_engine('sqlite:///./db.sqlite', convert_unicode=True)
Base = declarative_base()
Base.metadata.bind = engine
Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
session = scoped_session(Session)

モデルはこんな感じ。

from sqlalchemy import Column, Integer, String
from .db import Base


class Picture(Base):
    __tablename__ = 'pictures'
    id = Column(Integer, primary_key=True)
    name = Column(String(254), nullable=False)
    title = Column(String(254), nullable=False)
    description = Column(String(254), nullable=False)

    def __init__(self, name, title, description):
        self.name = name
        self.title = title
        self.description = description

呼び出し側はこんな感じ。Flask-Script を使ってみる。

import os
from flask_script import Manager, prompt_bool
from sqlalchemy_seed import load_fixtures, load_fixture_files
from app import create_app
from app.models.db import session


@manager.command
def loaddata(filename=''):
    """Load seed data."""
    if filename == '':
        return

    from app.models.db import session
    path = os.path.join(app.root_path, 'fixtures')

    fixtures = load_fixture_files(path, [filename])
    load_fixtures(session, fixtures)

動かしてみる。

$ python manage.py loaddata -f pictures.yaml
$ sqlite3 db.sqlite
SQLite version 3.16.2 2017-01-06 16:32:41
Enter ".help" for usage hints.
sqlite> select * from pictures;
1|spam.jpg|Spam|spam description
2|ham.jpg|Ham|ham description
3|beacon.jpg|Beacon|beacon description
sqlite>

便利になった。

*1:多分JSON ファイルも行けると思うし、機能は入れてるが未確認