Djangoのマイグレーションでスキーマ変更とデータ変更を分けたほうがよいか

Djangoマイグレーションは、1つのマイグレーションで複数のタスクを実行できます。

スキーマを変更する処理のあとに、データのマイグレーションを実行、といったようなものです。

意味のある変更をまとめるのは便利そうに思えるのですが、落とし穴があったりします。

例を挙げてみます。

コード

myapp/models.py (モデル)

from django.db import models


class Item(models.Model):
    col1 = models.CharField(max_length=30)
    col2 = models.CharField(max_length=30, blank=True, null=True)  # このカラムを追加するマイグレーション

myapp/migrations/0002_item_col2.py (マイグレーションコード)

from django.db import migrations, models


def migrate_data(apps, schema_editor):
    Item = apps.get_model('myapp', 'Item')
    db_alias = schema_editor.connection.alias
    Item.objects.all().update(col2=models.F('col1'))
    raise Exception("ここでエラー発生")


class Migration(migrations.Migration):

    dependencies = [
        ('myapp', '0001_initial'),
    ]

    operations = [
        migrations.AddField(
            model_name='item',
            name='col2',
            field=models.CharField(blank=True, max_length=30, null=True),
        ),
        migrations.RunPython(
            migrate_data,
            migrations.RunPython.noop
        )
    ]

実行例

このコードは意図的にマイグレーション中に例外を発生させていますが、実際にはマイグレーションコードが何らかの例外を発生させて落ちる状況を考えてください。

1回目の実行では、想定通りExceptionが発生して停止します。

Exception: ここでエラー発生

2回目の実行では、データベースにSQLite3を使っている場合は、1回目同様、Exceptionで落ちますが、MySQLを使っている場合はスキーマ変更のほうでエラーになります。

django.db.utils.OperationalError: (1060, "Duplicate column name 'col2'")

これは、SQLite3やPostgreSQLスキーマ変更もトランザクション内で実行し、ロールバックできるので、何度実行しても同じになりますが、MySQLOracleスキーマ変更はトランザクションの対象外だからです。

MySQLの場合はスキーマ変更をして、データ更新に失敗した場合、スキーマ変更はロールバックされません。

この例だとカラムが残ったままま、マイグレーション失敗、という扱いになります。リカバリには手動でカラムを削除してやりなおす必要があります。

結論

スキーマ変更とデータ変更のマイグレーションは、同じクラスにまとめずに分けておいたほうが、失敗時のリカバリが楽です。

参考

GoogleAppEngine上で動作しているDjangoアプリをStackdriver Debuggerでデバッグする

AppEngineで動かしてるDjangoアプリをStackdriver Debuggerでデバッグする手順について。

Python 用 Stackdriver Debugger の設定  |  Stackdriver Debugger のドキュメント  |  Google Cloud

ドキュメントには、Django 1.Xの話しかなく、手順も良くなかったのでメモを残します(フィードバックはしておきました)。

Python 3.7、Django 2.1.4、AppEngineはStandard、Flexible両方で試しました。

requirements.txtへの追記

インストールするライブラリは、ドキュメント通り google-python-cloud-debugger で大丈夫でした。

requirements.txtgoogle-python-cloud-debugger をインストールするように追記しておきます。

google-python-cloud-debugger

Djangoの設定

DjangoアプリをAppEngineで動かす場合、エントリポイントは wsgi.py になります(gunicorn等でwsgi.pyを読み込ませてる場合)。

mysite という名前でプロジェクトを作った場合、 mysite/wsgi.py にStackdriver Debuggerを有効にするコードを追記します。

import os
from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = get_wsgi_application()
# Enable Stackdriver Debugger(以下が追記部分)
try:
    import googleclouddebugger
    googleclouddebugger.enable()
except ImportError:
    pass

デバッグしてみる

変更したものをAppEngineにデプロイ後、GCPのコンソールからStackdriverデバッグの画面を開いてみます。

