ラベル SQL の投稿を表示しています。 すべての投稿を表示
ラベル SQL の投稿を表示しています。 すべての投稿を表示

2020年6月14日日曜日

MyBatisでヘッダ明細型のデータをINSERTする(自動採番もする)

MyBatisでヘッダ明細型のデータを扱うシリーズの3回目。今回はネストしたJavaオブジェクトをデータベースに新規作成する方法について書いてみたいと思う。またIDもデータベースで自動採番させようとちょっと欲張ったものにしている。 今回はSQLの都合上MySQLに特化した内容となっている。

今回のサンプルもいつもと同じ以下のクラスとテーブルを使用する。

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

public class Result {
  String course;
  int score;
}

studentテーブル(ヘッダ)

id name
100 やまだ

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90

データベースはMySQLを使用する。 idは連番を振りたいのでデータベースで自動採番させてしまおう、ということで以下のようにテーブル定義をした。

CREATE TABLE student (
  id INTEGER NOT NULL AUTO_INCREMENT UNIQUE,
  name VARCHAR(100)
);

CREATE TABLE result (
  refid INTEGER NOT NULL,
  course VARCHAR(100),
  score INTEGER
);

MyBatisのmapper定義

さて、ヘッダ明細型のテーブルに対してJavaオブジェクトをINSERTするMyBatisのmapperの定義をする。 INSERTの処理としてまず思いつくのは、MyBatisの設定でstudentテーブルとresultテーブルにINSERTするmapperをそれぞれ作成してJavaコードから順に呼び出す方法だろう。この場合はStudentクラスのオブジェクトをMyBatis経由で書き込んだ後、resultの配列ごとにループを回してResultクラスのオブジェクトをMyBatis経由で書き込む処理となる。

しかし、ネストしたStudentクラスのオブジェクトをMyBatisに渡して、その中でネストした部分も書き込んでもらうことはできないだろうかということで、以下のような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>

  <insert id="create" parameterType="student" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO student (name) VALUES (#{name});
    SET @uid = LAST_INSERT_ID();
    <if test="result != null">
      <foreach item="i" collection="result">
        INSERT INTO result (refid, course, source)
          VALUES (@uid, #{i.course}, #{i.source});
      </foreach>
    </if>
    SELECT @uid FROM dual;
  </insert>

</mapper>

まずは最初の "INSERT INTO student ~" でStudentクラスのオブジェクトをstudentテーブルに書き込む。

次の行で "SET @uid = LAST_INSERT_ID()" を行っているが、ここでstudentテーブルに書き込んで自動採番されたIDを取得している。 これはMySQL専用の構文となる。

次にMyBatisの動的SQLを使ってネストしたオブジェクトを1つづつINSERTする。
まず、<if test="result != null"> でStudentクラスのオブジェクトがネストしたResultオブジェクトを持っているか(nullでないか)を調べ、持っているなら <foreach item="i" collection="result"> でひとつづつ順にINSERT処理をしている。
resultテーブルではrefid列に紐づくstudentテーブルのidを保持するが、先にMySQLの変数 "@uid" に自動採番された値を保存しているので、その値を書き込んでいる。

自動採番された値はJavaのStudentクラスのオブジェクトにも反映したい。MyBatisの処理結果として返した値が反映されるので、最後に "SELECT @uid FROM dual" を実行して、自動採番された値を返している。

Javaコードから書き込み

以下のようなJavaコードでネストされたオブジェクトを1回のMyBatis呼び出しでヘッダ明細型のテーブルにINSERTできる。

// 英語の結果
Result result_english = new Result();
result_english.course = "英語";
result_english.score = 50;

// 英語の結果
Result result_math = new Result();
result_math.course = "数学";
result_math.score = 68;

// 生徒の情報
// IDは自動採番されるため
Student student = new Student();
student.name = "たなか";
student.result = new Result[] { result_english, result_math };

// オブジェクトをデータベースに書き込む
sqlSession.insert("mapper.student.create", student);

// 自動採番された生徒のIDを表示
System.out.println("ID=" + student.id);

おわりに:BadSqlGrammarExceptionが出た時

Springから使っていたらこんな例外が出てしまいました。

org.springframework.jdbc.BadSqlGrammarException: 
### Error updating database.  Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'SET @uid = LAST_INSERT_ID();
		 
		SELECT @uid FROM dual' at line 3

今回はMyBatisのmapperで複数のステートメントを一度に実行していますが、MySQLのJDBCドライバの仕様で複数ステートメントを実行するときは "allowMultiQueries=true" オプションを指定する必要があるようです。JDBC接続のURLに以下のようにオプションを追加したらうまく動くようになりました。


jdbc.url=jdbc:mysql://localhost/student?allowMultiQueries=true

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);
}

