S-JIS[2013-12-15/2021-12-21] 変更履歴

Asakusa Framework Option系クラス

Asakusa FrameworkOperator DSLで使うOption系クラスについて。


概要

ユーザー演算子をプログラマーが実装する際は、DMDLから生成されたモデルクラスのgetter/setterメソッドを使って値の移送や演算を行う。
これらのメソッドは、int・String等の“通常のJavaのプリミティブ型およびクラス”を扱うメソッドの他に、Option系クラス(AsakusaFW独自のクラス)で扱うメソッドが用意されている。

Option系クラスはint・String等を内包している感じのクラスで、モデルクラスのフィールドはOption系クラスを保持するようになっている。
また、Option系クラスはHadoopのWritableとの変換機能を持っている。

DMDLのデータ型とOption系クラスの対応
DMDL上の
型の名前
Optionクラス名 Optionクラスが内包している
Javaの型
説明
INT IntOption int 32bit符号付き整数
LONG LongOption long 64bit符号付き整数
FLOAT FloatOption float 単精度浮動小数
DOUBLE DoubleOption double 倍精度浮動小数
TEXT StringOption org.hadoop.io.Text 文字列
DECIMAL DecimalOption java.math.BigDecimal 十進数
DATE DateOption com.asakusafw.runtime.value.Date 日付
DATETIME DateTimeOption com.asakusafw.runtime.value.DateTime 日時
BOOLEAN BooleanOption boolean 論理値
BYTE ByteOption byte 8bit符号付き整数
SHORT ShortOption short 16bit符号付き整数

なお、Option系クラスは読み取るだけならマルチスレッドセーフ(MTセーフ)なので、Operatorクラスに定数のstaticフィールドとして保持する使い方は問題ない。[2015-07-06]


値の取得・設定(各Option共通)

DMDLから生成されたモデルクラスでは、intやlongといった普通のJavaの型を扱うメソッドと、Optionクラスそのものを返すメソッドが生成されている。

//データモデルsales_detailのINT型のプロパティーamountの例

	public void example(SalesDetail model) {
		int value = model.getAmount();
		model.setAmount(value * 2);
	}

↓↑同じ

import com.asakusafw.runtime.value.IntOption;

	public void example(SalesDetail model) {
		IntOption amountOption = model.getAmountOption();
		int value = amountOption.get();
		amountOption.modify(value * 2);
	}

モデルクラスの普通のJavaの型を扱うメソッドも、内部ではOptionクラスのメソッドを呼び出している。
なお、setterに相当するmodifyメソッドは、プログラマーが直接呼び出すのは非推奨になっている。
(OptionからOptionへコピーするcopyFromというメソッドもあるが、これも非推奨)
Java8のOptionalScalaのOptionは不変オブジェクトだが、AsakusaFWのOption系クラスは可変オブジェクトである。しかしなるべく変更させない(不変オブジェクトの様に扱わせたい)為に、setという名前を使わずmodifyという変わったメソッド名にし、またmodifyやcopyFromは非推奨にしているのだと思う)

※get()は値がnullだとNullPointerExceptionが発生するので注意。

→値を比較するために取得するなら、getメソッドでなくhasメソッドの方が便利かも。[2016-02-11]


nullの扱い(各Option共通)

Option系クラスでは、nullも保持できるようになっている。
もしnullを保持している状態でget()を呼び出すとNullPointerExceptionが発生するので、nullになる可能性があるプロパティーから値を読み出す場合はnullチェックをきちんと行う必要がある。

//データモデルsales_detailのINT型のプロパティーamountの例

	public void example(SalesDetail model) {
		int value;
		if (model.getAmountOption().isNull()) {
			value = 0;
		} else {
			value = model.getAmount();
		}

		model.setAmount(value * 2);
	}

↓↑同じ

	public void example(SalesDetail model) {
		int value = model.getAmountOption().or(0);

		model.setAmount(value * 2);
	}

保持している内容がnullのときに初期値を使うという場合は、orメソッドが便利。
orメソッドは、内容がnullの場合は与えられた引数を返し、null以外の場合は保持している内容を返す。

AsakusaFW 0.9.1で、isPresent(「! isNull()」と同等)、orOptionメソッドが追加になった。[2017-04-30]

→値を比較するためにnullチェックしているのなら、isNullメソッドでなくhasメソッドの方が便利かも。[2016-02-11]


AsakusaFWの思想としては、

  1. バッチ全体の最初に入力データの精査を行い、データ形式が正しいかどうかをチェックする。つまりnullチェック等をきちんと行い、エラーにしたり変換したりして正しいデータだけを後続処理に渡す。
  2. 後続処理では(正しいデータだけが来ているはずなので)(nullチェックはせずに)データを普通にgetする。