ソースコードが表示されない場合は、デプロイ時のコマンドを gcloud beta app deploy (betaコマンド)にしてみると改善するかもしれません。

ソースコードが表示されたら、デバッグしたい行にスナップショットのポイントや条件、式を指定し、実際にブラウザでアプリケーションを操作します。

指定したスナップショットのポイントを通過すると、デバッグの画面で、変数やコールスタックなどを確認できます。

f:id:nullpobug:20190115145321p:plain

本番環境を気軽にデバッグできるので便利ですね。

GoogleAppEngine上でDjangoフレームワークのログをStackdriver Loggingに流す

PythonのloggingモジュールからStackdriverにログを流す手順はドキュメントに書かれている。

Setting Up Stackdriver Logging for Python  |  Stackdriver Logging  |  Google Cloud

Djangoフレームワークを使っている場合は、Djangoのロギング設定を考慮する必要がある。

ちゃんと設定しておくと、logging.infoやlogging.errorで出力したレベルの情報をStackdriver上でも付与された状態でログを見れる。

f:id:nullpobug:20181222074919p:plain

Python 3.7、Django 2.1.4、google-cloud-logging 1.9.1で試した。

Djangoの設定

あらかじめ、対象のプロジェクトでStackdriver LoggingのAPIを有効にしておく。

また、クライアントライブラリ( google-cloud-logging ) をインストールしておく。

Djangosettings.py に追記する想定。

from google.cloud import logging as google_cloud_logging

log_client = google_cloud_logging.Client()

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'stackdriver_logging': {
            'class': 'google.cloud.logging.handlers.AppEngineHandler',
            'client': log_client,
        },
    },
    'loggers': {
        'django': {
            'handlers': ['stackdriver_logging'],
            'level': 'INFO',
            'propagate': True,
        },
        'django.request': {
            'handlers': ['stackdriver_logging'],
            'level': 'ERROR',
        },
    },
    'root': {
        'handlers': ['stackdriver_logging'],
        'level': 'INFO',
    }
}
  • AppEngineのデフォルトのサービスアカウントで書き込みは可能なので認証の設定は不要
  • ドキュメントにある setup_logging() は、呼ばないでおく(Django側でロギングのセットアップが走るので不要)
  • rootロガーの設定は好み
  • AppEngineを使っている場合のハンドラは、 google.cloud.logging.handlers.AppEngineHandler を使っておくとFlexible/Standardどちらでも都合が良いらしい
  • gunicornのログも流そうとすると、いろいろややこしいのでDjangoのみにしてある

参考

今年の振り返りとか

これは pyspa Advent Calendar 2018 の4日目の記事です。

https://adventar.org/calendars/3018

毎年のことですが振り返りです。

今年は気温が高かったせいか、夏野菜の苗は11月終わりぐらいまで枯れてなかった。すごい。

  • ほうれん草
    • 春手前の気温が上がりきる前に種を蒔いて育てたらうまく行った。

f:id:nullpobug:20180513144938j:plain

  • ミニ大根
    • これもほうれん草と同時期で栽培、気温が上がると花が咲いたり虫に食われまくったが、最初に収穫したものはいい感じ

f:id:nullpobug:20180513141322j:plain

  • ナス
    • 種まきの時期を逃していたので、苗から。植えて放置でも大丈夫だった。
  • ピーマン
    • 同じく種まきの時期を逃していたので、苗から。植えて放置でも大丈夫だった。
  • きゅうり
    • 世話できなかったので今年の収穫は少なめ。
  • パクチー
    • ベランダの鉢植えは日当たり足りなくて全然だめでしたが、畑に地植えしたものは大きく育ちました。

f:id:nullpobug:20181203023917j:plain

  • バジル
    • 畑に地植えしたら元気になった、のはいいのだけど、葉に土が付きやすいのがイマイチ。マルチで砂や土が舞うのを軽減させたほうがいいかもしれない。
  • イカ
    • 去年食べたカットフルーツに入ってたスイカの種から。品種不明だけど、植えたら育って20cmクラスの実を収穫できた。

