Asakusa Framework : MasterJoinのハマりやすいところ

f:id:teppei-studio:20131112202621j:plain

Asakusa Frameworkシリーズ、次はMasterJoin についてです。

これまで書いてきた記事はこちらです。

MasterJoinははじめの一歩でも使いましたが、Asakusa Frameworkが用意している数ある演算子の中でも、利用頻度の高い演算子の一つです。このMasterJoin演算子を使う上でハマりやすいところをまとめてみたいと思います。

この記事では、はじめの一歩と同様に、以下のような処理を行うバッチアプリケーションを題材に話を進めていきます。

f:id:teppei-studio:20131119191410p:plain

ソースは、GitHubにあげているので、適宜参照してください。

JOIN できるモデルは二つまで

今回のプロジェクトでは2つのモデルをJOINするだけですが、みっつ以上のモデルをJOINする場合、MasterJoinの処理を2回分実装いないといけません。でも、DMDLとしては以下のように書けてしまいます。ソース生成もうまくいきます。

joined joined_sales_detail = item_master -> {
	item_id -> item_id;
	item_name -> item_name;
} % item_id + sales_detail -> {
	sales_id -> sales_id;
	item_id -> item_id;
	sales_number -> sales_number;
} % item_id + item_category -> {
	item_id -> item_id;
	category_name -> category_name;
} % item_id;

で、試しに以下のようにOperatorクラスを実装してみようとしてみると、コンパイルエラーとなります。

package teppeistudio.operator;

import teppeistudio.modelgen.dmdl.model.ItemCategory;
import teppeistudio.modelgen.dmdl.model.ItemMaster;
import teppeistudio.modelgen.dmdl.model.JoinedSalesDetail;
import teppeistudio.modelgen.dmdl.model.SalesDetail;

import com.asakusafw.vocabulary.operator.MasterJoin;

public abstract class JoinOperator {

	@MasterJoin
	public abstract JoinedSalesDetail joinedData(ItemMaster master, SalesDetail detail, ItemCategory category);
}

エラー内容は以下の2つです。

  • マスタ結合演算子にはユーザー引数を利用できません
  • マスタ結合演算子の戻り値型は引数の結合結果を表す型である必要があります

ちょっと分かりにくいですね。

とにかく、MasterJoinはふたつのモデルのJoinのみだということに気をつけましょう。

結合キーとなるプロパティは結合後モデルに含める必要がある

結合キーにしたプロパティは結合後モデルに含める必要があります。

joined joined_sales_detail = item_master -> {
	item_id -> item_id;
	item_name -> item_name;
} % item_id + sales_detail -> {
	sales_id -> sales_id;
//	item_id -> item_id;
	sales_number -> sales_number;
} % item_id;

例えば、上記のようなモデルです。

item_idがコメントアウトされていますが、この状態だと、ソース生成時に以下のようにエラーになります。

[java] 14:26:35 [main] ERROR com.asakusafw.dmdl.util.AnalyzeTask - プロパティ "item_id" は定義されていません ( 略 )
[java] 14:26:35 [main] ERROR com.asakusafw.dmdl.util.AnalyzeTask - モデル "joined_sales_detail" のそれぞれのグルーピングは、同じ数のプロパティを指定する必要があります  ( 略 )

2行目のエラーが何を言っているのかよくわからないので、少し混乱しますが、キーに指定したプロパティを結合後モデルに含めることで、解消できます。

結合後モデルは出力できない

Joinの定義をしたので、その結合後モデルをそのままCSV出力したいところですが、できません。


【2013/12/10追記】
ごめんなさい。嘘です。できます。詳しくはAsakusa Framework : ケーススタディ:結合モデルのCSV出力 - TEPPEI STUDIOを参照してください。


GitHubにあげた例では、結合後モデルと同じプロパティ構成の別モデル「sales_detail2」を作って、このモデルで出力しています。結合後モデルからsales_detail2モデルは、射影演算子(project)を使っています。この演算子を使って、以下のように実装すれば、同じプロパティの値をコピーするようにすることができます。

Project<SalesDetail2> project = cp.project(joinedData.joined, SalesDetail2.class);