という事なのだと思う。


等値比較(各Option共通)

Option系クラスではhashCodeやequalsメソッドが実装されているので、Optionクラスのままでの比較も問題なく行える。

	private void example(SalesDetail model1, SalesDetail model2) {
		if (model1.getAmount() == model2.getAmount()) {	// int
			〜
		}
	}

↓↑ほぼ同じ

	private void example(SalesDetail model1, SalesDetail model2) {
		if (model1.getAmountOption().equals(model2.getAmountOption())) {
			〜
		}
	}

一見すると上の方式の方が短くて良さそうだが、値がnullの場合はNullPointerExceptionが発生するので注意。
下の方式だとお互いがnullの場合も真になる。

※特にStringOptionでは下の方式が良いと思う。


また、各Optionクラスにはhasメソッドが用意されている。[2016-02-11]
これは、Optionの中の値が一致しているかどうかを判定するもの。

	private boolean compare(StringOption option, String s) {
		if (option.isNull()) {
			return s == null;
		}
		return option.getAsString().equals(s);
	}

↓↑ほぼ同じ

	private boolean compare(StringOption option, String s) {
		return option.has(s);
	}
	private boolean compare(IntOption option, int n) {
		return option.isNull() ? false : option.get() == n;
	}

↓↑ほぼ同じ

	private boolean compare(IntOption option, int n) {
		return option.has(n);
	}

退避方法(各Option共通)

値を保存(退避)しておきたい場合、Option系クラスでは注意が必要。[2014-12-21]

プロパティー1の値をプロパティー2に移し、プロパティー1に新しい値をセットする例
    StringOption IntOption 備考
Optionで保存 × StringOption temp = model.getText1Option();
model.setText1AsString("text1");
model.setText2Option(temp);
IntOption temp = model.getValue1Option();
model.setValue1(123);
model.setValue2Option(temp);
退避した直後に値を変更しているので、
1も2も同じ値になってしまう。
StringOption temp = model.getText1Option();
model.setText2Option(temp);
model.setText1AsString("text1");
IntOption temp = model.getValue1Option();
model.setValue2Option(temp);
model.setValue1(123);
値をコピーしてから変更している。
StringOption temp = new StringOption();
temp.copyFrom(model.getText1Option());
model.setText1AsString("text1");
model.setText2Option(temp);
IntOption temp = new IntOption();
temp.copyFrom(model.getValue1Option());
model.setValue1(123);
model.setValue2Option(temp);
退避用のOptionにコピーしている。
Textで保存 × Text temp = model.getText1();
model.setText1AsString("text1");
model.setText2(temp);
  退避した直後に値を変更しているので、
1も2も同じ値になってしまう。
Text temp = model.getText1();
model.setText2(temp);
model.setText1AsString("text1");
  値をコピーしてから変更している。
String/intで保存 String temp = model.getText1AsString();
model.setText1AsString("text1");
model.setText2AsString(temp);
int temp = model.getValue1();
model.setValue1(123);
model.setValue2(temp);
 

OptionやTextを取得すると、モデルオブジェクト内で使っているインスタンスがそのまま返ってくる。
AsakusaFWのOption系クラスやTextは可変オブジェクトなので、モデルオブジェクトの値を変更すると、取得したOptionやTextも同じく変わってしまう。
Stringやint等のOption内部の値(不変オブジェクト)で取得すれば、(インスタンスは別になるので)モデルオブジェクトの値を変更しても影響を受けない。

プロパティーへの値のセット(StringOption同士やIntOption同士の値の受け渡し)は値のコピーなので(インスタンスの共有ではないので)、影響を受け合うことは無い。


数値系Option

IntOption・LongOptionやDecimalOptionといった数値を扱うOptionクラスでは、値を加算するaddメソッドが用意されている。

	public void example(SalesDetail model) {
		model.setAmount(model.getAmount() + 123);
	}

↓↑同じ

	public void example(SalesDetail model) {
		model.getAmountOption().add(123);
	}

↓↑同じ

	private static final IntOption INT123 = new IntOption(123);

	public void example(SalesDetail model) {
		model.getAmountOption().add(INT123);
	}

addメソッドも(get()と同じく)足される側の値がnullだったらNullPointerExceptionが発生する。
足す側を足される側と同じOptionクラスとすることも出来るが、その場合、足す側の値がnullだとNullPointerExceptionは起きず、何も処理されない。
(上記の例のINT123の中身がもしnullだったら、amountには何も加算されず、NPEも発生しない。(その場合でも、足される側(amount)がnullだとNPEが発生する))


