データストア関連記事のまとめ その1

最近、Googleのサイトに上がっているMastering the datastoreの一連の記事を読みました。今まで、わりと適当にGoogle App Engine のデータストアを扱っており、いくら趣味プログラミングとはいえ、ある程度は内部処理についての知識がないと突っ込んだことはできないよなぁと感じていたので、ちょうどいい記事でした。

私個人の備忘録としても、簡単にまとめておいたほうが有益だろうと思ったので、3回程度に分けて一連の記事の概要を箇条書きしておくことにします。

Life of a Datastore Write
  • Datastoreへのデータの書き込みは、PythonJavaともにひとつのメソッドで実行されるが、その背後で複数種類の処理が走っている。
  • 書き込みは、大きくCommitフェーズとApplyフェーズに分かれる。
  • Commitフェーズでは、対象のエンティティグループのログへのエンティティデータの書き込みと、コミットされたことを示すマーク付けとが行われる。
  • Applyフェーズでは、エンティティデータの保存と、インデックスの更新とが行われる。
  • データの書き込み中に問題が発生した場合、Datastoreは自動的になんどかリトライする。
  • それでも駄目な場合、エラーが返る。この際、エンティティデータやインデックスが中途半端な状態になっている可能性がある。
  • Commitフェーズで問題が起きた時、インデックスは更新されていない。
  • Applyフェーズで問題が起きた時、DatastoreはCommitフェーズで記録されたログに基づいてやりなおそうとする。
  • 開発者は、このようなエラーが発生した場合、タスクキューに登録して後で再実行するか、ユーザにエラーを知らせて再度実行してもらうかするべき。
Transaction Isolation in App Engine
  • Datastoreは、トランザクションの中においては、隔離レベルはSerializable。
  • Datastoreは、トランザクションの外においては、隔離レベルはRead Committedに近い。
  • commit()プロセスにおいては、エンティティデータの保存→インデックスの更新の順で処理される。
  • 従って、commit()プロセスの途中では、エンティティデータは更新されているがインデックスは更新されていないという状態がありうる。
  • これにより、トランザクション外では、条件を満たさないエンティティが検索にひっかかったり、逆に、条件を満たすエンティティが取得できなかったりしうる。

Google App Engine で 1.0以上のバージョンのDjangoを使う

先週に投稿したエントリで、Google App EngineではDjango0.96しか使えないようなことを書いたのですが、ちゃんと調べてみると、現在のGoogle App EngineではDjangoの1.1および1.0.2のバージョンも使用できることがわかりました。

英語のドキュメントにしか載っていなかったので気づきませんでした。失礼しました。

というわけで、Google App EngineDjango 1.1 を使う方法を記録しておきます。

まず、現状のGoogle App Engine SDKには Django 0.96 しか含まれていないので、ローカルで開発するためにはDjangoのサイトから1.1もしくは1.0.2のバージョンをダウンロードしてインストールしておく必要があります。なお、実際のクラウド上の実行環境にはDjango 1.1 と 1.0.2 が存在していますので、Djangoのライブラリをプロジェクトの中に含める必要はありません。

Django 1.1 をダウンロードして解答し、以下のコマンドを実行したらインストール完了です。ちなみに、古いバージョンのDjangoがローカルにある場合は、新しいバージョンをインストールする前に古いバージョンをアンインストールしておく必要があります。setup.py install コマンドでインストールしていた場合は、Pythonがインストールされているフォルダにあるsite-packagesからdjangoフォルダを削除することでアンインストールできます。詳細は、Djangoのページを確認してください。

# python setup.py install

こうして準備が整ったら、HTTPリクエストを処理するPythonスクリプトの一番最初に以下の内容を加えます。

import os
os.environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from google.appengine.dist import use_library
use_library('django', '1.1')

これで、Django 1.1が使えるようになります。

試しに、前回やってみた辞書の内容を一覧表示するTemplateを以下のように書き換えてみると、正常に動作しました。

<table border="1">
    <tr><th>Parameter</th><th>Value</th></tr>
{% for key, value in my_dic.items %}
  <tr>
    <td>{{ key }}</td>
    <td>{{ value }}</td>
  </tr>
{% endfor %}
</table>
{% endblock %}

