複数のレコードをまとめて更新する際にdjango-bulk-updateが便利

既存の複数のレコードをまとめて更新する際にdjango-bulk-updateというパッケージが便利だったので紹介します。
GitHub - aykut/django-bulk-update: Bulk update using one query over Django ORM

通常のDjangoのORMを使ったレコードの更新

Djangoで既存のレコードを更新するには、モデルインスタンスのsaveメソッドを呼ぶか、クエリセットのupdateを使うのが通常の方法です(Django1.9時点)

# Itemモデル
class Item(models.Model):
    value = models.CharField(max_length=20)
    class Meta:
        db_table = 'item'

# 新規作成(id=1で保存)
Item.objects.create(pk=1, value="spam")

# id=1のレコードの更新(saveメソッド)
item = Item.objects.get(pk=1)
item.value = "更新された値"
item.save()

# id=1のレコードの更新(QuerySetのupdateメソッド)
Item.objects.filter(pk=1).update(value="更新された値1")

これは、バックエンドにもよりますが、例えばMySQLの場合だとUPDATEクエリが実行されます。
複数件をまとめて更新する場合に、更新後の値がすべて同じであれば、クエリは単純でDjangoの標準ORMでも対応できます。

# 新規作成(id=1, 2, 3で保存)
Item.objects.create(pk=1, value="spam")
Item.objects.create(pk=2, value="egg")
Item.objects.create(pk=3, value="ham")

# id=1, 2, 3のレコードの更新(すべて同じ値にする)
Item.objects.filter(pk__in=[1, 2, 3]).update(value="更新された値")

しかし、一件ごとに違う内容で保存したい場合には、モデルインスタンスのsaveメソッドか、前述のupdateメソッドで何度もUPDATEクエリを実行することになります。
大量に更新する場合には、これだと時間がかかってしまいます。

raw SQLで対応するならどうやるか

例えばMySQLの場合、レコードの更新方法だと、UPDATE文以外にREPLACE文などもあります。またLOAD DATA INFILEでREPLACEを行う方法もあります。
ただしこれは、既存のレコードを削除した上で更新となるため、一部の列の内容だけを更新したい場合には、事前にSELECTした値を使う必要があったりして少し使いづらいことがあります。
更新対象のテーブルに主キー、または一意なキーがある前提の場合、UPDATE文のSETの値部分にCASE演算子を指定することで、一回のクエリの発行でそれぞれ別の値を更新できます。

mysql> SELECT * FROM item;
+----+-------+
| id | value |
+----+-------+
|  1 | spam  |
|  2 | egg   |
|  3 | ham   |
+----+-------+
3 rows in set (0.00 sec)

mysql> UPDATE item SET value=(
    ->  CASE id
    ->   WHEN 1 THEN 'updated_spam'
    ->   WHEN 2 THEN 'updated_egg'
    ->   WHEN 3 THEN 'updated_ham'
    ->  END)
    ->  WHERE id in (1, 2, 3);
Query OK, 3 rows affected (0.01 sec)
Rows matched: 3  Changed: 3  Warnings: 0

mysql> SELECT * FROM item;
+----+--------------+
| id | value        |
+----+--------------+
|  1 | updated_spam |
|  2 | updated_egg  |
|  3 | updated_ham  |
+----+--------------+
3 rows in set (0.00 sec)

他にもまとめて更新する方法はあるかと思いますが、ここでは言及しません。

django-bulk-updateを使う

django-bulk-updateを使うと、前述のUPDATEのSET部分にCASE演算子を使うクエリの発行をSQLの記述無しでできます。
PyPI上のパッケージ名はdjango-bulk-updateです。
django-bulk-update · PyPI

(venv)$ pip install django-bulk-update

使い方は、Djangoの標準ORMのbulk_createに似ています。
Django1.9、Python3.5、django-bulk-update1.1.8で試しました。

from bulk_update.helper import bulk_update