同じプロパティ構成のモデルを二つ定義するようで、少々冗長な感じがしますが、この方法でMasterJoin後のモデルを出力します。

モデル指定順序には注意が必要

Operator実装時には、どのモデルを第1引数に指定するか、第2引数に指定するか注意が必要です。
明確にマスターとなるモデルが決まっている場合はいいですが、そうではない場合には注意が必要です。

以下に、それぞれどう違うか示したいと思います。

尚、インプットとなるデータはどちらもこちらの通りです。

item-master.csv

"商品ID","商品名"
1,"チョコレート"
2,"アメ"

sales-detail.csv

"明細ID","商品ID","売上数"
10,1,100
11,2,200
12,1,300
13,2,400

マスタとなるモデルを第1引数、明細となるモデルを第2引数とした場合

Operatorで以下のように実装した場合、

@MasterJoin
public abstract JoinedSalesDetail joinedData(ItemMaster master, SalesDetail detail);

想定通り出力結果は以下のようになります。

明細ID,商品ID,商品名,売上数
10,1,チョコレート,100
12,1,チョコレート,300
11,2,アメ,200
13,2,アメ,400

マスタとなるモデルを第2引数、明細となるモデルを第1引数とした場合

逆にこのように実装した場合、

@MasterJoin
public abstract JoinedSalesDetail joinedData(SalesDetail detail, ItemMaster master);

以下のようになってしまいます。

明細ID,商品ID,商品名,売上数
10,1,チョコレート,100
11,2,アメ,200

「出力missedは結線されていません」とは

[java] java.io.IOException: 実行計画の作成に失敗しました ([JoinOperator.joinedDataの出力missedは結線されていません ([JoinOperator.joinedData(operator#222237550)])])

このようなエラーに出くわしても、Asakusa Frameworkに慣れていない限り、すぐにぴんとくる人は少ないのではないでしょうか。

【2014/1/4 追記】
0.5.3で改善されました

「出力missed」というのは、以下のような実装をした場合に、joinedDataでJOINされた結果として、マスタに存在しないキーが明細にあった場合にそのデータのことを指します。

JoinOperatorFactory op = new JoinOperatorFactory();
JoinedData joinedData = op.joinedData(this.itemMaster, this.salesDetail);

「結線」されていないのは、この「出力missed」の扱いが実装されていないということです。

つまり、以下の実装を追加することで、解消できます。

CoreOperatorFactory cp = new CoreOperatorFactory();
cp.stop(joinedData.missed);

CoreOperatorのstopメソッドに結線させていますが、これはつまり、「出力missed」を無視するという実装になります。

あるいは、以下のように実装すれば、「出力missed」を別ファイルに出力することができます。

Restructure<ErrorRecord> error = cp.restructure(joinedData.missed, ErrorRecord.class);
errorRecord.add(error.out);

Restructure演算子は、異なるモデル間のコピーを同じ名前のプロパティだけ実施するものです。

マスタモデルに存在して、明細モデルには存在しないキーはどこにも出力されない

当たり前といえば、当たり前なのですが、出力missedに出力されるのは、「マスタモデルには存在しなくて、明細モデルには存在するキーのレコード」だけです。逆に、「マスタモデルには存在するけど、明細モデルには存在しないキーのレコード」があったかどうかを知る方法はありません。

Operatorに実装するメッソド名の付け方

最後に、ハマり易いところとは、少し違いますが、私が工夫している点について、ひとつ紹介します。

今回の例では、Operatorに実装するJoin処理のメソッド名を、joinedDataとしています。

@MasterJoin
public abstract JoinedSalesDetail joinedData(ItemMaster master, SalesDetail detail);

通常は、joinDataのように、現在形動詞を名称に使うところでしょう。しかし、Asakusaの場合、このメソッド名を元に、クラス名が作られて、それをJobFlowで利用することになります。

JoinedData joinedData = op.joinedData(this.itemMaster, this.salesDetail);

動詞がクラス名になってしまうとなんとも分かりにくいので、私は過去形動詞をメソッド名にするようにしています。