かなり見やすくなりましたね。

ところで、早いうちに以下のサイトを改良していきたいと思っていますが、なかなか仕事が忙しくて着手できません。


また、これとは別に、日々の雑感を書きとめるためのブログも開設しました。こちらは、エントリを作成するのにほとんど時間が必要ないので、継続的にエントリを投稿できそうです。


まぁ、どちらもあまり慌てずに、ぼちぼち進めていこうと思います。

DjangoのTemplateにて辞書の内容を一覧表示する

DjangoのTemplateでは、{% for a in list %}...{% endfor %}を使うことで、リストの内容を順番に処理していくことができます。
それと同じように、ディクショナリに登録されている内容を順番に表示させようと考えたのですが、
Google App EngineDjangoのバージョンがやや古いためか、{% for key, value in my_dic %} というここに書いてあった書き方は使えませんでした。

そこで、なんとかならないかといろいろと試行錯誤した結果、動作する方法にたどりついたのでここにメモとして残しておきます。

リクエスト処理のメソッドの中で、my_dicという名前の辞書が登録されているとします。
すると、以下のように書けば、この辞書の内容が一覧表示できます。

<table border="1">
  <tr><th>Parameter</th><th>Value</th></tr>
{% for kv in my_dic.items %}
  <tr>
    <td>{{ kv.0 }}</td>
    <td>{{ kv.1 }}</td>
  </tr>
{% endfor %}
</table>

なんだか無理やり感が漂いまくっていますが、Google App Engineで使えるDjangoのバージョンが上がるまではこれで我慢することにしました。

app-engine-patchの開発って、止まってないのかな?

久々にプロジェクトのサイトを見てみると、8月にリリースされたあとは、新しいバージョンが出ていないようだ。
プロジェクトの続行を心配する書き込みもサイトにあったようで、せっかくの良いプロジェクトなのにと少し心配になった。

あけましておめでとうございます

どんだけ放置してるんだと自分でも気になっているんですが、実際ほとんどサンデープログラマー活動を進められなかったという。なんというか、不況のあおりなのか何なのか、全体的に受注とか単価とかがモリモリ下がり気味で無駄に忙しくなっていますが、同業者の皆さんは元気でしょうか。

それはともかく、数ヵ月ぶりに家のEclipseで開発練習サイトのプロジェクトを開いたんですが、デバッグ実行してみるといつもの独自ブラウザが出ない。あれ?Google Plugin for Eclipseって、自前で持っていたデバッグ用のブラウザ(hosted browser)なくなったの?そういえば、Googleのサイトのドキュメントにも記述されてないし。

というわけでリリースノートとかドキュメントの記述とかを読んでみると、どうも今のバージョンなら、普通のブラウザでも指定されるプラグインを入れることでデバッグ実行が出来るらしい。より本番に近い環境で試せるわけなので、これは便利だと思います。

それにしても、GWT2.0の機能とかほとんど把握してません。宣言的なUIの作成とかどうやるんだろうか。
もうちょっといろいろ調べて触ってみたいと思います。

なお、あらためて、ここで作ったサイトを書いておきます。

タスク名の編集機能を実装する

前回で、タスクの追加、開始と停止、削除は実装しました。そこで今回は、タスク名の編集機能をつけようと思います。

大まかな仕様としては、表示されているタスク名をクリックするとテキストボックスに変わり、そのテキストボックスに新しいタスク名を入力してEnterキーを押すとタスク名が変更される、という感じです。

まず、タスク名の表示をLabelウィジットにしてクリックイベントを拾うように、タスク追加時のイベントハンドラを変更します。

    private void addTask() {
        ...(略)...
        // 名称編集
        final Label taskNameLabel = new Label(task.getName());
        tasksFlexTable.setWidget(row, 2, taskNameLabel);

        taskNameLabel.addClickHandler(new ClickHandler() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void onClick(ClickEvent event) {
                editTaskName(task);
            }
        });
        ...(略)...

