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

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

2016年8月26日金曜日

mysqldump

mysqldumpで日本語名のテーブルをエクスポートしようと思ったが、指定したものとは別のテーブルがエクスポートされてしまう。 コマンドプロンプトからテーブル名を指定したので文字コードが中でおかしくなっているのかもしれない。 環境はWindows8.1のコマンドプロンプトから、DBはUTF-8で作成している。 今のところ解決方法が見つからず・・・・

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年4月4日木曜日

MySQLのwait_timeout

my.iniでwait_timeoutを設定してみたがshow variablesでみるとデフォルト値と変わっていないのだが @@GLOBAL.wait_timeoutのほうは設定値が反映されている。
mysql> show variables like 'wait_timeout';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| wait_timeout  | 28800 |
+---------------+-------+
1 row in set (0.00 sec)

mysql> SELECT @@GLOBAL.wait_timeout;
+-----------------------+
| @@GLOBAL.wait_timeout |
+-----------------------+
|                   600 |
+-----------------------+
1 row in set (0.00 sec)
どっちが正しいのだろうか?謎。

2013年3月6日水曜日

PHPからMySQLへの接続ではまる

WindowsでPHPからmysqliでMySQL接続しようとしてはまった時のメモ。 以下の接続時にエラーが出た。
$mysqli = new mysqli("localhost", "user", "pass", "dbname");
エラーメッセージが文字化けして読めず何が悪いのかさっぱり不明。
Warning: mysqli::mysqli() [mysqli.mysqli]: [2002] ڑς݂̌Ăяo悪̎Ԃ߂ĂȂ (trying to connect via tcp://localhost:3306) in C:\Program Files\Apache Software Foundation\Apache2.2\htdocs\mysqli.php on line 6

Warning: mysqli::mysqli() [mysqli.mysqli]: (HY000/2002): ڑς݂̌Ăяo悪̎Ԃ߂ĂȂ߁Aڑł܂łB܂͐ڑς݂̃zXgȂ߁Amꂽڑ͎s܂B in C:\Program Files\Apache Software Foundation\Apache2.2\htdocs\mysqli.php on line 6
ネットで調べると「mysql.sockの設定がおかしいのでは」などの情報がたくさん出てくるがWindowsだとそれらしいものも無い。 とても悩んだが、どうもlocalhostがIPv6として解決されているのが原因のだったようである。 試しにIPアドレスに変えてみるとすんなり接続できた。
$mysqli = new mysqli("127.0.0.1", "user", "pass", "dbname");
確かにlocalhostにpingをかけてみるとIPv6アドレスを見に行っているようである。
C:\Temp>ping localhost

MyPC [::1]に ping を送信しています 32 バイトのデータ:
::1 からの応答: 時間 <1ms
::1 からの応答: 時間 <1ms
::1 からの応答: 時間 <1ms

こちらの情報を参考にさせていただきました。 こちらのページのようにhostsを変更して解決する方法もあるようです。

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句の後につけるんですね。初めて知りました。