Continuous Ops

Exploring the world of 'Infrastructure as Code'

第6回テックヒルズ「Let’s Study Jenkins」- Jenkinsとのさまざまな付き合い方

2013/06/18 19:00~22:00に六本木ヒルズ(アカデミーヒルズ)で「第6回テックヒルズ Let’s Study Jenkins ~さまざまなケーススタディ~」というイベントが行われたので行ってきました。

第6回テックヒルズ本日開催!【Ustream配信決定】
Let’s study Jenkins ~さまざまなケーススタディ~ : ATND
http://atnd.org/events/39910

参加者700人超という平日の技術勉強会としてはかなりの規模で、この形式でやるのは6回目らしいのですが私は今回が初参加でした。

セッションは2つの部屋に分かれて行われました。私が参加したセッションについて紹介します。

検索技術基盤開発のための結合テスト環境の自動化(楽天:萩原さん)

検索基盤の開発にJenkinsを活用して、結合テストを自動化している話。単体テスト・結合テストの前にSmoke Testというフェーズがあり、早期発見可能なバグを検知する仕組みがあるというのが特徴的でした。Smoke Testの中身についてもう少し詳しく聞きたかった。。

結合テストについては15時間のビルドを分割して全て1時間以内に終わるようにして50台のテストサーバーで自動テストを回しているとのこと。それなりに大規模にJenkinsを活用している事例といえますが、ここまで緻密に自動テスト環境を構築するのは楽天内の検索システムのような大規模開発ならではという印象を持ちました。

CROOZにおけるJenkins活用事例(CROOZ: 鈴木さん)

コーディング規約に反したコードや除外ファイル(本番にデプロイしたくないファイル)の更新漏れを検知するのに活用しているののこと。自動テストに使うというより、その前段階で開発者へのフィードバックループを作るために活用しているという感じのようです。

そもそもソーシャルゲーム開発におけるテスト事情って最近どうなんでしょうか? 私が2年前にソーシャルゲーム開発をしているころは単体・結合テストを書いているという会社はあまり見かけなかったです。最近だと独自フレームワークが完成の域に達している会社も多いでしょうし、テストをするのは珍しくなくなっているのかもしれません。

ぼくとJenkinsおじさんとの300日戦争(mixi: 五嶋さん)

テスト数25万件、実行に45分かかっていたという、膨大なテストを高速化(時間短縮)するための奮闘の記録。

「モジュール読み込み時間を削る」「WriteするまでFixtureで生成したDBをキャッシュして使いまわす」という方法が最初に紹介されましたが、十分な効果が得られず苦労していたという話の後で、結局、最近Jenkinsスレーブ48台が使えるようになって8分でテスト終わるようになりました!という富豪的解決策で対応というオチになって、苦笑してしまいました。確かにリソースは大事ですね。

アメーバピグとJenkinsと私 (サイバーエージェント: 丸山さん)

2011年当時のアメーバピグはテストがまともになく、ci環境がないのでとりあえずJenkins導入してみたところ、数千個のFindbugs警告、160個は深刻、でパンドラの箱を開けてしまった\(^o^)/というところから話が始まりました。そういう状況から2年間かけて徐々にci環境の整備を行なってきたとのこと。

後半では、Jenkinsを使ってバッチ処理をしているというのを興味深く聞きました。cronやSpringBatchではジョブの成否や実行時間の把握が複雑になりがちで、私もバッチ管理の有効な方法を探している途中だったからです。SpringBatchを捨てて自作のシンプルなフレームワークへの置き換え+Jenkinsでのバッチ管理というのは一つの手法として合理的だと納得しました。

あと今回初めて知りましたが、アメーバピグは本番環境が2系統あって一週間ごとに切り替えて運用しているとのことです。

AWS Summit Tokyo 2013(2) FreakOut - 50ms or Dieの世界

AWS Summit Tokyo 2013のセッションでもう一つ、メモを取りながら聴講したのはFreakOut社のDSPシステム構築のセッションです。

私も仕事でほぼ同じようなシステムを作っているので、私にとってはFreakOut社は(ド競合ではないまでも)同業者にあたります。そういう意味で最も注目していたセッションでした。

クラウドで RTB ~50ms or die. を支えるFreakOut の AWS 活用~

彼らがよく使っている「50ms or die」という言葉はとてもキャッチーだと思っていて、好きです。

これはRTBでbid requestに対して50ミリ秒以内にレスポンスを返し続けなければいけないというルールを端的に言い表したフレーズです。

実際にはこれにネットワークレイテンシの考慮を加えて合計で100ミリ秒が目安であったり、SSP毎にレギュレーションが違ったりと、50ミリ秒という数字が全てではないのですが、レスポンスタイムのしきい値という意味ではとても現実的な数字だと思います。

実際には20~30ミリ秒くらいでレスポンスを返すシステムを想定して作っていくべきだと思っていて、私も実際そのへんをターゲットに置いています。

クラウドでRTB

FreakOUt

2010/10創業

4billion+imp
60+staff

## FreakOutとRTB

FreakOutは広告主のネット広告におけるROI最適化を目的としたDSPを展開

このページのオーディエンスはどんな人
興味関心
生活主観
過去の行動

RTBによって広告主の都合で買い付け可能に

960億/月のRTBreqを受けている 

## FreakoutとAWS

100msecで広告を表示する
100msecでSSPがリクエストを投げ、DSPがレスポンスを返しきるまで
TCPのRTTを含んでいる
SSPは複数あり、多くの同時多発的リクエストが発生する

### 課題

レイテンシの軽減
多数のRTB処理
落札後の画像&Flash配信

RTBはCPUバウンド

自作PCを使っている 数百台規模で使っている 
多コアを安く並べる

その一方でコンテンツ配信には広い帯域が必要

S3+CloudFrontを使用
コストパフォーマンスが良い

オンプレミス+クラウドで費用対効果を追求
すべてをクラウドに任せるのではなく、徹底して費用対効果を勘案したうえで技術選択を行っている

## 海外での展開とAWS

レイテンシ
singapore 80msec
us-west 110msec
us-east 160msec

海外でも物理的に近い必要がある

VPCとVPC connsを使って拡張

EC2 専用のAMIを作成して活用
ELB LVSの代わりとして活用
Elasticache 当時VPCで使用不能だった
RDS Slaveでバッチを稼働させるため不使用 
ELB以外はEC2で構築

構築初期 
c1.xlarge
app
m2.2xlarge
kvs
m1.medium
other

ログ処理関連
S3 海外でputして日本でget
オンプレミスのhadoopへ

まとめ
自分たちの都合で必要なことを判断して使う
AWSはいい意味でドライなので線引きをエンジニア自身で考える
AWS以上に都合のよいクラウドはいまのところない

## 改善点・課題

CloudFront Reserved Capacity
10tboverなら配信料ダウン
Region単位契約なので注意

HTML/JS
cookieをさわりたいので、ドメイン指定する必要がある
カスタムオリジンをhttps対応してくれると助かる