f:id:nullpobug:20181203023914j:plain

  • さつまいも
    • いつもどおり苗から。忙しくて放置してたけど、しっかり育ってた。

f:id:nullpobug:20181203023911j:plain

仕事

去年の末ごろから手伝っている案件をずっとやっています。

お手伝い先では、やれることを引き受けてやっていますが、範囲が広くなってきました。

  • アーキテクチャを決めること
    • システム全体をどう作っていくか、どう連携させるかとか考えて決める役割
  • プロダクトに実装する機能と中長期的な開発スケジュール決め
    • 機能追加、画面追加とかで、どういうものを実装するのか大雑把に決める役割
      • 細かいところは実装者に概ね任せてしまっている(そこまでやるほど時間がない)
    • 方針を決めたり、指示を出したり。
    • チーム全体で何をいつごろ実装するか決める(このあたり任されてる)
  • チームのタスク管理
    • 開発リーダー兼マネージャーという感じだったので、チームメンバーのレベルを見つつタスクを振ったり
    • 最近マネージャーやれる優秀な若者がチームに参加して、細かなタスク割当てはその人に委譲した。それまでは大変だった。
  • コードレビュー
    • コードの品質が均一化するように。
    • あんまり細かい指摘はしない。
  • 技術的な課題解決
    • 他のチームメンバーがつまづいている課題を助けたり、難易度の高い部分をやっています
  • 採用
    • チームに参加する技術者の採用面接をやっている
  • パートナー企業との打ち合わせ
    • 技術面での責任者として参加
  • 雑多なヘルプ

雇われCTOみたいな感じです。

来年はどうなってるかな。

Djangoのデータベースルーターのallow_migrateの挙動について

Djangoフレームワークでは、データベースルータークラスを作成して、 settings.DATABASE_ROUTERS に設定することで、モデル毎に使用するデータベースを変えたりできます。

データベースルーターにはいくつかのメソッドを実装しますが、その中の一つ、 allow_migrate を実装した際のマイグレーション処理の挙動について気になったので調べました。

試した環境は、Python3.7.1、Django 2.1.3。

前提

  • Djangoのプロジェクト名: project1
  • アプリケーション名: myapp

対象のモデルは以下の通り:

myapp/models.py:

from django.db import models

class Spam(models.Model):
    name = models.CharField(max_length=20)

この Spam モデルをデータベースルーターにて、マイグレーションの対象外とします。

データベースルーターは以下の通り:

project1/db_router.py:

class DatabaseRouter:
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label == 'myapp':
            return False
        return None

settings.pyDATABASE_ROUTERS に設定しています。

project1/settings.py (抜粋):

DATABASE_ROUTERS = [
    'project1.db_router.DatabaseRouter',
]

makemigrationsを実行した場合

マイグレーションファイルを作成する makemigrations コマンドを実行した場合は、 allow_migrateFalse を返却しても、マイグレーションファイルは作成されます。

$ python manage.py makemigrations myapp
Migrations for 'myapp':
  myapp/migrations/0001_initial.py
    - Create model Spam

migrateを実行した場合

マイグレーションファイルを作成した状態で、マイグレーションを実行する migrate コマンドを実行した場合は、対象のマイグレーションはスキップされます。

$ python manage.py showmigrations myapp
myapp
 [ ] 0001_initial

$ python manage.py migrate myapp
Operations to perform:
  Apply all migrations: myapp
Running migrations:
  Applying myapp.0001_initial... OK

$ python manage.py showmigrations myapp
myapp
 [X] 0001_initial

このとき、Djangoマイグレーションの管理情報を格納するテーブルには、マイグレーションの実行済みを表すレコードが作成されます。