2020年4月29日水曜日

MyBatisでヘッダ明細型のデータを扱う

「ヘッダ明細型」のデータはアプリを作るうえでよく登場するデータ構造である。 「ヘッダ」が親データ、「明細」が親に紐づく子データを表しており、例えば成績のデータなどで、各生徒の英語と数学の成績を表す場合に生徒の情報を親としてヘッダテーブルに格納、成績情報は親にidで紐づけて明細テーブルに格納すると以下のようにあらわすことができる。

studentテーブル(ヘッダ)

id name
100 やまだ
101 たなか

resultテーブル(明細)

refid course score
100 英語 80
100 数学 90
101 英語 50
101 数学 68

やまだ君の成績は英語80点で数学90点。たなか君の成績は英語50点で数学68点を表している・・・たなか君はもう少し頑張ったほうがいいですね・・・

このようなデータの持ち方は「Summary」と「Detail」などとも呼ばれたり、呼び方はさまざまであるが、商品カートや伝票など業務の世界でも頻繁に登場する。

このようなデータを扱う場合はStudentクラスがResultクラスのオブジェクトを持つようなネストした構造で表現することができる。 例えばJavaの場合のクラス定義は以下のようになる。setter/getterメソッドは省略して書いているので適宜付与してもらうとよいだろう。

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

public class Result {
  String course;
  int score;
}

ここでMyBatisを使ってstudentテーブルとresultテーブルの内容を上記クラスに読み込むことを考えてみる。studentテーブルをSELECTしてからresultテーブルをSELECTするように2つのSELECTに分けてもよいが、MyBatisではテーブルを結合した1つのSQLでネストしたクラスを読み込むことができる。

mapperでcollectionタグを使いネストした部分を定義する。property属性に子オブジェクトを格納するプロパティ名、oftype属性にそのクラス名を指定する。 SELECTのSQLでは2つのテーブルを結合する。

<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>

SELECTの結果は以下のようにstudentの内容とresultの内容が1行に結合されたものとなるが、親クラスの内容はmapperのidタグで指定された列でまとめられオブジェクトが構築されるようである。

SELECTの結果

id name course score
100 やまだ 英語 80
100 やまだ 数学 90
101 たなか 英語 50
101 たなか 数学 68

以下のコードで読み込みを行うと・・・

List<Student> entities = sqlSession.selectList("mapper.student.find");

下図のようなオブジェクトとして読み込まれる。



使いこなせば複雑なデータ構造をシンプルに扱えそうな方法であるが、実際使ってみると気を付けなければならない点もあり、そのような点は次回以降また紹介していこうと思う。

2013年5月2日木曜日

MySQL 行の作成日と更新日を自動でつける その2

「MySQL 行の作成日と更新日を自動でつける」の記事で、 行の作成日と更新日を自動でつけるDDL(テーブル定義)の方法を紹介した。 この方法では自動でつけられるのは作成日と更新日どちらか1テーブルに1つの制約があった。

その後の調査で分かったのだが、MySQLではNOT NULLなTIMESTAMP型の列にNULLを書き込むと現在時刻に置き換えられるという仕様がありそれを利用することで、一応自動で付与するということもできるようだ。

以下のようなテーブル定義として、ts1に行の作成日時、ts2に行の更新日時を入れることとする。