cc2.8xlargeによるインスタンス集約
c1.xlarge x 4以上

Elasticacheを今後使っていきたい

EC2はインスタンスを増やすのが簡単だが増やしすぎると運用負担増
1台あたりのパフォーマンス改善が重要 

## おわりに

* FreakOutは必要な技術を都度精査して選択する
* 結果責任は技術選択をする事業者自身にある
* AWSは有力な選択肢として魅力をもっていると考えており、今この瞬間もAWSを利用して広告を配信している

あまり新しい情報は無かったのですが、cc2.8xlargeインスタンスの使用を検討しているというのは興味深かったです。価格的にはc1.xlargeの4倍程度なのですが、じっさいには4倍以上のCPU能力はありそうだし、ネットワークは1Gbpsから10Gbpsになるのでネットワークも安定しそうだしということで、効果がありそうだからです。実際検証してみたいと思います。

クラウドでは安価で冗長性の高いインスタンスを並列させてスケールアウトしていくというのがこれまでは半ばポリシーでしたが、最近はそうでもないと思い始めています。

作るシステムにもよりますが、単体マシンの性能もどんどん向上している以上、その範囲内でもっとも効率よく処理できるのであれば1台になるべく詰め込んだほうがリソースは効率的に使えるし管理も楽だし価格も安くなる計算だからです。

あとは物理サーバーとの比較ですね。FreakOutは国内のbidサーバーについては自作のマシンを使っていると言っていて写真もありましたが、実際あればクラウド、例えばc1.xlargeと比べてどの程度コストメリットがあるのかどうか。

また、FreakOutはPerlをメイン言語としていると聞いているので、Perlでどう作っているかというのは大変気になりました。

AWS Summit Tokyo 2013(1) Redshiftのパフォーマンスと使いどころ

AWS Summit Tokyo 2013の中から2セッション、まとめておきたいと思います。1つめはRedshiftに関するセッションです。

セッション紹介 | AWS Summit Tokyo 2013
「Amazon Redshiftが切り開くクラウド・データウェアハウス」

私の勤務するAMoAdでは、RedShiftがUS-Eastリージョンで使えるようになって以来注目していて、実際に本番環境で使ってきました。 私は中心的にRedshiftをじっくり使ってきたわけではないのですが、運用担当として興味深く聴講しました。

Redshiftの概要

エンタープライズシステムにおける活用ノウハウ

Amazon Redshiftの取り組みについて

クラウド型データウェアハウス
オンプレ環境におけるデータウェアハウスの課題
・初期投資
・運用管理
・成長予測・費用対効果

install backupなどのルーチンワークをAWSに任せて、codingとperformance tuningに割く時間を増やせる

RDS, DynamoDBなどのオンライントランザクション→ EMR, S3→Redshift

カラムナ型データベース
列が多いケースにはパフォーマンスの向上が見られやすい

* Leader Node クエリのパース・分析をしてC++コードを生成してcompute nodeに投げる

* compute nodes ここを増やすことで器を大きくできる

    1.6PBまで拡張可能

JDBCのPostgreSQLドライバを使える

AmazonS3かDynamoDBからの並列ロード

S3への自動バックアップ・増分バックアップ・オンデマンドのバックアップ 
リサイズする場合:新しいクラスタをバックグラウンドでプロビジョニング
ノード数×時間単価の課金 

日本ではデータウェアハウスとOLTPの中間的な使い方をしている場合が多い 

まずはRedshiftの概要の説明です。Redshiftはすでに基本的なところは調べて実際に使ってきたのでほとんどが既知の事項でした。

エンタープライズ向けを主に意識しているセッションだけあって聞き慣れない単語がいくつかありましたが・・・そもそもRedshiftは個人で気軽に試すには若干値が張る(最小構成でも1ノードを1ヶ月フルで使うと$0.85 x 24hour x 30dayで6万円程度)

## Redshiftの性能は?

他と比較するとSEの寿命が縮まるので・・・

### 巨大テーブルからの検索処理

1.5TBくらいのデータの検索結果  500億件
8XL 2node 43.5sec
8XL 4node 27.8sec
8XL 8node 19.8sec

線形に性能が向上しているのに驚いた

### データロードのスピード 500億件

8XL 2node 2:54sec
8XL 4node 1:37
8XL 8node 0:46

### 1.2億件の検索結果

8XL 2node 3.30 sec
8XL 4node 1.40 sec
8XL 8node 1.80 sec

## バッチ処理は?

EMRとの比較
1.5TB, 500億件

EMR 29min
Redshift 17min

EMR 71min
Redshift 5min

チューニングのポイント
キー(index)

distribution keyに応じてスライスに配置される
sort keyをもとにレコードがソートされる

### チューニング効果
* 1.2億件

指定無しは1.8秒
指定ありは1秒

* バッチ処理

指定あり:24min
指定無し:16min

sort keyを入れるとロードは遅くなるので注意

### コスト:w
128coe 960gb 128tb storage
構築費はほぼ不要、維持管理のSEも不要、センター費用は不要

### 注意点は?
不得意なデータ形式もある
EMRで前処理

簡単につくれてしまうため、統制のきいていないシステムの乱立に注意

### まとめ

* 性能は線形にスケール
* バッチ処理も得意
* チューニングは新しい概念で
* 利用料課金はメリット大
* 注意点もある

次は野村総研の方からの検証結果報告です。データのSELECTが早いのは確かだと思います。ただ、セッションでも言及されているように、「不得意なデータ形式もある」ということで、結局のところRedshiftのポテンシャルをフルに引き出して使いこなすには、時間とともに溜まるノウハウの蓄積が欠かせない気がします。

データのロードについても同様で、FlyData for Amazon Redshiftというサービスがあるくらい、Redshiftに適切な速度で要領よくデータをアップロードするのは難しいという印象です。このへんも各所で検証が進むにつれて、うまいやり方というのが見いだされてくるのかなと思います。

最速ウェブアプリのためのJDBC再入門(7) RowSet Objects

RowSetはResultSetの拡張

RowSet ObjectはResultSetを拡張したクラスです。ResultSetがデータベースの実装をそのまま引き継ぐタイプの実装であるのに対し、RowSetはアプリケーション側で扱いやすいように機能が追加されています。

RowSetはJavaBeansでもあります。JavaBeansとは、下記のような特徴を持ちます。

  • 引数の無いコンストラクタで直接インスタンス化できる
  • getter/setterがある
  • Serializableである

RowSet Objectの種類

RowSetには主に下記の5種類のインタフェースがあります。

[接続維持型]

  • JDBCRowSet

接続型のRowSet

[接続非維持型]

  • CachedRowSet

データをDBから取得後、オブジェクト内にキャッシュし、接続を切断する。

  • WebRowSet

CachedRowSetにXMLの拡張がついている

  • JoinRowSet

WebRowSetの拡張で、別テーブル同士のJOINができる

  • FilteredRowSet

WebRowSetの拡張で、データのフィルタリング機能を持つ