値がnullだったら0として加算したいような場合は、orメソッドと組み合わせる。

// DecimalOption(BigDecimal)の例

import java.math.BigDecimal;

	private void example(HogeModel model1, HogeModel model2) {
		model1.setAmount(model1.getAmountOption().or(BigDecimal.ZERO).add(model2.getAmountOption().or(BigDecimal.ZERO)));
	}

↓↑同じ

import com.asakusafw.runtime.value.DecimalOption;

	private void example(HogeModel model1, HogeModel model2) {
		add(model1.getAmountOption(), model2.getAmountOption());
	}

	@SuppressWarnings("deprecation")
	private static void add(DecimalOption decimal1, DecimalOption decimal2) {
		decimal1.modify(decimal1.or(BigDecimal.ZERO).add(decimal2.or(BigDecimal.ZERO)));
	}

Option系クラスは内部で値を保持している為、Optionオブジェクトを渡して演算することも出来る。
こういったメソッドを集めたユーティリティークラスを用意しておくと便利かも。


DateOption・DateTimeOption

DateOptionは日付(年月日)、DateTimeOptionは日時(年月日時分秒)を保持するOptionクラス。

IntOptionがint、StringOptionがText(一応String)を保持しているのでDateOptionはDateを保持している…と思ったら、java.util.Dateではなく独自のcom.asakusafw.runtime.value.Dateを使っている^^;
DateTimeもAsakusaFW独自のクラスで、秒までしか保持しない。(ミリ秒以下は扱えない)
どうでもいいが、DateクラスのJavadocに民法へのリンクが張ってあるのは違和感があるな(爆)

DateやDateTimeクラスには年・月・日などの要素を個別に指定できるコンストラクターがあったり、それらを個別に取得するgetterがあったりして、便利。
ただ、要素を個別に設定するsetterや、差分を取ったり時間を加算したりするメソッドは無い。
com.asakusafw.runtime.value.DateUtilに似たことをやっているメソッドがあるので、それを参考にして自分で作ることは出来るが…。

import com.asakusafw.runtime.value.Date;
import com.asakusafw.runtime.value.DateTime;
import com.asakusafw.runtime.value.DateUtil;
Date・DateTimeの使用例
  Date DateTime
コンストラクター Date d = new Date(2013, 12, 15); DateTime dt = new DateTime(2013, 12, 15, 23, 59, 59);
Date d2 = new Date(d.getElapsedDays()); DateTime dt2 = new DateTime(dt.getElapsedSeconds());
int year = d.getYear(); int year = dt.getYear();
int month = d.getMonth(); int month = dt.getMonth();
int day = d.getDay(); int day = dt.getDay();
  int hour = dt.getHour();
  int minute = dt.getMinute();
  int second = dt.getSecond();
基準からの差分 int days = d.getElapsedDays(); long seconds = dt.getElapsedSeconds();
d.setElapsedDays(days); dt.setElapsedSeconds(seconds);
2つの時点の差 int days = d2.getElapsedDays() - d1.getElapsedDays(); long seconds = dt2.getElapsedSeconds() - dt1.getElapsedSeconds();
日の加算 int days = d.getElapsedDays() + 日数;
Date d2 = new Date(days);
long seconds = dt.getElapsedSeconds() + 日数 * 86400;
DateTime dt2 = new DateTime(seconds);
文字列からの変換
[/2015-06-21]
Date d = Date.valueOf("20131215", Date.Format.SIMPLE); DateTime dt = DateTime.valueOf("20131215235959", DateTime.Format.SIMPLE);
Date d = Date.valueOf("2015-06-21", Date.Format.STANDARD); 0.7.0 DateTime dt = DateTime.valueOf("2015-06-21 12:34:56", DateTime.Format.STANDARD); 0.7.0
Date d = new Date(DateUtil.parseDate("2015/06/21", '/')); 0.7.0 DateTime dt = new DateTime(DateUtil.parseDateTime("2015/06/21 12:34:56", '/', ' ', ':'));
DateTime dt = new DateTime(DateUtil.parseDateTime("2015-06-21T12:34:56", '-', 'T', ':'));
0.7.0
java.util.Dateからの変換 java.util.Date date = 〜;
java.util.TimeZone tz = java.util.TimeZone.getTimeZone("UTC");
java.util.Calendar cal = java.util.Calendar.getInstance(tz);
cal.setTime(date);
Date d = new Date(DateUtil.getDayFromCalendar(cal));
java.util.Date date = 〜;
java.util.TimeZone tz = java.util.TimeZone.getTimeZone("UTC");
java.util.Calendar cal = java.util.Calendar.getInstance(tz);
cal.setTime(date);
DateTime dt = new DateTime(DateUtil.getSecondFromCalendar(cal));
java.util.Dateへの変換 java.util.TimeZone tz = java.util.TimeZone.getTimeZone("UTC");
java.util.Calendar cal = java.util.Calendar.getInstance(tz);
DateUtil.setDayToCalendar(d.getElapsedDays(), cal);
java.util.Date date = cal.getTime();
java.util.TimeZone tz = java.util.TimeZone.getTimeZone("UTC");
java.util.Calendar cal = java.util.Calendar.getInstance(tz);
DateUtil.setSecondToCalendar(dt.getElapsedSeconds(), cal);
java.util.Date date = cal.getTime();
java.time.LocalDateとの変換
[2021-12-21]
LocalDate ldate = 〜;
Date d = new Date(ldate.getYear(), ldate.getMonthValue(), ldate.getDayOfMonth());