$ sqlite3 db.sqlite3 -line "select * from django_migrations"
     id = 1
    app = myapp
   name = 0001_initial
applied = 2018-11-20 20:59:28.637569

注意したい点

  • allow_migrateFalse を返しても、 migrate ではマイグレーションファイルがあればマイグレーションの管理テーブルには書き込まれる
    • 意図せず対象のデータベースに django_migrations データベースが作成されたり、レコードが書き込まれることがある
    • 管理情報を書き込みたくない場合は、モデルに Meta.managed = False を指定し、管理対象外にしておく必要がある

参考

Promiseとasyncとawait

Promiseとasync、awaitの使い方を理解するために書いたサンプルコード。 setTimeoutを使って1秒後に画面に文字列を出力する。

Promiseとthen()

Promiseオブジェクトの生成とthen()メソッドの利用例。

<html>
<head>
  <meta charset="utf-8"/>
  <title>Promiseとthen()</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

function main() {
  const promise = slowFunction();
  // Promiseの処理が完了したら(resolveが呼ばれたら)、結果を出力
  promise.then(result => {
    // resolve関数の引数で渡された値がresultに格納される
    print(result);
  })
}

function slowFunction() {
  // Promiseオブジェクトを返す
  return new Promise(resolve => {
    // 1秒後にresolveを呼び出す
    setTimeout(() => {
      resolve("Result");
    }, 1000);
  });
}
</script>
</body>
</html>

Promiseとthen() - JSFiddle

Promiseとreject()

Promiseオブジェクトの例外処理、reject()とcatch() メソッドを使う。

reject()を呼ぶと、catch()メソッドに渡したコールバック関数が実行される。

<html>
<head>
  <meta charset="utf-8"/>
  <title>Promiseとreject、catch()</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

function main() {
  const promise = slowFunction();
  // Promiseの処理が完了したら(resolveが呼ばれたら)、結果を出力
  promise.then(result => {
    print("Success");
  }).catch((message) => {
    // reject関数で渡された引数がcatchの引数に渡されてくる
    print(message);
  })
}

function slowFunction() {
  return new Promise((resolve, reject) => {
    // 1秒後にrejectを呼ぶ
    setTimeout(() => {
      // エラー内容などはreject関数の引数で渡す
      reject("Error!");
    }, 1000);
  });
}
</script>
</body>
</html>

Promiseとreject、catch() - JSFiddle

Promiseとthrow

then()メソッドに渡したコールバック関数内で例外を発生させる場合はthrowを使う。 catchメソッドに渡したコールバック関数が呼ばれる。

<html>
<head>
  <meta charset="utf-8"/>
  <title>Promiseとthrow、catch()</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

function main() {
  const promise = slowFunction();
  // When it is finished promise(called resolved), then output the result.
  promise.then(result => {
    throw new Error("Error!");
  }).catch((error) => {
    // result is Error object.
    print(error);
  })
}

function slowFunction() {
  // Returns promise object.
  return new Promise((resolve, reject) => {
    // Execute resolve function after a second.
    setTimeout(() => {
      resolve("Result");
    }, 1000);
  });
}
</script>
</body>
</html>

Promiseとthrow、catch() - JSFiddle

Promiseをawait構文で待つ

Promiseオブジェクトの処理が終わるのをawait構文で待つ例。

<html>
<head>
  <meta charset="utf-8"/>
  <title>Promiseとawait構文</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

// "await"構文は"async"キーワード付きの関数内でしか使えない
async function main() {
  // awaitで呼び出すと、resolveかrejectが呼ばれるまで止まる
  // resolveで渡された引数は、戻り値として返却される
  result = await slowFunction();
  print(result);
}

function slowFunction() {
  // Promiseオブジェクトを返す
  return new Promise(resolve => {
    // 1秒後にresolve関数を呼ぶ
    setTimeout(() => {
      resolve("Result");
    }, 1000);
  });
}
</script>
</body>
</html>