この中で、使いそうだと感じたCachedRowSetとJoinRowSetを試してみます。

Using CachedRowSetObjects (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/cachedrowset.html

Using JoinRowSet Objects (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/joinrowset.html

CachedRowSet: 更新時にコンフリクトした場合の解決, 他のオブジェクトへのメッセージ機能

1つ1つの処理が短く、長くても数百ミリ秒でレスポンスを返すウェブだとあまり恩恵がないかもしれないのですが、CachedObjectはDBからデータを取得した後、即座に接続を切断(実際にはコネクションプールに戻す)しても、オブジェクト自体はデータを保持しています。

ResultSetはConnectionから用意したStatementオブジェクトから返されるものなので、DBへの接続を保持している必要があったので、コネクションプールが枯渇しやすい状況においてはメリットになるかもしれません。

1
2
3
cachedRowSet = new CachedRowSetImpl();
cachedRowSet.setCommand("SELECT * FROM COFFEES");
cachedRowSet.execute(connection);

接続の取得方法はいくつかありますが、今回はexecuteのときにconnectionを渡すようにしています。 下記のようにDataSouceを渡す方法も使えます。

1
2
3
4
cachedRowSet = new CachedRowSetImpl();
cachedRowSet.setDataSouceName("java:comp/env/jdbc/continuousops");
cachedRowSet.setCommand("SELECT * FROM COFFEES");
cachedRowSet.execute();

他のオブジェクトに通知をしたい場合、addRowSetListener()で指定できます。 RowSetListenerは下記の3つのイベントをサポートしています。

  • rowSetChanged
  • rowChanged
  • cursorMoved
1
cachedRowSet.addRowSetListener(new ExampleRowSetListener());

CachedRowSetはオブジェクトにキャッシュしているデータなので、データを更新してコミットしようとした際に、元のデータとズレが生じる可能性があります。CachedRowSetはそれが発生した際に例外を投げる仕組みが用意されているので、それを試してみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//getting first cachedRowSet
cachedRowSet = new CachedRowSetImpl();
cachedRowSet.setCommand("SELECT * FROM COFFEES");
cachedRowSet.execute(connection);

//getting second cachedRowSet
cachedRowSet2 = new CachedRowSetImpl();
cachedRowSet2.setCommand("SELECT * FROM COFFEES");
cachedRowSet2.execute(connection);

cachedRowSet.next();
cachedRowSet.updateInt("SUP_ID", 101);
cachedRowSet.updateRow();
cachedRowSet.acceptChanges();

cachedRowSet2.next();
cachedRowSet2.updateInt("SUP_ID", 150);
cachedRowSet2.updateRow();
cachedRowSet2.acceptChanges();

cachedRowSetのインスタンスを2つ用意し、データをそれぞれ取得した後データを更新かけてみます。こうすると、1つめのcachedRowSetを更新した後、2つめのcachedRowSetはオブジェクト内のデータとDBのレコードにずれが出るので、一番最後の文であるcachedRowSet2.acceptChanges();を実行したときに例外が投げられます。

それが、SyncProviderExceptionです。これをcatchしてコンフリクトを解決させようとしました。下記はほとんどOracleのサンプルコードのままです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
} catch (SyncProviderException spe){
    System.out.println("conflict detected.");

    SyncResolver resolver = spe.getSyncResolver();

    Object crsValue;
    Object resolverValue;
    Object resolvedValue;

    try {
        while (resolver.nextConflict()) {

            if (resolver.getStatus() == SyncResolver.UPDATE_ROW_CONFLICT) {
                int row = resolver.getRow();
                cachedRowSet2.absolute(row);

                int colCount = cachedRowSet2.getMetaData().getColumnCount();
                for (int j = 1; j <= colCount; j++) {
                    System.out.println("conflict value: " + resolver.getConflictValue(j));
                    if (resolver.getConflictValue(j) != null) {
                        crsValue = cachedRowSet2.getObject(j);
                        resolverValue = resolver.getConflictValue(j);

                        resolvedValue = crsValue;

                        resolver.setResolvedValue(j, resolvedValue);
                        System.out.println("conflict resolved.");
                    }
                }
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
}

ただ、これは実際にはうまく動作させることができていません。 下記のようになってしまい、conflictが検知できるものの、実際にループを回すとコンフリクトが生じているデータが取得できず、そのままループが終わってしまいます。

1
2
3
4
5
6
conflict detected.
conflict value: null
conflict value: null
conflict value: null
conflict value: null
conflict value: null

結果としては、1つ目の更新が反映され、2つ目の更新は無視されるという挙動になります。 これがMySQLとかConnector-Jの実装によるものなのか、私が書いたコードの問題なのかは結局のところ分かりませんでした。

JoinRowSet: 2つのRowSetからテーブル同士のJOINと同等のことを行う

もう一つの興味深い機能である、RowSetのジョイン機能も試してみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
mysql> SELECT * FROM COFFEES;
+--------------------+--------+-------+-------+-------+
| COF_NAME           | SUP_ID | PRICE | SALES | TOTAL |
+--------------------+--------+-------+-------+-------+
| Amaretto           |    101 |  5.99 |     0 |     0 |
| Amaretto_decaf     |     49 | 10.99 |     0 |     0 |
| Colombian          |    101 |  0.07 |     0 |     0 |
| Colombian_Decaf    |    101 |  8.99 |     0 |     0 |
| Espresso           |    150 |  9.99 |     0 |     0 |
| French_Roast       |     49 |  8.99 |     0 |     0 |
| French_Roast_Decaf |     49 |  9.99 |     0 |     0 |
| Hazelnut           |     49 |  9.99 |     0 |     0 |
| Hazelnut_decaf     |     49 | 10.99 |     0 |     0 |
+--------------------+--------+-------+-------+-------+
9 rows in set (0.00 sec)

mysql> SELECT * FROM SUPPLIERS;
+--------+---------------------------+---------------------+--------------+-------+-------+
| SUP_ID | SUP_NAME                  | STREET              | CITY         | STATE | ZIP   |
+--------+---------------------------+---------------------+--------------+-------+-------+
|     49 | Superior Coffee           | 1 Party Place       | Mendocino    | CA    | 95460 |
|    101 | Acme, Inc.                | 99 Market Street    | Groundsville | CA    | 95199 |
|    150 | The High Ground           | 100 Coffee Lane     | Meadows      | CA    | 93966 |
|    456 | Restaurant Supplies, Inc. | 200 Magnolia Street | Meadows      | CA    | 93966 |
|    927 | Professional Kitchen      | 300 Daisy Avenue    | Groundsville | CA    | 95199 |
+--------+---------------------------+---------------------+--------------+-------+-------+
5 rows in set (0.00 sec)

このように、COFFEESテーブルとSUPPLIERテーブルがあるとして、サプライヤー名を指定したときにそのIDをキーにしてコーヒーリストから商品名を持ってきたい・・・というようなケースはテーブルジョインのよくあるケースです。その際、普通は

1
2
3
4
SELECT c.COF_NAME
FROM COFFEES AS c, SUPPLIERS AS s
WHERE c.SUP_ID = s.SUP_ID
AND SUP_NAME = "Acme, Inc."

などとやるのが普通ですが、これを各テーブルのRowSetをJoinRowSetを使ってJavaコード内でジョインさせることができます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
coffees = new CachedRowSetImpl();
coffees.setCommand("SELECT * FROM COFFEES");
coffees.execute(connection);

suppliers = new CachedRowSetImpl();
suppliers.setCommand("SELECT * FROM SUPPLIERS");
suppliers.execute(connection);

jrs = new JoinRowSetImpl();
jrs.addRowSet(coffees, "SUP_ID");
jrs.addRowSet(suppliers, "SUP_ID");

System.out.println("Coffees bought from " + supplierName + ": ");
while (jrs.next()) {
    if (jrs.getString("SUP_NAME").equals(supplierName)) {
        String coffeeName = jrs.getString(1);
        System.out.println("     " + coffeeName);
    }
}

これでJOINしたSQLと同等の結果が得られます。

実際には使う機会は多くなさそうですが、データベースをシャーディングしていて別データベースにあるテーブルをJOINしたい場合などには便利に使えると思いました。

今回のソースはこちらです。

https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter7

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(6) トランザクション

一般的なトランザクション処理の流れ

複数の処理をまとめてコミットorロールバックできるトランザクション機能。JDBCでの基本フローを試してみます。 前回と同様下記の内容を参考にしています。

Using Transactions (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/transactions.html

下記のようなレコードがあるとして、

1
2
3
4
5
6
7
8
mysql> SELECT * FROM COFFEES WHERE COF_NAME IN ("Amaretto", "Espresso");
+----------+--------+-------+-------+-------+
| COF_NAME | SUP_ID | PRICE | SALES | TOTAL |
+----------+--------+-------+-------+-------+
| Amaretto |     49 |  9.99 |     0 |     0 |
| Espresso |    150 |  9.99 |     0 |     0 |
+----------+--------+-------+-------+-------+
2 rows in set (0.00 sec)

これらのPRICEをひとまとまりで更新してみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
try {
  context = new InitialContext();
  DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/continuousops");
  connection = dataSource.getConnection();

  connection.setAutoCommit(false);

  preparedStatement1 = connection.prepareStatement("UPDATE COFFEES SET PRICE=? WHERE COF_NAME = 'Amaretto' LIMIT 1");
  preparedStatement2 = connection.prepareStatement("UPDATE COFFEES SET PRICE=? WHERE COF_NAME = 'Espresso' LIMIT 1");

  preparedStatement1.setFloat(1, 7.99f);
  preparedStatement1.executeUpdate();

  preparedStatement2.setFloat(1, 6.99f);
  preparedStatement2.executeUpdate();

  connection.commit();

} catch (NamingException | SQLException e) {
  connection.rollback();
  e.printStackTrace();
}finally{
  connection.setAutoCommit(true);
}

実行結果

1
2
3
4
5
6
7
8
mysql> SELECT * FROM COFFEES WHERE COF_NAME IN ("Amaretto", "Espresso");
+----------+--------+-------+-------+-------+
| COF_NAME | SUP_ID | PRICE | SALES | TOTAL |
+----------+--------+-------+-------+-------+
| Amaretto |     49 |  7.99 |     0 |     0 |
| Espresso |    150 |  6.99 |     0 |     0 |
+----------+--------+-------+-------+-------+
2 rows in set (0.00 sec)

JDBCでのトランザクションは、

  • connection.setAutoCommit(false)でトランザクション開始
  • connection.commit()でコミット
  • connection.rollback()でロールバック
  • connection.setAutoCommit(true)で元のモードに戻す 

というのが一連の流れです。

トランザクション分離レベル: REPEATABLE READ

トランザクションは、更新を一括で行うという意味の他に、トランザクション処理中はデータの一貫性が保証されるという意味も持っています。

1
2
3
4
5
6
7
mysql> show variables like '%isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

MySQL5.5のデフォルトのトランザクション分離レベルはREPEATABLE READです。 JDBCのトランザクション分離レベルは使用しているデータベースの設定に従うので、下記のコードで調べてみると、

1
int isolationLevel = connection.getTransactionIsolation();

結果は、4となります。

Constant Field Values (Java Platform SE 7 ) http://docs.oracle.com/javase/7/docs/api/constant-values.html#java.sql.Connection.TRANSACTION_READ_COMMITTED

4はREPEATABLE_READとなっているので、MySQLの設定を使っていると確認できます。

アプリケーションによって性能を優先したい場合、一段階下げてREAD COMMITEDで運用するようにしてもよいと思います。InnoDBはREPEATABLE READでネクストキーロックをするので、INSERT性能が下がってしまうためです。

Rollbackする地点を指定:Sevepointの使用

JDBCには一歩進んだ機能として、トランザクション中の任意のポイントまでロールバックできるSavepointという仕組みが用意されています。これをConnector-Jでも使用可能か試してみます。

下記のようなコードの場合、savepoint2まで途中でロールバックしているので、ステートメント1の更新は反映されるが、ステートメント2の更新はロールバックされているという挙動になります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    Savepoint savepoint1 = null;
    Savepoint savepoint2 = null;
    try {
        context = new InitialContext();
        DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/continuousops");
        connection = dataSource.getConnection();

        connection.setAutoCommit(false);

        savepoint1 = connection.setSavepoint("point1");

        preparedStatement1 = connection.prepareStatement("UPDATE COFFEES SET PRICE=? WHERE COF_NAME = 'Amaretto' LIMIT 1");
        preparedStatement2 = connection.prepareStatement("UPDATE COFFEES SET PRICE=? WHERE COF_NAME = 'Espresso' LIMIT 1");

        preparedStatement1.setFloat(1, 5.99f);
        preparedStatement1.executeUpdate();

        savepoint2 = connection.setSavepoint("point2");

        preparedStatement2.setFloat(1, 7.99f);
        preparedStatement2.executeUpdate();

        connection.rollback(savepoint2);

        connection.commit();

    } catch (NamingException | SQLException e) {
        try {
            connection.rollback(savepoint1);

実行前

1
2
3
4
5
6
7
8
mysql> SELECT * FROM COFFEES WHERE COF_NAME IN ("Amaretto", "Espresso");
+----------+--------+-------+-------+-------+
| COF_NAME | SUP_ID | PRICE | SALES | TOTAL |
+----------+--------+-------+-------+-------+
| Amaretto |     49 |  9.99 |     0 |     0 |
| Espresso |    150 |  9.99 |     0 |     0 |
+----------+--------+-------+-------+-------+
2 rows in set (0.00 sec)

実行後

1
2
3
4
5
6
7
8
mysql> SELECT * FROM COFFEES WHERE COF_NAME IN ("Amaretto", "Espresso");
+----------+--------+-------+-------+-------+
| COF_NAME | SUP_ID | PRICE | SALES | TOTAL |
+----------+--------+-------+-------+-------+
| Amaretto |     49 |  5.99 |     0 |     0 |
| Espresso |    150 |  9.99 |     0 |     0 |
+----------+--------+-------+-------+-------+
2 rows in set (0.00 sec)

1個めのUPDATEだけが適用されました。

Savepointは、大規模なトランザクションでは使いどころがありそうです。

今回のソースはこちらです。

https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter6

参考:

トランザクション分離レベル – Wikipedia http://ja.wikipedia.org/wiki/%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E5%88%86%E9%9B%A2%E3%83%AC%E3%83%99%E3%83%AB

MySQL5 分離レベル ~ transaction_isolation | QuickKnowLedge http://www.s-quad.com/wordpress/?p=956

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(5) カーソルと更新

Cursorの自由な移動

引き続き下記のページを見ていきます。後半部分の、カーソルを移動させたりrowを更新したりする部分がConnector-Jで実際にできるかどうかを確認していきます。

Retrieving and Modifying Values from Result Sets (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html

ドキュメント通りに、下記のようなテーブルがあるとします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
mysql> select * from COFFEES;
+--------------------+--------+-------+-------+-------+
| COF_NAME           | SUP_ID | PRICE | SALES | TOTAL |
+--------------------+--------+-------+-------+-------+
| Colombian          |    101 |  7.99 |     0 |     0 |
| Colombian_Decaf    |    101 |  8.99 |     0 |     0 |
| Espresso           |    150 |  9.99 |     0 |     0 |
| French_Roast       |     49 |  8.99 |     0 |     0 |
| French_Roast_Decaf |     49 |  9.99 |     0 |     0 |
+--------------------+--------+-------+-------+-------+
5 rows in set (0.00 sec)

mysql> show create table COFFEES\G
*************************** 1. row ***************************
       Table: COFFEES
Create Table: CREATE TABLE `COFFEES` (
  `COF_NAME` varchar(32) NOT NULL,
  `SUP_ID` int(11) NOT NULL,
  `PRICE` decimal(10,2) NOT NULL,
  `SALES` int(11) NOT NULL,
  `TOTAL` int(11) NOT NULL,
  PRIMARY KEY (`COF_NAME`),
  KEY `SUP_ID` (`SUP_ID`),
  CONSTRAINT `coffees_ibfk_1` FOREIGN KEY (`SUP_ID`) REFERENCES `SUPPLIERS` (`SUP_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

まず1レコード目を取得します。

1
2
3
4
5
6
7
8
private List<HashMap> viewTable(Connection connection, String dbName) throws SQLException {
        String query = "SELECT COF_NAME, SUP_ID, PRICE, SALES, TOTAL FROM " + dbName + ".coffees" ;
        preparedStatement = connection.prepareStatement(query);
        resultSet = preparedStatement.executeQuery();
        resultSet.next();
        System.out.println("|" + resultSet.getString("COF_NAME")
                         + "|" + resultSet.getInt("SUP_ID")
                         + "|" + resultSet.getBigDecimal("PRICE") + "|");
1
|Colombian|101|7.99|

2レコード進んで取得します。

1
2
3
4
5
resultSet.next();
resultSet.next();
System.out.println("|" + resultSet.getString("COF_NAME")
               + "|" + resultSet.getInt("SUP_ID")
               + "|" + resultSet.getBigDecimal("PRICE") + "|");
1
|Espresso|150|9.99|

1レコード戻ります。

1
2
3
4
resultSet.previous();
System.out.println("|" + resultSet.getString("COF_NAME")
      + "|" + resultSet.getInt("SUP_ID")
      + "|" + resultSet.getBigDecimal("PRICE") + "|");
1
|Colombian_Decaf|101|8.99|

最後のレコードに飛びます。

1
2
3
4
resultSet.last();
System.out.println("|" + resultSet.getString("COF_NAME")
      + "|" + resultSet.getInt("SUP_ID")
      + "|" + resultSet.getBigDecimal("PRICE") + "|");
1
|French_Roast_Decaf|49|9.99|

4レコード目にジャンプします。

1
2
3
4
resultSet.absolute(4);
System.out.println("|" + resultSet.getString("COF_NAME")
      + "|" + resultSet.getInt("SUP_ID")
      + "|" + resultSet.getBigDecimal("PRICE") + "|");
1
|French_Roast|49|8.99|

カーソルを最後まで進めます。その次は無いのでnext()でfalseになるはず。

1
2
resultSet.afterLast();
System.out.println(resultSet.next());
1
false

期待通りの結果が得られました。

ResultSetを直接Update

使うかどうか分かりませんが、ResultSetからrowを直接更新する方法も用意されています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void updateTable(Connection connection, String dbName) throws SQLException {
        float percentage = 0.3f;
        String query = "SELECT cof_name, sup_id, price, sales, total FROM " + dbName + ".coffees" ;
        preparedStatement = connection.prepareStatement(query);
        resultSet = preparedStatement.executeQuery();
        resultSet.next();
        System.out.println("|" + resultSet.getString("COF_NAME")
                + "|" + resultSet.getInt("SUP_ID")
                + "|" + resultSet.getBigDecimal("PRICE") + "|");
        float f = resultSet.getFloat("PRICE");
        resultSet.updateFloat("PRICE", f * percentage);
        resultSet.updateRow();
        System.out.println("|" + resultSet.getString("COF_NAME")
                + "|" + resultSet.getInt("SUP_ID")
                + "|" + resultSet.getBigDecimal("PRICE") + "|");
    }

ただ、これをそのまま実行しようとするとエラーになります。属性がCONCUR_UPDATABLEである必要があるのですが、昨日みたように、デフォルトはCONCUR_READ_ONLYだからです。

1
2
com.mysql.jdbc.NotUpdatable: Result Set not updatable.This result set must come from a statement that was created with a result set type of ResultSet.CONCUR_UPDATABLE, the query must select only one table, can not use functions and must select all primary keys from that table. See the JDBC 2.1 API Specification, section 5.6 for more details.This result set must come from a statement that was created with a result set type of ResultSet.CONCUR_UPDATABLE, the query must select only one table, can not use functions and must select all primary keys from that table. See the JDBC 2.1 API Specification, section 5.6 for more details.
  at com.mysql.jdbc.ResultSetImpl.updateFloat(ResultSetImpl.java:8319)

CONCUR_UPDATABLEにしてやってみます。サンプル通りStatementオブジェクトを使ってしまいましたが、PreparedStatementにしたければprepareCall()の中でCONCUR_UPDATEBLEを指定すればいけると思います。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void updateTableRevised(Connection connection, String dbName) throws SQLException {
  float percentage = 0.3f;
  String query = "SELECT cof_name, sup_id, price, sales, total FROM " + dbName + ".coffees" ;
  Statement statement = connection.createStatement(ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE);
  resultSet = statement.executeQuery(query);
  resultSet.next();
  System.out.println("|" + resultSet.getString("COF_NAME")
          + "|" + resultSet.getInt("SUP_ID")
          + "|" + resultSet.getBigDecimal("PRICE") + "|");
  float f = resultSet.getFloat("PRICE");
  resultSet.updateFloat("PRICE", f * percentage);
  resultSet.updateRow();
  System.out.println("|" + resultSet.getString("COF_NAME")
          + "|" + resultSet.getInt("SUP_ID")
          + "|" + resultSet.getBigDecimal("PRICE") + "|");
}
1
2
|Colombian|101|2.40|
|Colombian|101|0.72|

更新できました。

executeBatch()による更新

batchによる更新もサポートされています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public void batchUpdate(Connection connection) throws SQLException {
  Statement stmt = null;
  try {
      connection.setAutoCommit(false);
      stmt = connection.createStatement();

      stmt.addBatch(
              "INSERT INTO COFFEES " +
                      "VALUES('Amaretto', 49, 9.99, 0, 0)");

      stmt.addBatch(
              "INSERT INTO COFFEES " +
                      "VALUES('Hazelnut', 49, 9.99, 0, 0)");

      stmt.addBatch(
              "INSERT INTO COFFEES " +
                      "VALUES('Amaretto_decaf', 49, " +
                      "10.99, 0, 0)");

      stmt.addBatch(
              "INSERT INTO COFFEES " +
                      "VALUES('Hazelnut_decaf', 49, " +
                      "10.99, 0, 0)");

      int [] updateCounts = stmt.executeBatch();
      System.out.println(updateCounts);
      connection.commit();

  } catch(BatchUpdateException b) {
      b.printStackTrace();
  } catch(SQLException ex) {
      ex.printStackTrace();
  } finally {
      if (stmt != null) { stmt.close(); }
      connection.setAutoCommit(true);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> select * from COFFEES;
+--------------------+--------+-------+-------+-------+
| COF_NAME           | SUP_ID | PRICE | SALES | TOTAL |
+--------------------+--------+-------+-------+-------+
| Amaretto           |     49 |  9.99 |     0 |     0 |
| Amaretto_decaf     |     49 | 10.99 |     0 |     0 |
| Colombian          |    101 |  0.07 |     0 |     0 |
| Colombian_Decaf    |    101 |  8.99 |     0 |     0 |
| Espresso           |    150 |  9.99 |     0 |     0 |
| French_Roast       |     49 |  8.99 |     0 |     0 |
| French_Roast_Decaf |     49 |  9.99 |     0 |     0 |
| Hazelnut           |     49 |  9.99 |     0 |     0 |
| Hazelnut_decaf     |     49 | 10.99 |     0 |     0 |
+--------------------+--------+-------+-------+-------+

これはトランザクションではないようなので使いどころがいまいち分かりませんが、名前の通りバッチ処理をする際に少しでも実行時間を短縮するために使えるのかもしれません。

今日のソースはこちらです。 https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter5

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(4) ResultSetの特性

MySQL-Connector-J 5.1のResultSetの実装

前回SELECTクエリを発行したときに、ResultSetというオブジェクトが返ってくるのでクエリ結果はそこから取り出すということをやりました。

Oracle公式のドキュメントを読んでみるとResultSetは多くの機能をサポートしていますが、MySQLのconnector-Jで全てサポートしているかどうかがよく分からなかったので調べてみました。複数の機能の切り替えをサポートしている場合、切り替えることでパフォーマンスが改善する可能性が考えられるからです。

Retrieving and Modifying Values from Result Sets (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/retrieving.html

このドキュメントページによると、ResultSetが提供する特性(characteristics)には、Types, Concurrency, Holdabilityの3種類があります。

ResultSet Types

カーソルがどのように操作されるかと、並行実行された変更が反映されるかどうかの違いによって3種類あるようです。日本語訳を引用します。

 TYPE_FORWARD_ONLY
 結果セットは、スクロール不可能です。カーソルは上から下へ、順方向にのみ移動します。
 結果セット内のデータの見え方は、DBMS が結果をインクリメンタルに生成するかどうかに依存します。

 TYPE_SCROLL_INSENSITIVE
 結果セットはスクロール可能です。 カーソルは順方向および逆方向に移動可能で、移動先の行を直接指定することも、現在位置からの相対位置で指定することもできます。
 一般に、結果セットは、基となるデータベースが開いている間に行われた変更を表しません。 一般に、行のメンバシップ、順序、および列値は、結果セットの作成時に固定されます。

 TYPE_SCROLL_SENSITIVE
 結果セットはスクロール可能です。カーソルは順方向および逆方向に移動可能で、移動先の行を直接指定することも、現在位置からの相対位置で指定することもできます。
 結果セットは、開いている間に行われた変更を反映します。 基となる列値が変更されると、新たな値が反映されます。つまり、基となるデータの動的なビューを提供します。 結果セットに含まれる行とその順序は、実装によって固定されることも、されないこともあります。

Connector-Jの現行バージョンではどれなのか調べてみました。

1
2
3
4
DatabaseMetaData databaseMetaData = connection.getMetaData();
System.out.printf("[TYPE_FORWARD_ONLY]%s%n", databaseMetaData.supportsResultSetType(ResultSet.TYPE_FORWARD_ONLY));
System.out.printf("[TYPE_SCROLL_INSENSITIVE]%s%n", databaseMetaData.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE));
System.out.printf("[TYPE_SCROLL_SENSITIVE]%s%n", databaseMetaData.supportsResultSetType(ResultSet.TYPE_SCROLL_SENSITIVE));

結果

1
2
3
[TYPE_FORWARD_ONLY]false
[TYPE_SCROLL_INSENSITIVE]true
[TYPE_SCROLL_SENSITIVE]false

TYPE_SCROLL_INSENSITIVEのみがサポートされています。

ResultSet Concurrency 並行処理のタイプ

次は結果セットを更新可能かどうかです。

 CONCUR_READ_ONLY
 プログラム的に更新不可能な結果セットを示します。
 JDBC 1.0 API だけを実装するドライバから利用可能な並行処理のタイプ
 最高レベルの並行処理を提供します (同時使用可能なユーザ数を最大にします)。 読み取り専用の並行処理が可能な ResultSet オブジェクトでロックを設定する場合には、読み取り専用ロックが使用されます。 これにより、ユーザはデータを読み取ることができますが、変更はできません。 データに対して同時に設定可能な読み取り専用ロックの数は無制限であるため、DBMS またはドライバが制限を課さない限り、同時使用するユーザ数には制限がありません。

 CONCUR_UPDATABLE
 プログラム的に更新可能な結果セットを示します。
 JDBC 2.0 コア API を実装するドライバから利用可能
 並行処理のレベルを低下させます。 一度にデータ項目にアクセス可能なユーザを 1 人だけに限定する場合、更新可能な結果セットは書き込み専用ロックを使用します。 これにより、2 人以上のユーザが同じデータを変更する可能性が排除されるため、データベースの一貫性が保証されます。 ただし、この一貫性の代償として、並行処理のレベルが低下します。

これもサポート状況を調べてみました。

1
2
System.out.printf("[CONCUR_READ_ONLY]%s%n", databaseMetaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY));
System.out.printf("[CONCUR_UPDATABLE]%s%n", databaseMetaData.supportsResultSetConcurrency(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE));

結果

1
2
[CONCUR_READ_ONLY]true
[CONCUR_UPDATABLE]true

両方サポートしているようです。デフォルトはCONCUR_READ_ONLYです。

ResultSet Cursor Holdability

Cursor Holdabilityというのは、トランザクションをコミットしたときに通常はカーソルがクローズされるものを、保持しておくことを可能にするか否かのオプションのようです。

 HOLD_CURSORS_OVER_COMMIT
 現在のトランザクションがコミットされたときに、この保持機能を持つオープンしている ResultSet オブジェクトがクローズする。

 CLOSE_CURSORS_AT_COMMIT
 現在のトランザクションがコミットされたときに、この保持機能を持つオープンしている ResultSet オブジェクトがオープンしたままになる。

これも同様にサポート状況を調べてみました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
DatabaseMetaData dbMetaData = connection.getMetaData();
System.out.println("ResultSet.HOLD_CURSORS_OVER_COMMIT = " +
          ResultSet.HOLD_CURSORS_OVER_COMMIT);

System.out.println("ResultSet.CLOSE_CURSORS_AT_COMMIT = " +
          ResultSet.CLOSE_CURSORS_AT_COMMIT);

System.out.println("Default cursor holdability: " +
          dbMetaData.getResultSetHoldability());

System.out.println("Supports HOLD_CURSORS_OVER_COMMIT? " +
          dbMetaData.supportsResultSetHoldability(
                    ResultSet.HOLD_CURSORS_OVER_COMMIT));

System.out.println("Supports CLOSE_CURSORS_AT_COMMIT? " +
          dbMetaData.supportsResultSetHoldability(
                    ResultSet.CLOSE_CURSORS_AT_COMMIT));

結果

1
2
3
4
5
ResultSet.HOLD_CURSORS_OVER_COMMIT = 1
ResultSet.CLOSE_CURSORS_AT_COMMIT = 2
Default cursor holdability: 1
Supports HOLD_CURSORS_OVER_COMMIT? true
Supports CLOSE_CURSORS_AT_COMMIT? false

通常、コミットされたらカーソルをクローズしてくれたほうがいいように直感的には思いますが、

 Holdable cursors might be ideal if your application uses mostly read-only ResultSet objects.

とのことなので、トランザクションの実行に関わらず読み取りを高速に行いたい場合が考慮されているのかもしれません。そもそもカーソルのクローズは自動的にされるか、明示的に呼ぶと思いますので、よほど長い処理でない限りはどちらでも大差ない気はします。

基本的にはデフォルトのまま使うので問題ないように思いますが、このへんはMySQLの設定のほうにあるトランザクションの分離レベルとも関わってくると思うのですが、今回調べただけだとどう影響しあうかがよく分かりませんでした。

次回ももう少しこのへんを深掘りしてみます。

今回のソースはこちら。 https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter4

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(3) INSERT文とSELECT文

SQL発行の基本的な手順

前回はデータベースに接続し、そのまま切断しただけでしたが、今回はWriteとReadの処理を1回ずつやってみます。

ソースはこちらです。 https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter3

基本的にはPHPのPDOやPerlのPythonのMySQLdbなどとクエリの発行の構造は同じで、PrepareStatementというオブジェクトを使います。

ConnectionオブジェクトからPrepareedStatementを作成し、そこからクエリを実行、という流れです。

参考: Using Prepared Statements (The Java™ Tutorials > JDBC™ Database Access > JDBC Basics) http://docs.oracle.com/javase/tutorial/jdbc/basics/prepared.html

INSERT文:executeUpdate()メソッドを使う:戻り値は処理レコード数

1
2
3
4
5
6
7
8
9
private void insertRecord(Connection connection) throws SQLException {
  PreparedStatement preparedStatement = connection.prepareStatement("INSERT INTO address (name, address, tel, email) VALUES(?, ?, ?, ?)");
  preparedStatement.setString(1, getParameter("name"));
  preparedStatement.setString(2, getParameter("address"));
  preparedStatement.setString(3, getParameter("tel"));
  preparedStatement.setString(4, getParameter("email"));
  preparedStatement.executeUpdate();
  preparedStatement.close();
}

connection.prepareStatement()メソッド内でSQLを記述し、”?“をプレイスホルダとして使います。”?“の部分はsetXXXX()メソッド(XXXXの部分は型が入る)のなかで番号に対応する形で値を入れています。

INSERT/UPDATE/DELETE文の発行はexecuteUpdate()メソッドを使います。

疑問:JDBCにはキーワード付プレイスホルダは無いんでしょうか?

SELECT文:executeQuery()メソッドを使う:戻り値はResultSet

1
2
3
4
5
6
7
8
9
10
11
12
13
private HashMap selectRecord(Connection connection) throws SQLException {
  PreparedStatement preparedStatement = connection.prepareStatement("SELECT * FROM address LIMIT 1");
  ResultSet resultSet = preparedStatement.executeQuery();
  HashMap<String, String> map = new HashMap<>();
  if(resultSet.next()){
      map.put("name-1", resultSet.getString("name"));
      map.put("address-1", resultSet.getString("address"));
      map.put("tel-1", resultSet.getString("tel"));
      map.put("email-1", resultSet.getString("email"));
  }
  preparedStatement.close();
  return map;
}

SELECT文も、connection.prepareStatement()メソッド内でSQLを記述します。

SELECT文の実行には、executeQuery()メソッドを使います。このメソッドはResultSetオブジェクトが返るので、resultSet.next()でループして結果を取得します。

今回はPreparedStatementを使いましたが、下記のように単純なStatementオブジェクトで記述することもできます。

1
2
3
4
5
6
7
Statement statement = null;
String query = "SELECT * FROM address LIMIT 1";

try {
  statement = connection.createStatement();
  ResultSet resultSet = statement.executeQuery(query);
(以下略)

Statementとその派生クラスには3種類あります。

  • Statement パラメータを持たない単純なSQLの実行
  • PreparedStatement SQLをプリコンパイルして実行する
  • CallableStatement プロシージャを呼び出すときに使う

StatementよりPreparedStaementのほうが高速な場合が多いので、SELECT/INSERT/UPDATE/DELETE文のSQLの実行にはPreparedStatementを使うとおぼえておいて基本的に間違いないと思います。

StatementオブジェクトはGCによって自動的にクローズされますが、明示的にclose()を呼んでおくのが良いようです。今回は明示的にclose()を呼んでいます。

ひとまずシンプルにINSERTとSELECTを行なってみました。 次回はSELECTの最適化・高速化という観点から、ResultSetオブジェクトの扱いについて詳しくみてみます。

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(2) プロジェクト作成

JSP & Servletを使った基本的なプロジェクトを作成

まずは基本的なプロジェクトを作成しました。 mavenのプロジェクト構成に沿った形にして、JSPとServletを連携させた最低限の構成です。

ソースはこちらです。  chokkoyamada/DigIntoJDBC at chapter2 · GitHub https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter2

「独習Java サーバーサイド編 第2版」 を教科書的に使いながら作りました。 ただ、書籍の中ではJSPの中で直接データベースに接続しているので、それはJavaのクラスファイルのほうへ外出ししました。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
├── lib
├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── kirishikistudios
│       │           └── continuousops
│       │               └── digintojdbc
│       │                   └── Chapter2.java
│       ├── resources
│       └── webapp
│           ├── META-INF
│           │   └── context.xml
│           ├── WEB-INF
│           │   └── web.xml
│           └── chapter2
│               └── index.jsp

MySQLサーバーはlocalhostに立てて、ユーザー名jdbcuser、パスワードjdbcuserで作ってcontinuousopsというデータベースを作成してあります。

context.xmlの置き場所と意味

上記の中で、context.xmlはどういうルールで書いてどこに置けばよいのかが分かりませんでした。 まとめると下記のようなことのようです。

  • “context.xml”という名前自体に特別な意味はないが、ルールとして推奨されている
  • xmlファイル内で定義する<Context>要素に、アプリケーション単位の情報を定義する。今回の場合はJDBCの接続情報。
  • この<Context>要素を独立した設定ファイル(コンテキスト定義ファイル)にしたものがcontext.xml
  • 配置先は下記の5つのどれかと決まっている。

  • %CATALINA_HOME%/conf/server.xml

  • %CATALINA_HOME%/conf/context.xml
  • %CATALINA_HOME%/conf/<エンジン名>/<ホスト名>/context.xml.default
  • %CATALINA_HOME%/conf/<エンジン名>/<ホスト名>/<アプリケーション名>.xml
  • %CATALINA_HOME%/webapps/<アプリケーション名>/META-INF/context.xml

参考 context.xmlの配置について分かったこと. – 小さな星がほらひとつ http://d.hatena.ne.jp/WorldWorldWorld/20101202/1291293444

「独習Java サーバーサイド編」にも同じことが書いてありました。

下記を見ると、説明の方法がちょっと違います。ひとまず無難なところでMETA-INFの下に置きました。

Apache Tomcat 7 Configuration Reference (7.0.40) – The Context Container http://tomcat.apache.org/tomcat-7.0-doc/config/context.html#Defining_a_context

ローカルのMySQLに接続してみる

Chapter2.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
package com.kirishikistudios.continuousops.digintojdbc;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.sql.DataSource;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;

public class Chapter2 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
        request.setAttribute("subject", "JDBC");
        Context context;
        Connection connection = null;
        try {
            context = new InitialContext();
            DataSource dataSource = (DataSource)context.lookup("java:comp/env/jdbc/continuousops");
            connection = dataSource.getConnection();
            request.setAttribute("message1", "Successfully connected to database.");
            connection.close();
        } catch (NamingException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            if(connection != null) {
                try {
                    connection.close();
                    request.setAttribute("message2", "Successfully disconnected from database.");
                } catch (SQLException e) {
                    e.printStackTrace();
                }
            }
        }
        this.getServletContext().getRequestDispatcher("/chapter2/index.jsp").forward(request, response);
    }
}
index.jsp
1
2
3
4
5
6
7
8
9
<%@ page contentType="text/html; charsert=UTF-8" %>
<!DOCTYPE html>
<html>
<body>
<h2>Hello ${requestScope['subject']}!</h2>
<p>${requestScope['message1']}</p>
<p>${requestScope['message2']}</p>
</body>
</html>

どこからどこをtry-catchで囲んで、例外処理すればよいのかがよくわかってないですが・・・このへんはエラーが出た際にアプリケーションでどう処理したいかにもよってくると思います。

突っ込みどころがあればPull Requestお願いします。

https://github.com/chokkoyamada/DigIntoJDBC/tree/chapter2

https://github.com/chokkoyamada/chokkoyamada.github.com/blob/source/source/_posts/2013-05-23-dig-into-jdbc-2-create-project.markdown

次は実際にSQLを発行していきます。

→最速ウェブアプリのためのJDBC再入門TOPへ

最速ウェブアプリのためのJDBC再入門(1)

Javaの記事はどれも古い!

Javaを仕事で書けるようになるためにあらためて勉強しているのですが、Javaの記事をぐぐってみると古い記事があたることが多いです。

Javaは後方互換性に優れているので10年前の記事でも余裕で参考になるのですが、それでも例えば2003年の記事を読んだだけだと現在の状況にどこまで適用してよい話なのか不安が残ります。

Oracleの公式ドキュメントをあたってみても、全てが日本語訳されているわけではなく、重い腰あげて読み込まないとなという気持ちがします。

いまの私の仕事は運用エンジニアということもあってJavaのアプリケーションコードを自分で書いてはいないのですが、重要な場面ではコードの細部まで追って対応できることが不可欠。その中でもデータベース周りはクリティカルに効いてくることが多いので、深く理解しておきたいと思っています。

Java(Servlet + JDBC(Connector-J))は相当速い

最近、下記のウェブアプリケーションのベンチマークが話題になりました。

http://www.techempower.com/benchmarks/

これを見ると、JavaのServletは常に上位にいます。 私自身の最近の仕事の経験からしても、Servlet+JDBCの組み合わせはかなりのスループットが出ます。チューニングしていくたびにどんどん速くなってきたし、まだ速くする余地がかなりあるんじゃないかと思っています。

ベンチマークのソースを見たのですが、DataSourceのオプションなどはかなり参考になります。

https://github.com/TechEmpower/FrameworkBenchmarks/

Javaでウェブアプリケーションを書くことが常にベストの選択とは思いませんが、レスポンスタイムとスループットを安定して出したい場合の選択としてはかなりいい線行ってると思います。これをもっと極めていきたいです。

そこで、シリーズものとして、JavaのデータベースライブラリであるJDBCについてまとまったものを書いてみたいと思います。JDBCの基礎から始めて、1台あたり1000req/secを50msec以内で安定して返し続けるサーバーに必要なチューニングまで、順にステップアップしながら自分自身の勉強を兼ねて書いていきます。

環境は、CentOS6 – Java7 – tomcat7 – JDBC5.1 – MySQL5.5

前提となる環境ですが、まずサーバーはMac OS X 10.7のローカルで開発しつつ適宜VagrantでCentoS 6.3を起動して使います。ある程度細かくベンチマークを取りたい場合はAWSでAmazon Linux(最新版)を使います。

Javaはバージョン1.7でOracle公式のJDKを使います。 アプリケーションサーバーはtomcatのバージョン7、JDBCドライバはmysql-connector-javaの5.1系です。 MySQLは5.5です。

まずはOracle公式やその他の入門記事に沿って簡単なウェブアプリを作っていきながらひと通りのJDBCの機能を使っていきます。

→最速ウェブアプリのためのJDBC再入門TOPへ