Asakusa FrameworkのOperator DSLで使うOption系クラスについて。
|
|
|
ユーザー演算子をプログラマーが実装する際は、DMDLから生成されたモデルクラスのgetter/setterメソッドを使って値の移送や演算を行う。
これらのメソッドは、int・String等の“通常のJavaのプリミティブ型およびクラス”を扱うメソッドの他に、Option系クラス(AsakusaFW独自のクラス)で扱うメソッドが用意されている。
Option系クラスはint・String等を内包している感じのクラスで、モデルクラスのフィールドはOption系クラスを保持するようになっている。
また、Option系クラスはHadoopのWritableとの変換機能を持っている。
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]
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のOptionalやScalaのOptionは不変オブジェクトだが、AsakusaFWのOption系クラスは可変オブジェクトである。しかしなるべく変更させない(不変オブジェクトの様に扱わせたい)為に、setという名前を使わずmodifyという変わったメソッド名にし、またmodifyやcopyFromは非推奨にしているのだと思う)
※get()は値がnullだとNullPointerExceptionが発生するので注意。
→値を比較するために取得するなら、getメソッドでなくhasメソッドの方が便利かも。[2016-02-11]
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の思想としては、
という事なのだと思う。
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系クラスでは注意が必要。[2014-12-21]
StringOption | IntOption | 備考 | ||
---|---|---|---|---|
Optionで保存 | × | StringOption temp = model.getText1Option(); |
IntOption temp = model.getValue1Option(); |
退避した直後に値を変更しているので、 1も2も同じ値になってしまう。 |
○ | StringOption temp = model.getText1Option(); |
IntOption temp = model.getValue1Option(); |
値をコピーしてから変更している。 | |
○ | StringOption temp = new StringOption(); |
IntOption temp = new IntOption(); |
退避用のOptionにコピーしている。 | |
Textで保存 | × | Text temp = model.getText1(); |
退避した直後に値を変更しているので、 1も2も同じ値になってしまう。 |
|
○ | Text temp = model.getText1(); |
値をコピーしてから変更している。 | ||
String/intで保存 | ○ | String temp = model.getText1AsString(); |
int temp = model.getValue1(); |
OptionやTextを取得すると、モデルオブジェクト内で使っているインスタンスがそのまま返ってくる。
AsakusaFWのOption系クラスやTextは可変オブジェクトなので、モデルオブジェクトの値を変更すると、取得したOptionやTextも同じく変わってしまう。
Stringやint等のOption内部の値(不変オブジェクト)で取得すれば、(インスタンスは別になるので)モデルオブジェクトの値を変更しても影響を受けない。
プロパティーへの値のセット(StringOption同士やIntOption同士の値の受け渡し)は値のコピーなので(インスタンスの共有ではないので)、影響を受け合うことは無い。
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は日時(年月日時分秒)を保持する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 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() + 日数; |
long seconds = dt.getElapsedSeconds() + 日数 *
86400; |
||
文字列からの変換 [/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", '/', ' ', ':')); |
0.7.0 | |
java.util.Dateからの変換 | java.util.Date date = 〜; |
java.util.Date date = 〜; |
||
java.util.Dateへの変換 | java.util.TimeZone tz =
java.util.TimeZone.getTimeZone("UTC"); |
java.util.TimeZone tz =
java.util.TimeZone.getTimeZone("UTC"); |
||
java.time.LocalDateとの変換 [2021-12-21] |
LocalDate ldate = 〜; |
|||
Date d = 〜; |
||||
java.time.LocalDateTimeとの変換 [2021-12-21] |
LocalDateTime ldt = 〜; |
|||
DateTime dt = 〜; |
||||
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]
一方、SimpleDateFormatやCalendarはタイムゾーンを保持しているので、変換の際には考慮が必要。
AsakusaFWのDateUtilのgetFromCalendar系メソッドでは、Calendarのgetメソッドを呼んで値を取得しているので、Calendarのタイムゾーンが考慮された値が使われることになる。
setToCalendar系メソッドでもCalendarのsetメソッドを呼んで値を設定しているので、Calendarのタイムゾーンの値としてセットされることになる。
DateUtilでjava.util.Dateを引数にとるgetFromDate系メソッドでは内部でCalendarインスタンスが使われるが、このCalendarはデフォルトタイムゾーンとなるので、注意が必要。
AsakusaFWのOption系クラスは、ScalaのOptionクラスと似ている部分がある。
AsakusaFWの IntOption |
Scalaの Option[Int] |
更新日 | ||
---|---|---|---|---|
nullチェック | opt.isNull() |
opt.isEmpty |
||
非nullチェック | 0.9.1 | opt.isPresent() |
opt.isDefined |
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 |