次に、タスク名変更メソッドを追加します。名称を表示していたLabelの代わりにTextBoxを表示し、そのTextBoxに対して、Enterキーが押されたら名称を変更するというイベントハンドラをセットします。

    private void editTaskName(final TaskDto task) {
        final TextBox taskNameEditBox = new TextBox();
        taskNameEditBox.setText(task.getName());
        final int row = taskList.indexOf(task) + 1;
        final Label nameLabel = (Label) tasksFlexTable.getWidget(row, 2);
        tasksFlexTable.setWidget(row, 2, taskNameEditBox);
        taskNameEditBox.selectAll();

        taskNameEditBox.addKeyPressHandler(new KeyPressHandler() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void onKeyPress(KeyPressEvent event) {
                if (event.getCharCode() == KeyCodes.KEY_ENTER) {
                    task.setName(taskNameEditBox.getText());
                    nameLabel.setText(task.getName());
                    tasksFlexTable.setWidget(row, 2, nameLabel);
                }
            }
        });
    }

これで、タスク名の編集機能が実装されました*1。実際に作ったページはここですので、実際の動きはこのページで確認してください。

*1:ただ、このメソッドの書き方だと、LabelがクリックされるたびにTextBoxが生成されてしまいますね。実際にそれが問題になることはなさそうですが、もうちょっとちゃんと実装したほうがいいのかな。まぁ、ちょっと考えておきます。

いろいろな仕事に使った時間を記録するページを作る

調査が終わったところで、なんかちょっと作ってみようと思います。というところで、最初に思いついたのが「各仕事の作業時間を記録する」ページです。

私は、普段の仕事では、『あっちを少しやって、そのあとこっちも少しやって、また別件のメールを書いて、、、』というように、いくつかの仕事を同時に進めていることが多く、一日の終わりに『今日は結局、何にどれだけの時間を使ったのだろう?』と思うことが多いです。なので、着手した仕事を登録し、そのそれぞれに開始ボタンと停止ボタンを置き、開始している間の時間を記録するようなページを作ったら少し便利かなと思いました。

本来は登録した仕事をサーバ側に永続化したほうが何かと便利だと思いますが、とりあえずはクライアント側だけで処理を書いてみます。こうすると、当然のことながらそのブラウザを閉じたら情報は失われることになってしまいますので、1日中そのページを開き続けていないといけなくなりますが、まぁ、当面はそれで構わないでしょう。

# なお、今回作ったサイトはここ(Monotowa TIMEKEEPER)ですので、実際の動きはこちらのリンク先のページを確認してください。

まず、Timekeeperという名前でモジュールを作ります。説明のため、パッケージ名はcom.example.timekeeperとしておきます。まずは、各仕事を記録するためのTaskDtoクラスを作ります。このクラスは、最終的にサーバ側との通信にも利用するつもりですので、com.example.timekeeper.clientパッケージ内に作ります。

com.example.timekeeper.client.TaskDto

public class TaskDto implements Serializable {
    
    /** 状態:実行中 */
    public static int STATE_RUN = 1;
    /** 状態:停止中 */
    public static int STATE_STOP = 2;

    /** 名前 */
    private String name;
    /** 開始日時 */
    private Date startDate;
    /** 状態 */
    private int state = TaskDto.STATE_STOP;
    /** 投下時間(秒) */
    private long spendTime;
    // ...以下、Setter/Getterが続く

次に、EntryPointクラスを実装します。HTML側には、仕事を登録するためのフォームであるinputFormというパートと、仕事一覧を表示するmainCanvasというパートの二つがある想定です。

com.example.timekeeper.client.TimeKeeperEntryPoint

public class TimekeeperEntryPoint implements EntryPoint {

    /** タスク入力 */
    private TextBox taskInputText = new TextBox();
    /** タスク追加ボタン */
    private Button addTaskButton = new Button();

    /** タスク表示用テーブル */
    private FlexTable tasksFlexTable = new FlexTable();

    /** タスクリスト */
    private ArrayList<TaskDto> taskList = new ArrayList<TaskDto>();