def my_update_task():
    # 更新対象のインスタンスを用意(明示的に生成せずに、QuerySetで取得したインスタンスでも可)
    updates = [
        Item(pk=1, value="updated_spam"),
        Item(pk=2, value="updated_egg"),
        Item(pk=3, value="updated_ham"),
    ]
    # value列だけを更新
    bulk_update(updates, update_fields=['value'])
    # 結果を表示
    print(Item.objects.values_list())

実行結果は次の通り。

>>> my_update_task()
[(1, 'updated_spam'), (2, 'updated_egg'), (3, 'updated_ham')]

MySQLの場合、実行されたクエリは次の通りです。

UPDATE `item` SET `value` = (CASE `id` WHEN 1 THEN 'updated_spam' WHEN 2 THEN 'updated_egg' WHEN 3 THEN 'updated_ham' END) WHERE id in (1, 2, 3)

このようにbulk_updateを使うことで、1クエリで複数件の更新を簡単にできるようになりました。便利。

2019/2/28追記

Django 2.2で同等の機能となるbulk_updateメソッドが実装されます。

django.db.models.query.QuerySet.bulk_update

jedi-vimのrpcフォークをWindowsで使ってみた

メモ書き。jedi-vimのjedi部分をRPCで動かすforkを@nakamurayが作ってくれたのでWindowsで動かしてみてた。
https://github.com/nakamuray/jedi-vim/tree/rpc
https://github.com/nakamuray/jedi-vim/tree/rpc2
そのままではWindowsで動かなかったので、手元で少し修正して使った。

virtualenvでも問題なく動作しているのでありがたい。

Windowsで動かすために修正したところ

  • Windowsだとstderrを指定しないと例外が発生していたので指定した(os.devnull)
  • サブプロセスの起動時にコマンドプロンプトのウィンドウが表示されてしまうのでstartupinfoを指定

作業のお供にノンアルコールビール(主にドライゼロ)

この記事はpyspa Advent Calendar 2015の2日目の記事です。
Python温泉は主催者の意向により、酒を飲むのは禁止となってます。
ノンアルコールビールは、酒としてカウントされないのでOKとなっています 。
前回のPython温泉で、「ノンアルコールビールでもドライゼロはうまい」とのタレコミがあり、どれどれと飲んでみたところ、確かにうまい。
参加者たちの間で人気となり、熱海周辺のスーパーマーケットでドライゼロを買い占める集団となってしまいました。

自分も飲んでみて、「これは新しい選択肢だ」と思ったので、この記事で少し紹介したいと思います。

ビールと比べてどうなのか?

ドライゼロの成分表はウェブサイトで確認できます。
アサヒ ドライゼロ | アサヒビール
まず、ドライゼロについては、アルコール分が0.00%でノンアルコールです(0%じゃないノンアルコールもあるので注意しよう)
飲んでも酔いませんし、自動車の運転もできます。
カロリー、糖質も0gとなっています。
後味はすっきりです。ブラックのほうは少し癖がありました。
うまみについては、生ビールと比べると少し物足りなさを感じます。

清涼飲料水やいわゆるノンアルコールの飲み物としてはどうなのか?

では酒ではなく、コーヒーや紅茶、ジュースなどと比べるとどうなのか。
カロリー、糖質が0gとなっている飲み物はノンアルコールビール以外にもありますね。
ノンアルコールビールは、 甘くない(むしろ苦い) カテゴリの飲み物ですね。
コーヒーなどと比べると、 ノンカフェイン の飲み物でもあります。
また、炭酸飲料でもあります。

価格

酒ではないので酒税がかからないため、ビールよりは安いですが、同容量のジュースよりは高めになっているようです。
2015年現在、350ml缶で120円前後で販売されているようです。

飲み物の選択肢として

ノンカロリー、ノンカフェイン、甘くない、炭酸を含む、だとウィルキンソンの炭酸水などが該当しますが、ノンアルコールビールも同様に選択肢としてはありだと思います。