Promiseとawait構文 - JSFiddle

asyncとawait

async functionで関数を実装すれば、Promiseの発行を減らせるのと、then()のメソッドチェーン地獄がマシになる

<html>
<head>
  <meta charset="utf-8"/>
  <title>asyncとawait</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

// "await"構文は"async"キーワード付きの関数内でしか使えない
async function main() {
  // async functionの結果が返ってくるまで待つ
  result = await slowFunction();
  print(result);
}

async function slowFunction() {
  // setTimeoutをPromiseでラップし、終わるまでawaitで待つ
  await new Promise(resolve => setTimeout(resolve, 1000));
  // async functionの結果として文字列を返却
  return "Result";
}
</script>
</body>
</html>

asyncとawait - JSFiddle

async、awaitと例外処理

async、awaitを使う場合、例外はthrowで発生させて、try catch構文で処理できる。

<html>
<head>
  <meta charset="utf-8"/>
  <title>asyncとawait、throw()</title>
</head>
<body>
<button onclick="main()">Run</button>
<div id="output">
</div>
<script>
function print(text) {
  const element = document.getElementById('output');
  element.innerText = element.innerText + text + '\n';
}

// "await"構文は"async"キーワード付きの関数内でしか使えない
async function main() {
  // async functionでthrowされた例外はtry catch構文で処理できる
  try {
    result = await slowFunction();
    print(result);
  } catch (error) {
    print(error);
  }
}

async function slowFunction() {
  // setTimeoutをPromiseでラップし、終わるまでawaitで待つ
  await new Promise(resolve => setTimeout(resolve, 1000));
  // throwで例外を発生させる
  throw new Error("Error in async function!");
  return "Result";
}
</script>
</body>
</html>

asyncとawait、throw() - JSFiddle

参考

AWS IoT エンタープライズボタンを試してみる

AWS IoT エンタープライズボタンは、AmazonのDashボタンの汎用のやつ。

f:id:nullpobug:20180904024122j:plain
IoTボタン

www.amazon.co.jp

ボタンを押した際にEメール送信、SMS送信、Lambda関数の実行などを設定できる。

ボタンの動作にはWifiネットワークが必須となっている。初期セットアップはスマートフォンのアプリから実行すると簡単でした。

バイスの管理画面

管理画面は結構シンプル。バッテリー残量もわかるので便利。

f:id:nullpobug:20180904024124p:plain
IoT 1-Clickの管理画面

開発の流れ

  1. バイスを登録
  2. Lambda関数を用意しておく
  3. AWS IoT 1-Clickプロジェクトを作成
    • 実行したいテンプレートを選択(Lambda関数の実行など)
    • プレイスメントを選択(プレイスメントに対し、デバイスを登録する)

サンプルコードを用意して試す

Lambda関数でSlackに投稿してみるサンプルコードを作成し、試してました。Chaliceを使おうと思ってたんですが、AWS IoT 1-Clickのイベントに対応していないようだったので、素のPythonハンドラで作成。

app.py:

import requests
import json
import os


def post_to_slack(event, context):
    env_dict = os.environ
    url = env_dict['ENDPOINT']
    payload = {
        'text': 'ボタンが押されました。',
    }
    requests.post(url, data=json.dumps(payload))
    return {'status': 'ok'}

ソースコード一式はbitbucketに置いてます。

https://bitbucket.org/tokibito/sample_nullpobug/src/dd6f851e43e4872875e8c3dc57dd217b52017811/python/iot_button/?at=default

zipパッケージを作ってLambdaにアップロードし、環境変数でSlack APIのENDPOINTを設定しておきます。

また、ボタンを押すとこの関数が実行されるように設定しておきます。

ボタンを押すと、Slackにメッセージが投稿されることを確認しました。

f:id:nullpobug:20180904024120p:plain
Slackにメッセージが投稿された

うまく組み合わせれば使い所ありそうかな。