    /**
     * @see com.google.gwt.core.client.EntryPoint#onModuleLoad()
     */
    @Override
    public void onModuleLoad() {
        // タスク入力フォームの描画
        addTaskButton.setText("Add");

        HorizontalPanel hPanel = new HorizontalPanel();
        hPanel.add(taskInputText);
        hPanel.add(addTaskButton);

        RootPanel.get("inputForm").add(hPanel);

        // タスク表示テーブルの描画
        VerticalPanel vPanel = new VerticalPanel();

        tasksFlexTable.setText(0, 0, "Start/Stop");
        tasksFlexTable.setText(0, 1, "Cost");
        tasksFlexTable.setText(0, 2, "Name");
        tasksFlexTable.setText(0, 3, "Delete");

        vPanel.add(tasksFlexTable);

        RootPanel.get("mainCanvas").add(vPanel);
    }
...

コンポーネントの表示はこれでできたと思うので、いくつか処理をつけたしていきます。

まず、タスク追加ボタンを押されたときに、タスクを追加する処理です。追加する際に、そのタスクの開始/終了ボタンと、削除ボタンを追加しておきます。タスク追加処理はaddTask()メソッド内に実装し、各タスクの開始/停止/削除は、それぞれstartTask()/stopTask()/deleteTask()メソッドに記述します。

    public void onModuleLoad() {
        ...(略)...
        // タスク追加処理
        addTaskButton.addClickHandler(new ClickHandler() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void onClick(ClickEvent event) {
                addTask();
            }
        });
        ...(略)...
    }

    private void addTask() {
        final TaskDto task = new TaskDto();
        task.setName(taskInputText.getText());
        task.setSpendTime(0);
        task.setState(TaskDto.STATE_STOP);
        taskList.add(task);

        int row = taskList.size();

        tasksFlexTable.setText(row, 1, String.valueOf(task.getSpendTime()));
        tasksFlexTable.setText(row, 2, task.getName());

        // 削除ボタン
        final Button deleteButton = new Button();
        deleteButton.setText("Delete");
        tasksFlexTable.setWidget(row, 3, deleteButton);

        deleteButton.addClickHandler(new ClickHandler() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void onClick(ClickEvent event) {
                deleteTask(task);
            }
        });

        //開始停止ボタン
        final Button startStopButton = new Button();
        startStopButton.setText("Start");
        tasksFlexTable.setWidget(row, 0, startStopButton);

        startStopButton.addClickHandler(new ClickHandler() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void onClick(ClickEvent event) {
                if (task.getState() == TaskDto.STATE_RUN) {
                    startStopButton.setText("Start");
                    stopTask(task);
                } else if (task.getState() == TaskDto.STATE_STOP) {
                    startStopButton.setText("Stop");
                    startTask(task);
                }
            }
        });

        taskInputText.setText("");
    }
    /**
     * タスク削除処理
     * @param task 削除するタスク
     */
    private void deleteTask(TaskDto task) {
        int row = taskList.indexOf(task);
        taskList.remove(row);
        tasksFlexTable.removeRow(row + 1);
    }

    /**
     * タスク開始処理
     * @param task 開始するタスク
     */
    private void startTask(TaskDto task) {
        task.setState(TaskDto.STATE_RUN);
        task.setStartDate(new Date());
    }

    /**
     * タスク停止処理
     * @param task 停止するタスク
     */
    private void stopTask(TaskDto task) {
        task.setState(TaskDto.STATE_STOP);
        Date now = new Date();
        task.setSpendTime(task.getSpendTime()
                + (now.getTime() - task.getStartDate().getTime()));
    }

最後に、定期的にタスクリストを再描画して、各タスクに費やしている時間を表示する処理を付け加えて出来上がりです。具体的には、タスクリストを再描画するrefreshTaskList()メソッドを用意し、1秒おきにそのメソッドが呼び出されるようにします。

    public void onModuleLoad() {
        ...(略)...
        // 定期処理
        Timer timer = new Timer() {
            @SuppressWarnings("synthetic-access")
            @Override
            public void run() {
                refreshTaskList();
            }
        };
        timer.scheduleRepeating(1000);
        ...(略)...
    }

    /**
     * タスクリストの再描画
     */
    private void refreshTaskList() {
        ...(略)...
    }

これで、最初に思っていたページが出来上がりました。サーバとの通信がないならば、ローカルで動作するGUIアプリを作っているのとほぼ同じ感覚で作っていけますので、個人的にはそれほど違和感がありませんでした。まぁ、そうやって作ったJavaのコードがJavaScriptコンパイルされるのだと思うと違和感ありまくりですが。