作業のお供に試してみてはいかがでしょうか?

プログラマーは飲み物をつい過剰摂取してしまいがちなデスクワークですし、この機会に飲み物について見なおしてみてはどうでしょうか?
炭酸水のような甘くない炭酸飲料に抵抗がないのであれば、是非ドライゼロを試してみることをおすすめします。

Delphiでインターフェースを使う

久々にDelphiを少し触ってました。かなり忘れてる。
Delphiのインターフェースを使って2つの実装クラスを作り、実行時にどちらのクラスを使うか決定してる。
試したバージョンはDelphi XE4。

Main.dpr

program Main;
{$APPTYPE CONSOLE}

uses
  System.Classes,
  System.SysUtils,
  System.Generics.Collections;

type
  IStore = interface
  { データを読み書きするインターフェース }
    procedure SetValue(Key, Value: String);
    function GetValue(Key: String): String;
  end;

  TStore = class(TInterfacedObject, IStore)
  { データを読み書きするベースクラス }
  public
    procedure SetValue(Key, Value: String); virtual; abstract;
    function GetValue(Key: String): String; virtual; abstract;
  end;

  TMemoryStore = class(TStore)
  { データをメモリ上にのみ保持するクラス }
  private
    StoredData: TDictionary<String, String>;
  public
    constructor Create;
    destructor Destroy; override;
    procedure SetValue(Key, Value: String); override;
    function GetValue(Key: String): String; override;
  end;

  TFileStore = class(TStore)
  { データをファイルに保持するクラス }
  private
    Loaded: Boolean;
    FilePath: String;
    StoredData: TDictionary<String, String>;
    procedure Load;
    procedure Save;
  public
    constructor Create(FilePath: String);
    destructor Destroy; override;
    procedure SetValue(Key, Value: String); override;
    function GetValue(Key: String): String; override;
  end;

constructor TMemoryStore.Create;
begin
  WriteLn('-- TMemoryStore --');
  StoredData := TDictionary<String, String>.Create;
end;

destructor TMemoryStore.Destroy;
begin
  StoredData.Free;
end;

procedure TMemoryStore.SetValue(Key, Value: String);
begin
  StoredData.AddOrSetValue(Key, Value);
end;

function TMemoryStore.GetValue(Key: String): String;
begin
  if StoredData.ContainsKey(Key) then
    Result := StoredData.Items[Key]
  else
    Result := '';
end;

constructor TFileStore.Create(FilePath: String);
begin
  WriteLn('-- TFileStore --');
  Loaded := False;
  Self.FilePath := FilePath;
  StoredData := TDictionary<String, String>.Create;
end;

destructor TFileStore.Destroy;
begin
  Save;
  StoredData.Free;
end;

procedure TFileStore.SetValue(Key, Value: String);
begin
  { データベースの遅延読み込み }
  if not Loaded then Load;
  StoredData.AddOrSetValue(Key, Value);
end;

function TFileStore.GetValue(Key: String): String;
begin
  { データベースの遅延読み込み }
  if not Loaded then Load;
  if StoredData.ContainsKey(Key) then
    Result := StoredData.Items[Key]
  else
    Result := '';
end;

procedure TFileStore.Load;
var
  Buffer: TStringList;
  Index: Integer;
  KeyValuePair: TArray<String>;
begin
  Buffer := TStringList.Create;
  if FileExists(FilePath) then
    try
      WriteLn('Loading database...');
      Buffer.LoadFromFile(FilePath);
      for Index := 0 to Buffer.Count - 1 do
      begin
        KeyValuePair := Buffer[Index].Split([','], 2);
        StoredData.AddOrSetValue(KeyValuePair[0], KeyValuePair[1]);
      end;
    finally
      Buffer.Free;
    end;
  Loaded := True;
end;