Date d = new Date((int) ldate.toEpochDay() + 719162);
 
Date d = 〜;
LocalDate date = LocalDate.ofEpochDay(d.getElapsedDays() - 719162)
 
java.time.LocalDateTimeとの変換
[2021-12-21]
  LocalDateTime ldt = 〜;
DateTime dt = new DateTime(ldt.getYear(), ldt.getMonthValue(), ldt.getDayOfMonth(), ldt.getHour(),
ldt.getMinute(), ldt.getSecond());

DateTime dt = new DateTime(ldt.toEpochSecond(ZoneOffset.UTC) + 62135596800L);
  DateTime dt = 〜;
LocalDateTime ldt = LocalDateTime.ofEpochSecond(date.getElapsedSeconds() - 62135596800L, 0, ZoneOffset.UTC);
DateとDateTimeの相互変換 Date d = new Date(DateUtil.getDayFromSeconds(dt.getElapsedSeconds())); DateTime dt = new DateTime(d.getElapsedDays() * 86400L);

値は、Dateはint(基準日0001/01/01からの経過日数)、DateTimeはlong(基準日時0001/01/01 00:00:00からの経過秒数)で保持している。
(Java標準のjava.util.Date等とは基準日が異なるので注意)

String(文字列)からの変換は専用のvalueOfメソッドが提供されているので、その書式を使う場合はSimpleDateFormatよりも実行効率が良い。
(少なくとも、SimpleDateFormatをstaticフィールドに定義するのはやめた方がよい。→マルチスレッドの注意 [2015-06-21]


AsakusaFWのDateやDateTimeは(基準時点からの経過日数・秒数で値を保持しているので)(java.util.Dateと同様に)タイムゾーンは保持していない。[2016-05-30]
一方、SimpleDateFormatCalendarはタイムゾーンを保持しているので、変換の際には考慮が必要。

AsakusaFWのDateUtilのgetFromCalendar系メソッドでは、Calendarのgetメソッドを呼んで値を取得しているので、Calendarのタイムゾーンが考慮された値が使われることになる。
setToCalendar系メソッドでもCalendarのsetメソッドを呼んで値を設定しているので、Calendarのタイムゾーンの値としてセットされることになる。

DateUtilでjava.util.Dateを引数にとるgetFromDate系メソッドでは内部でCalendarインスタンスが使われるが、このCalendarはデフォルトタイムゾーンとなるので、注意が必要。


Scalaとの類似

AsakusaFWのOption系クラスは、ScalaのOptionクラスと似ている部分がある。

  AsakusaFWの
IntOption
Scalaの
Option[Int]
更新日
nullチェック   opt.isNull() opt.isEmpty  
非nullチェック 0.9.1 opt.isPresent() opt.isDefined
opt.nonEmpty
2018-09-08
値を取得する(nullだったら例外発生)   opt.get() opt.get  
値を取得する(nullだったら初期値)   opt.or(123) opt.getOrElse(123)  
Optionを取得する(nullだったら初期値) 0.9.1 opt.orOption(new IntOption(123)) opt.orElse(Some(123)) 2018-09-08
小さい方を取得   opt.min(new IntOption(123)) opt.min とは異なる 2018-09-08
大きい方を取得   opt.max(new IntOption(123)) opt.max とは異なる 2018-09-08

Operator DSLへ戻る / AsakusaFW目次へ戻る / 技術メモへ戻る
メールの送信先:ひしだま