私へのインタビュー記事がエンジニアHubというメディアに掲載されています。 employment.en-japan.com Pythonの学習のためにどういったコードを読むと良いかなど話しました。
VagrantのUbuntu環境をアップグレードする
作業環境としてVagrant(VirtualBox)でUbuntu16.04を2017年ごろから使ってます。 そろそろ20.04も近くなってきたので、今更ですが既存の環境をUbuntu18.04に上げておこうかと思い、色々調べていたのでメモを残します。
使っているboxは ubuntu/xenial64
と ubuntu/bionic64
Vagrant box ubuntu/bionic64 - Vagrant Cloud
Q. Vagrantの仕組みで既存のUbuntu環境のメジャーアップデートはできるのか?
A.できない。Ubuntuなら通常通りdo-release-upgradeコマンドを使おう。新規にVMを作り直してもよいなら、そちらのほうが手軽。
Q. Vagrantfileのboxを変更したら既存のVMは使えない?
A.そのまま使える。boxの指定を ubuntu/xenial64
から ubuntu/bionic64
に変更しましたが、特に問題はなかった。
他に気をつけた点
- アップグレードするとadd-apt-repositoryで追加したaptリポジトリが無効化されるので、
vagrant provision
を再度実行すれば有効になるようにしとくのがよい。 - ホスト名はアップグレードで変わらないので、気になるなら変更すればいい。
- Vagrantfileに書いていたパッケージのインストールが、パッケージの名称変更により一部失敗していたので修正した。
実際にやった手順
Pythonの入門者向け動画教材
去年から手伝っていて、今年の4月には公開されていたのですが、Pythonの入門者向けの動画の教材を作っていました。
JMOOCというオンライン講座の1つとして公開されています。 www.fisdom.org 講座はFisdomというプラットフォームで配信されているため、利用には登録が必要ですが、受講料はかかりません。
CMSコミュニケーションズの寺田さんと2人で半分ずつ分担してやってました。
Pythonを学習するための手助けになればいいなと思います。
2020/4/24追記
2020年度分についてはこちらです。
DjangoでLOGGING設定をしているときに、IPythonの補完に不具合が出る場合の対処
Djangoに限ったことではないのですが、PythonのloggingでrootのハンドラをDEBUGにしていると、IPythonの補完を実行した際にparsoという依存ライブラリのログが出てしまい、カーソル位置がずれてしまうことあります。
例えば、Djangoで以下のように settings.py
でロギングの設定をした場合。
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'root': { 'handlers': ['console'], 'level': 'DEBUG', } }
Djangoの manage.py shell
では、IPythonがインストールされていると、IPythonが起動します。この設定で補完機能を使おうとTabキーを押したとき、次のようにログが出力されてカーソル位置がずれます。
parsoというモジュール(ipython → jedi → parsoの依存)がデバッグログを出力するのですが、これでは補完がまともに使えなくなってしまいます。
parsoのログをINFOレベルに設定することで、回避できます。(もしくは disable_existing_loggers
やrootロガーの設定変更で回避することもできます)
LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'handlers': { 'console': { 'level': 'DEBUG', 'class': 'logging.StreamHandler', }, }, 'loggers': { 'parso': { 'handlers': ['console'], 'level': 'INFO', 'propagate': False, }, }, 'root': { 'handlers': ['console'], 'level': 'DEBUG', } }
参考
オープンソースカンファレンス2019 HokkaidoでDjangoの紹介をしました
DjangoのSubqueryとOuterRefを使ってサブクエリを組み立てる
DjangoのORMで、サブクエリを使う方法について。任意のSQLであればrawメソッドを使えばよいのですが、なるべくORMのAPIを使いたい。
DjangoのORMでは任意の位置にサブクエリを使えるわけではないですが、例えば「テーブル単位での問い合わせ結果にサブクエリで得た列を追加する」ぐらいのことは、annotateメソッドを使ってできます。
サブクエリ側で条件を指定した絞り込みを行う場合、SubqueryとOuterRefを組み合わせるとできます。
Query Expressions | Django documentation | Django
モデル
Categoryモデルを親に持つ、Itemモデル、という関係性のデータです。
from django.db import models from django.utils import timezone class Category(models.Model): parent = models.ForeignKey( 'myapp.Category', null=True, blank=True, on_delete=models.CASCADE) name = models.CharField(max_length=20) class Meta: db_table = 'category' ordering = ['id'] def __str__(self): return self.name class Item(models.Model): category = models.ForeignKey( 'myapp.Category', null=True, blank=True, on_delete=models.SET_NULL) name = models.CharField(max_length=20) updated = models.DateTimeField(default=timezone.now) class Meta: db_table = 'item' ordering = ['id'] def __str__(self): return self.name
Djangoのシェルで試したコード
CountなどのAggregate Expressionでうまくクエリを作れない場合、Subqueryを継承すると、自由にクエリを作れます。
サブクエリの結果をCountするCountQueryクラスを定義して使ってます。
from datetime import timedelta from django.utils import timezone from django.db.models import OuterRef, Subquery, IntegerField import sqlparse from myapp.models import Category, Item class CountQuery(Subquery): """件数をカウントするサブクエリ """ template = "(SELECT COUNT(*) FROM (%(subquery)s) _count)" output_field = IntegerField() categories = Category.objects.annotate( updated_count=CountQuery( Item.objects.filter( category_id=OuterRef('pk'), updated__gt=timezone.now() - timedelta(days=1) # 24時間以内に更新されたItemの件数を取得 ) ) ) print(sqlparse.format(str(categories.query), reindent=True)) # sqlparseでSQL文を整形
組み立てたSQLは、sqlparseで整形して表示してみてます。
SELECT "category"."id", "category"."parent_id", "category"."name", (SELECT COUNT(*) FROM (SELECT U0."id", U0."category_id", U0."name", U0."updated" FROM "item" U0 WHERE (U0."category_id" = ("category"."id") AND U0."updated" > 2019-03-19 08:22:03.638946) ORDER BY U0."id" ASC) _count) AS "updated_count" FROM "category" ORDER BY "category"."id" ASC
理想はもう1段減らしたいのだけど、きれいに作る方法を見つけれてないです...
参考
Django 1.11 Annotating a Subquery Aggregate - Stack Overflow
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はスキーマ変更もトランザクション内で実行し、ロールバックできるので、何度実行しても同じになりますが、MySQLやOracleはスキーマ変更はトランザクションの対象外だからです。
MySQLの場合はスキーマ変更をして、データ更新に失敗した場合、スキーマ変更はロールバックされません。
この例だとカラムが残ったままま、マイグレーション失敗、という扱いになります。リカバリには手動でカラムを削除してやりなおす必要があります。
結論
スキーマ変更とデータ変更のマイグレーションは、同じクラスにまとめずに分けておいたほうが、失敗時のリカバリが楽です。