2020年5月26日火曜日

JavaのSimpleDateFormatで日付文字列チェックではまる

java.text.SimpleDateFormatを使って日付文字列をチェックしようとしてはまったときのメモ。 parseメソッドに文字列を与えてParseExceptionが出るか出ないかでチェックしようとした。

SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");

// 不正な日付
Date d = fmt.parse("197405112"); // ParseExceptionが発生しない!!

しかし、エラーとはならず、yyyy=1970、MM=05、dd=112と解釈され、5月112日→8月20日として、d=1974/8/20として通ってしまい予想外の動きになってしまった。 SimpleDateFormatはデフォルトだとできるだけ日付として解釈するように動作をするらしい。 日付を3桁で拾ってしまうのは予想外だったが・・・

存在しない日付を厳密にチェックするときはsetLenientをfalseに設定する必要があるようだ。 以下にすることでチェックをすることができた。

SimpleDateFormat fmt = new SimpleDateFormat("yyyyMMdd");

// 厳密な日付チェックを行う設定
fmt.setLenient(false);

// 不正な日付
Date d = fmt.parse("197405112"); // ParseException発生!!

SimpleDateFormatはスレッドセーフではなく使いまわしが難しいので、そろそろ新しいスレッドセーフなDateTimeFormatterに移行するべきとは思うが、Dateを使っている手持ちライブラリも多く使い勝手の悪さからなかなか移行できていない・・・

2020年5月15日金曜日

MySQLでUnknown command '\''.がでる。

MySQL5.7でSQLを流した時にUnknown command '\''.がでる時の対処法。

どういう状況で出るのかはいまいちはっきりしていないが、UTF-8のファイルを流した時に出る? 5.6などでは同じSQLでも出ていなかったように思われる。 このようなときは--default-character-set=utf8オプションを指定してやると成功するようだ。

mysql -umydb -pmypass -Dmydb --default-character-set=utf8 < data.sql

2020年5月12日火曜日

MyBatisでヘッダ明細型のデータを扱う(行範囲を指定)

前回の記事でヘッダ明細型のデータをMyBatisで読み込む方法としてcollectionタグを使いネストしたJavaオブジェクトに読み込む方法を紹介した。ヘッダテーブルと明細テーブルを結合した1つのSQLで読み込みができるため楽な方法ではあるが使い方に癖があるようなので注意しないと思わぬところではまってしまう。その一つが行数を指定して読み込むときである。

例として以下のようなデータをcollectionタグを使いネストしたJavaオブジェクトで読み込んでみよう。

studentテーブル(ヘッダ)

id name
100 やまだ
101 たなか
102 おばた
103 ささおか

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90
101 英語 50
101 数学 68
102 英語 77
102 数学 70
103 英語 98
103 数学 92

読み込むオブジェクトのJavaクラスの定義とMyBasisのmapperは前回の記事と同じものを使用する。

Javaクラス定義

public class Student {
  int id;
  String name;
  Result[] result;
}

public class Result {
  String course;
  int score;
}

MyBasisのmapper定義

<mapper namespace="mapper.student">
  <resultmap id="student" type="student">
    <id column="id" property="id" />
    <result column="name" property="name" />

    <collection property="result" oftype="Result">
      <result column="course" property="course" />
      <result column="score" property="score" />
    </collection>
  </resultmap>

  <select id="find" resultmap="student">
    SELECT id, name, course, score
    FROM student, result
    WHERE id = refid
    ORDER BY id
  </select>
</mapper>

IDの小さい順に2人、やまだ君(100)、たなか君(101)、のデータだけ読み込むことを考えてみる。MyBatisではRowBoundsを使うことで指定した行範囲のデータを読み込むことができるので、以下のコードで0行目から2行の読み込みをしようとした。

// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;
RowBounds rowBounds = new RowBounds(offset, limit);

// SQLに渡すパラメータ(特に無し)
Map<String,Object> params = new HashMap();

// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find", params, rowBounds);

しかしcollectionを使っている場合はそんなに単純ではない。RowBoundsへの期待はstudentテーブルの先頭2行だけであるが、SELECTの結果は以下のようにstudentの内容とresultの内容が1行に結合されたものとなり、RowBoundsはその先頭2行を対象としてしまうようである。結果的にstudentsには、やまだ君のデータだけが読み込まれる。

SELECTの結果

id name course score RowBoundで選ばれる行
100 やまだ 英語 80
100 やまだ 数学 90
101 たなか 英語 50 ×
101 たなか 数学 68 ×
102 おばた 英語 77 ×
102 おばた 数学 70 ×
103 ささおか 英語 98 ×
103 ささおか 数学 92 ×

これを回避するにはRowBoundsを使わず、全行読み込んだ後にoffsetとlimitでリストを切り取る処理を行っている。 一旦全行読み込んで処理をするのでメモリ的にもCPU的にも負荷がかかるのが欠点ではあるが、それほどの規模でなければコードの読みやすさでこの方法を使っている。 ただ、行数が多いテーブルではSQLの書き方も工夫するなど何らかの対策が必要がと思われる。

// 読み込む行範囲を指定、先頭から2行のデータを読み込む
int offset = 0;
int limit = 2;

// SQLを発行してオブジェクトにマッピングする
List<Student> students = sqlSession.selectList("mapper.student.find");

// 行範囲で切り取り
int s = offset;
int e = offset+limit;
if (s < 0) { s = 0; }
if (e >= students.size()) { e = students.size(); }
if (s == 0 && e >= students.size()) {
	return students;
} else {
	return students.subList(s, e);
}