procedure TFileStore.Save;
var
  Buffer: TStringList;
  Key: String;
begin
  Buffer := TStringList.Create;
  try
    for Key in StoredData.Keys do
    begin
      Buffer.Add(Key + ',' + StoredData.Items[Key]);
    end;
    Buffer.SaveToFile(FilePath);
  finally
    Buffer.Free;
  end;
end;

function StoreFactory(FilePath: String = ''): IStore;
begin
  { FilePathが指定されているならTFileStore、なければTMemoryStoreを使います }
  if FilePath <> '' then
    Result := TFileStore.Create(FilePath)
  else
    Result := TMemoryStore.Create;
end;

var
  Store: IStore;

begin
  { メモリリーク検出 }
  ReportMemoryLeaksOnShutdown := True;

  { コマンドライン引数の有無で実装を切り替える }
  if ParamCount > 0 then
    Store := StoreFactory(ParamStr(1))
  else
    Store := StoreFactory;

  { 値の書き込みと取得 }
  Store.SetValue('MyKey', 'MyValue');
  WriteLn(Store.GetValue('MyKey'));
end.

実行結果

C:\MyApp>dcc32 Main.dpr
Embarcadero Delphi for Win32 compiler version 25.0
Copyright (c) 1983,2013 Embarcadero Technologies, Inc.
Main.dpr(169)
170 行, 0.14 秒, コード 919344 バイト, データ 35392 バイト

C:\MyApp>Main
-- TMemoryStore --
MyValue

C:\MyApp>Main database.txt
-- TFileStore --
Loading database...
MyValue

外部入出力の部分は実装の切り替えができると、自動テストのときに便利です。DIコンテナは大仰だなと思ったら、このぐらいでもいいかもしれませんね。
また、TStoreはTInterfacedObjectを継承しているので、明示的にFreeを呼ばなくても、開放されるようにしています。

通信料金の計算2015年秋

通信料金がいくらぐらいかかっているのか計算などやってたので、メモを残す。

現状の使い方

  • auガラケー、Nexus4、PocketWifi(GL10P)の3台を持ち歩いてる
  • Nexus4はデータ通信専用、SORACOM AirのSIMを挿してるけど、できるだけPocketWifiのほうを使ってる。
  • b-mobileのSIMをしばらく使っていたけど、期限が切れたタイミングでSORACOM Airに変更した。

通信料金

  • au使用料(ガラケー) 2060円/月
  • ワイモバイル(PocketWifi) 4535円/月
  • SORACOM Air(Nexus4/s1.standard) 約500円/月

合計: 7095円/月

感想

auガラケーは滅多に通話しないので安い。
ワイモバイルがやっぱりちょっと高いので、同等のもので安く抑える方法を模索したい。

dulwichを使ってPythonでGitのカスタムコマンドを作る

Gitのコマンドがわかりづらくて好きになれないんですが、気に入らない部分はカスタムコマンドを作ればいいよね。
ということで方法を調べてました。

カスタムコマンドの作り方

Gitのカスタムコマンドは、「git-コマンド名」のような実行可能ファイルを用意して、パスが通ってる場所に置けば有効になるようです。
実行可能であればシェルスクリプトでも問題ないようで、git-flowなどはシェルスクリプトで実装されていますね。

Pythonでdulwichを使ってみる

dulwichはGitリポジトリPythonで読み書きするためのモジュールです。
Dulwich
Pythonでdulwichを使って実行可能なスクリプトを書くことで、カスタムのGitコマンドをPythonで実装する、というのをやってみます。
試したバージョンは、dulwich 0.11.2, Python 3.4。
dulwichのインストールはpipでvirtualenvにでも入れておけばよいかと。
dulwichの使い方はドキュメントのチュートリアルが参考になります。
Tutorial — dulwich 0.14.0 documentation
以下のようにgit-helloという名前のファイルを作成します。
git-hello:

#!/usr/bin/env python
import os
import itertools

from dulwich.repo import Repo


def decode(b):
    return b.decode('utf8')


def main():
    print("----- My Git Command! -----")
    repo = Repo(os.getcwd())
    # Walkerを使ってコミットを取得し、id, author, messageを表示する
    for entry in itertools.islice(repo.get_walker(), 10):
        print("{}: {}, {}".format(
            decode(entry.commit.id),
            decode(entry.commit.author),
            decode(entry.commit.message).strip()))


if __name__ == '__main__':
    main()

先頭のshebangの部分は、環境に応じて変更すると良いかと思います(virtualenvを使っているなら、virtualenv内のpythonにするとか)
これを適当なディレクトリに置いてパスを通します。今回は $HOME/bin に置きました。

(venv)tokibito@ubuntu:~$ ls -l bin
合計 8
-rwxrwxr-x 1 tokibito tokibito  471 11月  5 23:34 git-hello
drwxrwxr-x 5 tokibito tokibito 4096 11月  5 23:07 venv
(venv)tokibito@ubuntu:~$ export PATH=$PATH:$HOME/bin

gitリポジトリになっているディレクトリで実行してみると、最近の10件のコミットが表示されました。
試したのは django-ftpserver のローカルリポジトリ

(venv)tokibito@ubuntu:~/github/django-ftpserver$ git hello
----- My Git Command! -----
a2fa94d4e594d25a9b28203b3a82e5f47b43d67d: Shinya Okano <tokibito@gmail.com>, fixes PEP8, and update version 0.3.2
a8fa3f773714c8b80d8e65cc42c2e5add2b70724: Shinya Okano <tokibito@gmail.com>, Merge pull request #7 from akoumjian/master

merged: Custom Authorizer and Handler classes via settings
659e0eb3204a56a56d0946a9ee880a9adb259cc5: Shinya Okano <shinya.okano@beproud.jp>, added: docker settings for test env
9df037ba02770e1bdd0be8e23ed024031a842948: Alec Koumjian <akoumjian@gmail.com>, allow custom authorizers and handlers

custom tls

add note in docs
8f63fcb3873aa2e9f85870e84a05cf30fc26bb85: Shinya Okano <tokibito@gmail.com>, modify document config
0189b075a633717b27443449558ed6ceda77dbdc: Shinya Okano <tokibito@gmail.com>, update library for documentation
0bc3875f6649db4a0d4234c20a9a14360667e651: Shinya Okano <tokibito@gmail.com>, update version 0.3.1
34d5ef6dc168e43010e4a2e80c346c097671caa7: Shinya Okano <tokibito@gmail.com>, flake8
9c9437f30336abb779871f78ce7d0f39a5e90b17: Shinya Okano <tokibito@gmail.com>, update travis config
664fd42ef583b4f5f2c6ac26827e11ea4f50a4c3: Shinya Okano <tokibito@gmail.com>, modify setup test

こんな感じで、Repoオブジェクトからいろいろ辿れるので、やりたい放題ですね。

A5:SQL Mk-2が便利だった

A5:SQL Mk-2はER図の作成やSQLの実行、結果の表示などができるGUIのツール。Windows用。
ちょっとしたSQLを書くときや、ER図作成のためにWindows用のツールがほしくて、探してたところ見つけた。
しばらく使ってみて、そこそこいい感じだったので紹介する。
A5:SQL Mk-2 - フリーの汎用SQL開発ツール/ER図ツール
MySQLPostgreSQL、SQLite3、OracleSQLServerなど、自分が使うことがありそうなデータベースには一通り対応しているようだ。
PostgreSQLMySQLとSQLite3で試しているが、DDL生成で一部うまくいかない部分があったけど、データを参照したり追加したりなどの操作では概ね問題ない感じ。

ポータブルで動かす機能や、読み込み専用版が用意されているなど、調査業務でも役に立ちそう。