CREATE TABLE t3 (
  rid INTEGER,
  txt VARCHAR(20),
  ts1 TIMESTAMP DEFAULT 0,
  ts2 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

INSERT時にts1にはNULL値を書き込むことで書き込みの発生した現在時刻が自動で入る。

mysql> INSERT INTO t3 (rid,txt,ts1) VALUES(1,'aaa',NULL);
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO t3 (rid,txt,ts1) VALUES(2,'bbb',NULL);
Query OK, 1 row affected (0.09 sec)

mysql> SELECT * FROM t3;
+------+------+---------------------+---------------------+
| rid  | txt  | ts1                 | ts2                 |
+------+------+---------------------+---------------------+
|    1 | aaa  | 2013-04-23 16:21:44 | 2013-04-23 16:21:44 |
|    2 | bbb  | 2013-04-23 16:21:44 | 2013-04-23 16:21:44 |
+------+------+---------------------+---------------------+
2 rows in set (0.00 sec)

UPDATE時にはts1には値を書き込まないようにする。このようにすることでts1の日時は更新されず、行が作成された日時を示すようになる。

mysql> UPDATE t3 SET txt='zzz' WHERE rid=1;
Query OK, 1 row affected (0.10 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM t3;
+------+------+---------------------+---------------------+
| rid  | txt  | ts1                 | ts2                 |
+------+------+---------------------+---------------------+
|    1 | zzz  | 2013-04-23 16:21:44 | 2013-04-23 16:21:49 |
|    2 | bbb  | 2013-04-23 16:21:44 | 2013-04-23 16:21:44 |
+------+------+---------------------+---------------------+
2 rows in set (0.00 sec)

まあ、でもINSERT時にNULLを指定して書き込まないといけないので、ここに現在時刻を指定するようにしてもプログラムの質としてはあまり変わらないような気がする。 むしろその方がトリッキーでなくわかりやすいかも。

2013年4月23日火曜日

MySQL 行の作成日と更新日を自動でつける

MySQLで日付型の列に行の作成日と更新日を自動で入れる方法を調査した。 以前使った、SQL Anywhereの場合だと「行の作成日と更新日を自動でつける」で書いたように行の作成日時、更新日時を列のデフォルト値とする構文があった。

MySQLでも同様のデフォルト値CURRENT_TIMESTAMPはあるようだがTimestamp型にのみ指定できるようである。 記法は以下2通りある。

DEFAULT CURRENT_TIMESTAMP
行が挿入された時刻
DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
行が挿入または更新された時刻

行が挿入された時刻をデフォルト値とする

DEFAULT CURRENT_TIMESTAMPを使う。

CREATE TABLE t1 (
  rid INTEGER,
  txt VARCHAR(20),
  ts1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
このようなテーブルに対して以下の行を挿入すると挿入時刻が自動で入る。
mysql> INSERT INTO t1 (rid,txt) VALUES(1,'aaa');
Query OK, 1 row affected (0.08 sec)

mysql> INSERT INTO t1 (rid,txt) VALUES(2,'bbb');
Query OK, 1 row affected (0.09 sec)

mysql> SELECT * FROM t1;
+------+------+---------------------+
| rid  | txt  | ts1                 |
+------+------+---------------------+
|    1 | aaa  | 2013-04-23 13:26:52 |
|    2 | bbb  | 2013-04-23 13:26:58 |
+------+------+---------------------+
2 rows in set (0.00 sec)

行の更新では変化しない。

mysql> UPDATE t1 SET txt='zzz' WHERE rid=1;
Query OK, 1 row affected (0.14 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM t1;
+------+------+---------------------+
| rid  | txt  | ts1                 |
+------+------+---------------------+
|    1 | zzz  | 2013-04-23 13:26:52 |
|    2 | bbb  | 2013-04-23 13:26:58 |
+------+------+---------------------+
2 rows in set (0.00 sec)

行が挿入または更新された時刻をデフォルト値とする

DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPを使う。
CREATE TABLE t2 (
  rid INTEGER,
  txt VARCHAR(20),
  ts1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
このようなテーブルに対して以下の行を挿入すると挿入時刻が自動で入る。
mysql>> INSERT INTO t2 (rid,txt) VALUES(1,'aaa');
Query OK, 1 row affected (0.11 sec)

mysql>> INSERT INTO t2 (rid,txt) VALUES(2,'bbb');
Query OK, 1 row affected (0.12 sec)

mysql>> SELECT * FROM t2;
+------+------+---------------------+
| rid  | txt  | ts1                 |
+------+------+---------------------+
|    1 | aaa  | 2013-04-23 13:27:09 |
|    2 | bbb  | 2013-04-23 13:27:12 |
+------+------+---------------------+
2 rows in set (0.00 sec)

行を更新すると自動で書き換わる。

mysql> UPDATE t2 SET txt='zzz' WHERE rid=1;
Query OK, 1 row affected (0.08 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> SELECT * FROM t2;
+------+------+---------------------+
| rid  | txt  | ts1                 |
+------+------+---------------------+
|    1 | zzz  | 2013-04-23 13:28:00 |
|    2 | bbb  | 2013-04-23 13:27:12 |
+------+------+---------------------+
2 rows in set (0.00 sec)

1つのテーブルにDEFAULT CURRENT_TIMESTAMPDEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPを両方定義しようとすると"Incorrect table definition"のエラーになってしまう。 両方同時に使えると作成時刻と更新時刻の両方が管理でき便利なのだが残念。 ただし、DDLとINSERTのSQLを少し工夫することで同等のことができるようだ。 その方法は「その2」のほうに書いてみた。

CREATE TABLE t3 (
  rid INTEGER,
  txt VARCHAR(20),
  ts1 TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  ts2 TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

詳しくはリファレンスに書いてある。

2013年1月7日月曜日

SQL Anywhere 連番を生成する

一行のSQLで処理を書きたいときなど連番テーブルがあると簡単に書けることがあるが、 SQL Anywhereの場合rowgeneratorテーブルを使って1~255までの連番を生成することができる。 rowgeneratorテーブルにはrow_num列が一つだけありその列に連番が入っている。
SELECT row_num
FROM rowgenerator
WHERE row_num <=10
ORDER BY row_num
実行すると以下のような連番が返ってくる。
row_num
-------
1
2
3
4
5
6
7
8
9
10

(10 ロー)
試していないのだが、255よりたくさん連番がほしい場合は入れ子のSELECTにするなど複数回組み合わせればできると思う。

2013年1月4日金曜日

MySQL 先頭の指定件数だけselectする

SQLで先頭のN件のみselectしたい場合、SQL AnywhereとSQL ServerはTOP句を使って指定する。
たとえば3件だけとってきたい場合は以下のように記述する。
SELECT TOP 3 * FROM mytable ORDER BY mycol1;


MySQLで同じことをする場合はLIMIT句を使うようである。
SELECT * FROM mytable ORDER BY mycol1 LIMIT 0,3;

ORDER BY句の後につけるんですね。初めて知りました。

2012年1月11日水曜日

Java JDBCのエラー ローにありません

SQL Anywhere 12 にJavaからiAnywhere JDBC ドライバ(ianywhere.ml.jdbcodbc.jdbc3.IDriver)で接続しSELECT文を実行、結果を読むところで「ローにありません」というエラーが出た。


Caused by: java.sql.SQLException: ローにありません。
at ianywhere.ml.jdbcodbc.jdbc3.IIResultSet.getInt(Native Method)
at ianywhere.ml.jdbcodbc.jdbc3.IResultSet.getInt(IResultSet.java:464)
... 2 more


まったく意味がわからないメッセージなので、どうしたものかと思い該当部分のコードを見る。


ResultSet rs = statement.executeQuery();
int n = rs.getInt(1); // ← ここでSQLException
rs.close();


結果を一行しか返さないSQL文だったのですっかり油断していた。

原因はResultSet.next()が抜けていただけでした。

正しくは…


ResultSet rs = statement.executeQuery();
int n;
if (rs.next()) {
n = rs.getInt(1);
}
rs.close();


これに気づく間、SQLを変えてみたり試行錯誤してました。
適切なエラーメッセージはとても大事です。

2011年11月3日木曜日

SQL Anywhere 行の挿入と更新を1ステートメントで

行の挿入と更新を1ステートメントでできるとトランザクションを考える必要がなく便利な場合が多い。
SQL Anywhereの場合はINSERT INTO ~ ON EXISTING UPDATE という句を使うとプライマリーキーを比較して挿入と更新を自動で切り替えて実施してくれる。


例として、前記事と同じこのようなテーブルに対して

CREATE TABLE mytable (
pkey varchar(10) NOT NULL,
value varchar(20),
created datetime NOT NULL DEFAULT current timestamp,
modified datetime NOT NULL DEFAULT timestamp,
PRIMARY KEY (pkey)
)


1回目の実行結果

INSERT INTO mytable (pkey,value)
ON EXISTING UPDATE
VALUES('1', 'abc');

INSERTとして実行されます。



pkeyvaluecreatedmodified
1abc2011-11-02 23:44:39.1792011-11-02 23:44:39.179


2回目の実行結果

INSERT INTO mytable (pkey,value)
ON EXISTING UPDATE
VALUES('1', 'xyz');

プライマリーキーであるpkey=1に対するUPDATEとして実行されます。



pkeyvaluecreatedmodified
1xyz2011-11-02 23:44:39.1792011-11-03 00:06:21.069



他のRDBMSではMERGEやREPLACEというのが似たような動きをしてくれるようである。

2011年11月2日水曜日

SQL Anywhere 行の作成日と更新日を自動でつける

CREATE TABLE文のカラムにDEFAULT句をつけると、そのカラムの値が省略された場合のデフォルト値を自動で入れることができる。SQL Anywhereの場合はこのDEFAULT句を使って行の作成日や更新日を自動でつけることができる。


current timestamp

行が挿入された時間

timestamp

行が挿入または更新された時間



例としてこのような定義のテーブルがあった場合・・・

CREATE TABLE mytable (
pkey varchar(10) NOT NULL,
value varchar(20),
created datetime NOT NULL DEFAULT current timestamp,
modified datetime NOT NULL DEFAULT timestamp,
PRIMARY KEY (pkey)
)

行の追加結果

INSERT INTO mytable (pkey,value) VALUES('1', 'abc');




pkeyvaluecreatedmodified
1abc2011-11-02 23:44:39.1792011-11-02 23:44:39.179

行の更新結果

UPDATE mytable SET value='xyz' WHERE pkey='1';




pkeyvaluecreatedmodified
1xyz2011-11-02 23:44:39.1792011-11-02 23:45:32.928

こんな感じで、modifiedのほうはUPDATE実行時間に変わりますが、createdのほうはINSERT実行時間のまま変わりません。

2011年10月25日火曜日

SQL Anywhere ランダムな結果を取得

SQL Anywhereで複数の行からランダムに1件選びたい場合はtop句とrand関数を使う。

たとえばテーブルmytableのランダムな1行を返す場合は次のようなselect文を書けばいい。

SELECT TOP 1 * FROM mytable
ORDER BY RAND()


rand関数はSQL2003 Vendor extension。他のDBでは使えるものとダメなものがある。

MySQLとSQLServerはrand関数だが、PostgreSQL はrandom関数。
Oracleはまた違うようである。(dbms_random.random関数?)
SQL Anywhere以外は未検証です。

2011年1月31日月曜日

Java JDBC-ODBC 文字列またはバッファの長さが無効です

JavaアプリからJDBC-ODBCブリッジでデータベースSQL Anywhereに接続していると、不定期に「文字列またはバッファの長さが無効です」というエラーが出る現象に遭遇した。

DriverManager.getConnection()で出てみたり・・・

java.sql.SQLException: [Microsoft][ODBC Driver Manager] 文字列またはバッファの長さが無効です。
at sun.jdbc.odbc.JdbcOdbc.createSQLException(JdbcOdbc.java:6957)
at sun.jdbc.odbc.JdbcOdbc.standardError(JdbcOdbc.java:7114)
at sun.jdbc.odbc.JdbcOdbc.SQLGetDataString(JdbcOdbc.java:3907)
at sun.jdbc.odbc.JdbcOdbcResultSet.getDataString(JdbcOdbcResultSet.java:5698)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:354)
at sun.jdbc.odbc.JdbcOdbcConnection.buildTypeInfo(JdbcOdbcConnection.java:1503)
at sun.jdbc.odbc.JdbcOdbcConnection.initialize(JdbcOdbcConnection.java:381)
at sun.jdbc.odbc.JdbcOdbcDriver.connect(JdbcOdbcDriver.java:174)
at java.sql.DriverManager.getConnection(DriverManager.java:582)
at java.sql.DriverManager.getConnection(DriverManager.java:185)


ResultSet.getString()で出てみたりと発生箇所もさまざま。

java.sql.SQLException: [Microsoft][ODBC Driver Manager] 文字列またはバッファの長さが無効です。
at sun.jdbc.odbc.JdbcOdbc.createSQLException(JdbcOdbc.java:6957)
at sun.jdbc.odbc.JdbcOdbc.standardError(JdbcOdbc.java:7114)
at sun.jdbc.odbc.JdbcOdbc.SQLGetDataString(JdbcOdbc.java:3907)
at sun.jdbc.odbc.JdbcOdbcResultSet.getDataString(JdbcOdbcResultSet.java:5698)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:354)
at sun.jdbc.odbc.JdbcOdbcResultSet.getString(JdbcOdbcResultSet.java:411)


調べてみるとある特定の環境だけで頻発するようだ。

英語のエラーメッセージ "Invalid string or buffer length" で検索すると、SQL ServerOracleでも発生している様子。これらは不定期でなくコードの決まった場所で必ず発生するとのことだが、共通するのはx64版のJavaVMを使っていると言うことである。

今エラーが起きているのもx64 JavaVMなので何か関係がありそうである。

JDBC-ODBCをやめてType4ドライバを使うようにしたほうがよさそうだ。

2011年1月13日木曜日

SQL Anywhere 時間差を求める

SQL Anywhereで2つの時刻の差が何秒あるかを求めるにはdatediff関数を使う。

この関数の書式は以下のようになっており、date-partには求めたい差の単位day,hour,secondなど、date-expression-1,2には差を求めたい2つの時刻を入れる。date-expression-1のほうに古いほうの時刻があると関数の結果は正となり、新しいと負になる。


DATEDIFF( date-part, date-expression-1, date-expression-2 )


たとえば、以下のようなスキーマのテーブルがあり、upd_timeに各行の更新時刻が入っているような場合、

CREATE TABLE mytable (
r_id integer NOT NULL
,r_data varchar(100) NOT NULL
,upd_time timestamp NOT NULL
);


このテーブルから、1時間以内に更新された行を抽出するSQLは次のように書ける。
条件句にDATEDIFF関数を使って現在時刻NOW()関数とupd_time列の差を秒単位で求め、3600秒以内である行のみを抽出する。

SELECT r_id,r_data,upd_time
FROM mytable
WHERE DATEDIFF(second,upd_time,NOW()) <= 3600
ORDER BY start_time


この関数はSQL2003標準であるため、サポートしている別のDB、たとえばSQL Server 2008などでも同じように使えるようだ。

2010年12月29日水曜日

SQL Anywhere SELECT結果でのUPDATE

あるテーブルのSELECT結果を使って別テーブルのUPDATEをする。

このようなことをしたい場合SQL Anywhereでは UPDATE~FROM~WHERE 構文を使うことで実現できる。この構文ではFROM句で取得したあるテーブルのデータをWHERE句で結合して更新をかける。

例として、TableAとTableBがあったときにTableAからcol1,col2列の値を読み込み、その値で同じpkeyを持つTableBの行を書き換えるには以下のようなSQLを使う。


UPDATE TableB SET col1=A.col1, col2=A.col2
FROM (
select pkey, col1, col2
from TableA
) A
WHERE pkey = A.pkey


他のDBでは同じことを複数の列をSETに書いて UPDATE table SET(col1,col2...)=(SELECT col1,col2...) のようにできるものもあるようだが、SQL Anywhereではこの構文は使えないようである。

2010年12月28日火曜日

SQL Anywhere PHP Module

SQL Anywhere を PHP でつかうための SQL Anywhere PHP Module。

SQL Anywhere バージョン10.0.1以降で使える。

ソースもあるのでサポート対象外のPHPバージョンについてもビルドをがんばれば何とかなるかもしれない。

ダウンロード

2010年6月21日月曜日

SQL Anywhere 自動採番したIDを取得する

データベースSQL Anywhereにはテーブル列のデフォルト値にAUTOINCREMENTを指定することで行をinsertしたときに自動採番した値が入るようになる。

たとえば以下のようなテーブルを作っておいて、

CREATE TABLE mytbl (
rid INTEGER NOT NULL DEFAULT AUTOINCREMENT CONSTRAINT PRIMARY KEY,
val VARCHAR(20) NOT NULL
)


ridは指定しないで行を書き込む。

INSERT INTO mytbl (val) VALUES('aaaaa');
INSERT INTO mytbl (val) VALUES('bbbbb');
INSERT INTO mytbl (val) VALUES('ccccc');


この結果は以下の3行となる。

  • rid=1, val='aaaaa'

  • rid=2, val='bbbbb'

  • rid=3, val='ccccc'




さて、ここで採番された値を知る方法であるが、グローバル変数@@identityに最後に割り振られた値が格納されるので以下のSQLを実行することで値を取得できる。

SELECT @@identity;


例の場合は結果は3になる。