groovyを高速起動するgyコマンド
...を作った。 作ったといっても既存のgroovyコマンドを改造しただけなんだけど。
背景
groovyを高速に起動するにはgroovyservというものがある。これは初回起動は遅いがその後はgroovyの起動が爆速になるというものだ。
でも、使い勝手が今ひとつだった。 例えばgroovyservを使って、スクリプトで書いたサーバを起動しCtrl-cで停止すると、ポートが解放されなかったりしたためだ。
ちなみに、初回起動でサーバを起動し次回からはそのサーバで実行することでコマンドの起動を高速化する方法はNailGunやGradleでもやっていたり結構メジャーなようだ。
だが、この方法以外に高速化の方法がないのだろうか、ってことで調べたらあった。下記リンクによるとJVMの設定でClojureを高速に起動できるらしい。ならgroovyだって高速に起動できるはずだ。
http://tnoda-clojure.tumblr.com/post/51495039433/jvm-clojure-30
ということで、高速化オプションを設定してJVMを起動するようにgroovyコマンドを改造した。
追加したjava起動オプション
- -client
- -Xverify:none
- -XX:+TieredCompilation
- -XX:TieredStopAtLevel=1
インストール手順
※シェルスクリプトなのでwindowsだと動かない(cygwinなら動くが)。
機能制限
少しでも高速化するために起動時に~/.groovy/setupファイルは読み込まないようにした。なので、grapeのプロキシ設定とかをsetupファイルに書いている場合は通常のgroovyコマンドを使うしかない。
その他
プロキシサーバ経由でGroovyのGrapeを使う
会社ではプロキシ経由じゃないとインターネットに繋げない。 なので、Grapeを使うときもプロキシの設定が必要だ。
Grapeでプロキシを使う場合は2通りある。 公式によるとgroovyコマンドを使うときにオプションを付けるか、環境変数JAVA_OPTSを設定するかだ。
groovy -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080 yourscript.groovy
JAVA_OPTS = -Dhttp.proxyHost=yourproxy -Dhttp.proxyPort=8080
でもこのJAVA_OPTSはどこに書いたらいいんだろうか。groovyにだけこの環境変数を適用したいんだけど。
そんな場合は ~/.groovy/startup ファイルに書く。 groovyコマンド実行時にそのスクリプトを自動的に呼び出すようになっている。 なので下の記述をstartupファイルに書けばいい。
JAVA_OPTS="-Dhttp.proxyHost=proxy.example.com -Dhttp.proxyPort=8080 -Dhttp.proxyUser=user =Dhttp.proxyPassword=password"
ちなみに、windowsのcygwin環境でgroovyを実行する際、startupファイル内で改行するには注意が必要。 改行コードが¥r¥nになっていると、¥rのせいで文字が消える。 このせいでめちゃくそハマった。
コメントは書かなくていい Javadocを書いてくれ
会社のとあるプロジェクトでコーディングの可読性に関する勉強会を実施してるらしい。
そのプロジェクトはリリースは終わって保守に移行してんだけど今更そこ勉強してんのって感じが半端ない。新人向けか?
そのプロジェクトはもちろん炎上しまくってた。今も炎上してると思う。
さて、その勉強会の資料を見る機会があってだいたいはごくごく当たり前のことを書いてた。メソッドの行数とかそういうの。
でも資料の中にコメントを書けっていう項目があってJavadocを書けって言う項目がなかったのに驚きを隠せなかった。
コメントなんかよりJavadocの方が大事なんだってことがわかってないみたい。
というか、そのプロジェクトではJavadocを書かない主義だったらしい。全く意味がわからない。恐ろしい。。。
と、前置きが長くなってしまったが、 コメントを書くよりJavadoc(privateメソッドも含む)を書く方がいいんだってことをざっとまとめてみようと思う。
Javadocの方がコメントよりも何を書くべきかがはっきりしている
Javadocの方がフォーマットが決まっているし、メソッド全体についてのコメントを書くようなものなので、 意味のない情報を書くことが少ない。
意味のない情報って何か?例えばこういうの。
i++; // 値を1だけ増加させる
こういうのを書いてしまうのはコード一つ一つに対してコメントが必要だと思っているから。 たとえコメントは重要なことだけ書けって言われたって、何が重要なのかは経験積まないとわかんないと思う。
Javadocの方がコメントがまとまっているのでコードが読みやすい
コードの途中にコメントが散在しているのをよく見かける。
これすごく読みづらい。特にコメントをコードの横に書いていないものは。
例えばこういうの。
// オブジェクトの数だけループする for (int i = 0; i < objs.length; i++) { // オブジェクトが有効である場合 if (objs[i].isValid()) { // 実行用のメソッドを呼ぶ objs[i].exec(); } }
Javadocだと別途ドキュメントを生成できる
ソースコードから仕様書を作成できるって言うのがJavadocの魅力だよね。
privateメソッドであっても別途ドキュメントできるのはいいことだ。
仕様書とソースコードを別々に管理したがる人っているんだろうか?
管理が大変だからできるだけソースコード中に仕様書を書くようにすべきだと思う。
もちろん基本設計仕様とか上流のものなら別文書で管理してもいいけど。
最近読み返していた人月の神話にも仕様書は別管理すると大変だからソースコードに書けみたいなことが書いてあった。
Javadocを書くことを意識した方が良いコードが書ける
コメントは多すぎても少なすぎてもいけない。 コメントが多い人の特徴はコードがまとまってなくてコードがわかりにくいためそれをコメントで補うということをしてる。 また、コメントを大量に書く人はメソッドを長く書く傾向にあると思う。
例えばこういうの。
public void doSomething() { // xxxオブジェクトから繰り返しデータを取得する // 取得するのはこれこれの理由があるため ... ... ... // oooオブジェクトにデータを設定する // 設定するのはこれこれの理由があるため ... ... ... // fooとbarを実行する ... ... ... }
普通は各コメントに書いている処理をprivateメソッドに切り分けるべきなんだが、 コメントを書いているからということでprivateメソッドに切り分けることを横着してる。 こういう場合はprivateメソッドに切り分けてきちんとJavadocを書かないといけない。
まとめ
publicメソッドは基本的にはJavadocが必須(もちろん例外もある、例えばgetter/setterとか)でprivateメソッドは任意。 でも、コメントを書くぐらいならprivateメソッドでもJavadocを書いた方がメリットは大きい。 だから、なるだけJavadocを書く、ということを意識した方がいい。 もちろんコメントを書いたほうがいい場合もある。だから優先度的には下のような順かな。
Java8のラムダ式をわかりやすく解説
できるかどうかわからんがやってみる。
きっかけは会社の人がJava8のラムダ式は難しいと言っていたから。
確かに関数型言語をいきなりJavaから学ぶのは難しいんじゃないかな。
なぜ難しく感じるのかというと、Javaはオブジェクト指向言語を前提に設計しているのに関数型言語の概念を無理矢理ねじ込んだから。 Javaは良くも悪くも互換性を大事にするから、既存の構成を崩さず関数型を利用できるようにすると使い勝手が悪くなる。 元からオブジェクト指向と関数型の両方の概念をベースに設計していたらもっとわかりやすくなってたと思うけど。
さて、本題。
Javaのラムダ式を説明するにあたって、Groovyを比較にして説明しようと思う。
なぜGroovyか?Javaに近いし、ラムダ式の概念がわかりやすいから。
Groovyだとラムダ式はこう書ける。
// 定義 Closure increment = { x -> x + 1 } // 呼び出し increment(1)
Javaだとこうだ。
// 定義 Function<Integer, Integer> increment = (x) -> { return x + 1; }; // 呼び出し increment.apply(1);
まあ、ほとんど同じだ。
ただ、Groovyと違ってJavaにはapplyとかがあって、なんじゃこりゃって思うんじゃないだろうか。
GroovyではClosure型がラムダ式を構成する。そしてClosureはブロック{}のことだ。
このブロックってどこかで見たことがないだろうか。そう初期化子だ!...じゃなくてメソッドだ!
つまり、メソッドの中身がClosureってことだ。メソッドの中身をオブジェクトとして使える、これが関数型言語なんだ。
なのでClosureはメソッドと同じように引数を受け取れる。コードで言うxがそれだ。
また、Closureを呼び出す場合もメソッドと何ら変わらない。引数に値を入れて呼び出すだけだ。
一方JavaはClosure型じゃなくてFunction型で、ジェネリクスを使ってたりする。
Groovyは動的型付けなので引数や戻り値の型は書かなくて良かった。
でも、Javaは静的型付けなので引数や戻り値の型は書かないといけない。
Groovyのラムダ式の構文はブロック{}と引数のための矢印->だけで良かったが、Javaはもう少し複雑だ。
引数に括弧()が必要なのとブロック{}の位置が違う。まあそれだけなん。
これがJavaのラムダ式の基本形で、引数が一個だけだったら括弧()を省略できたり、文が一文だけだったらブロック{}とreturnを省略できたりする。GroovyはメソッドでもClosureでもreturn文を省略できるんだけどね。
そして、謎のapplyだ。これは一体なんなのか?
ここがJavaをオブジェクト指向から関数型のパラダイムを可能にした際のひずみだと俺は思う。
Javaには関数を表現するオブジェクトなんてものはない。ただ単に関数型のインターフェースというものがあるだけだ。
そのインターフェースでラムダ式を表現しようとしてるだけだ。
つまり、上のJavaのコードはこういうことだ。
Function<Integer, Integer> increment = new Function<Integer, Integer>() { @Override public Integer apply(Integer x) { return x + 1; } };
これはいわゆる無名クラスだ。つまり、Javaのラムダ式の正体はなんてことはない、ただの無名クラスだったということだ。
それをもう少し簡潔に書ける文法で表現したのがラムダ式っていうものなんだ。
だからapplyっていうのはラムダ式で暗黙で作った無名クラスのapplyメソッドを呼んでいるだけなんだ。
こういったことから、Javaのラムダ式はGroovyとは違って今のJavaの仕様に無理矢理当てはめたものという感じがしないでもない。
Javaのややこしさはもう少し続く。それはラムダ式を表現する型にある。
つまり、上で言うFunction型だ。これは一つの引数と一つの戻り値の関数を表す型である。
じゃあ、一つの引数は取るけど戻り値は必要ない場合はどうすりゃいいんだろうか。
もちろんFunction型で表現できなくもない、要は戻り値をVoid型にすればいいんだから。
でもJavaにはそれ専用の型があるんだよね、ややこしいことに。
それはConsumer型だ。
じゃあ、戻り値だけ受け取りたい場合は?Supplier型だ。引数を2つ受け取りたい場合は?BiFunction型だ。
...というように型がいっぱいある。一方GroovyではClosure型だけで表現できる。
以上のように、Groovyと比較することでラムダ式の理想(Groovy)と現実(Java)が見えてJavaのラムダ式の理解が深まったかな?と思う。Javaも本当はGroovyみたいなラムダ式を書けるようにしたかったんだと思うんだけど、いかんせん互換性の問題からこんな形になってしまったんだと思う。
なんかGroovyを持ち上げてるような文章になってしまった。
別にC#と比較してもいいんだけど、C#は俺はわからん。
Lispだと説明が複雑になるしなあ。
インデックス設計の基本方針
インデックスの最適な定義の仕方がわからん。
会社では、見た感じ検索で使うカラムは何でもかんでもインデックスを作成しているようだ。 こんな感じで闇雲にインデックスを作成するアンチパターンをSQLアンチパターンでは「インデックスショットガン」と命名している。いいネーミングセンスだと思う。
このパターンの場合の解決策はEXPLAIN文などを使ってきちんとインデックスを評価しろってことだ。 まあそりゃそうなんだけど、めんどくさい。なんか基準がないのかということで、考えてみた(つーか調べた)。
インデックスを作成した方がよいもの
- 検索でよく利用されるカラム。NOT NULLであり、LIKEを使って後方一致や部分一致で検索していないもの(前方一致は問題ない)。あと否定とかORを使っていないもの
- ORDER BYでよく利用するもの(B-Treeインデックスのように木構造だとソート済みなので効率よく使える)
- JOINで使うカラム
インデックスを作成してはいけないもの
- 主キー(ほとんどのDBで自動的に作成されるため)
- 外部キー(ほとんどのDBで自動的に作成される?)
- Enumのカラム(値の分布が小さいためインデックスの恩恵が得られないにもかかわらず容量を食うため)
正直Enumに関しては自信がないが、たぶん作成しない方が効率的なんじゃないかな。
MyBatisでEnumを使う
MyBatisでEnumを使うにはどうすればいいのか。
もちろんマニュアルに書いてあるが、まとめるとこうだ。
具体例をマニュアルから引用すると
<resultMap type="org.apache.ibatis.submitted.rounding.User" id="usermap2"> <id column="id" property="id"/> <result column="name" property="name"/> <result column="funkyNumber" property="funkyNumber"/> <result column="roundingMode" property="roundingMode" typeHandler="org.apache.ibatis.type.EnumTypeHandler"/> </resultMap> <!-- mybatis-config.xml --> <typeHandlers> <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/> </typeHandlers>
この方法、resultMapはいいけどparameterを使うときにわざわざmybatis-config.xmlにhandlerを定義するっていうのはめんどくさくないか。俺はめんどくさい。
なので、mybatis-config.xmlにhandlerを定義しなくて済む方法がないか探した。 結果、あったにはあったが正攻法じゃないっぽいので注意が必要。
要はtypeHandlerの代わりにordinalを使うってことだ。
つまり、これを
#{roundingMode} <!-- mybatis-config.xml --> <typeHandlers> <typeHandler handler="org.apache.ibatis.type.EnumOrdinalTypeHandler" javaType="java.math.RoundingMode"/> </typeHandlers>
こうする
#{roundingMode.ordinal}
ちなみにordinal()だとうまくいかない。 どうやら、#{}の中ではReflectionでフィールドを参照することができるらしい。
上の例ではEnumOrdinalTypeHandlerの場合だが、EnumTypeHandlerを使いたい場合はordinalを書かなくていい。型がEnumだとデフォルトでEnumTypeHandlerを使うから。
あと、parameterが1つだけでそれがEnumの場合は@Paramつけた方がいい。つけないとうまく動かなかった。
JPAとN+1問題
JPAとは
JPA(Java Persistence API)とはJavaEEのために定義された永続化(persistence)に関するAPI仕様です。JPAはAPI仕様なのでJPA単体では動きません。JPAを実装したHibernateやEclipseLinkなどのO/R Mapperが必要になります。
N+1問題とは
N+1問題とはO/R Mapperを利用した際にSQL文が意図せず大量に発行されてしまう問題です。この問題はO/R MapperによるSQL文の自動生成に起因します。Object側の関連も含めてデータを取得する場合に、O/R Mapperは下記のようにN+1回SELECT文を発行してしまいます。
- TableからN個のRecordを取得するためにSELECT文を1回発行
- N個のRecordが関連するデータを取得するためにSELECT文を1回ずつ、計N回発行
N+1問題の解決策
N+1問題を解決するにはテーブルをJOINしてデータを取得する必要があります。具体的な解決策は大きく分けて2つです。
- JPQLでJOIN FETCHを使用する
- @Fetchアノテーションを使用する
ただし、後者の方はJPAの仕様にはなく、実装であるHibernateに依存します。EclipseLinkの場合は@JoinFetchを使用します。
注意:下記で説明する解決策はJPA + Hibernate環境の場合のものです。EclipseLinkを使用した場合でも同様の方法で解決できるとは限りません。
1. JPQLでJOIN FETCH を使用する
JPAでは下記の4つの方法でDBにアクセスすることができます。
- Entityのメソッド(persist(), find(), remove(), merge())
- JPQL(JPA用のSQL。SQL文の方言を吸収するためのもの)
- Criteria(Javaの型を利用してクエリを作成するためのもの)
- Native SQL
この中のJPQLを使った解決策です。JPQLでできることはCriteiaでもできますが、Criteriaでの解決方法はここでは触れません。
単方向関連の場合
N+1問題は関連があるために発生します。ここで言う関連はObject側の関連です。 具体的には下記のようなコードです。
public class Parent { private List<Child> children; ... } public class Child { ... }
上記ではParentだけがChildを参照しているため単方向関連と呼びます。さらにParentはChildをList型で複数持っているため、1対多(One to Many)の関連と呼びます。
単方向関連の場合にN+1問題が発生するのは、Parentと関連しているchildrenをO/R Mapperが取得するときです。したがって、Lazy Fetchの場合はParentオブジェクトを取得しただけではSELECT文は発行されません。Parentオブジェクトからchildrenを取得しようとした場合に初めてN回のSELECT文が発行されます。
JPQLで下記のようにJOIN FETCH
を使うことでN+1問題を解決できます。
SELECT p FROM Parent p JOIN FETCH p.children
このとき発行されるSELECT文は1回です。JOIN FETCH
はINNER JOINのSELECT文として発行されます。OUTER JOINを使用したい場合は下記のようになります。
SELECT p FROM Parent p LEFT OUTER JOIN FETCH p.children
双方向関連の場合
双方向関連とは2つのオブジェクトがお互いに参照している状態のことです。
具体的には下記のようなコードです。
public class Parent { private List<Child> children; // One to Many ... } public class Child { private Parent parent; // Many to One ... }
上記は1対多の関連ですが、Childから見た場合は多対1(Many to One)の関連と呼びます。
単方向関連の場合ではParentを取得した場合のN+1問題だけを考えれば良かったのですが、双方向関連の場合はChildを取得した場合のN+1問題も考えなければいけません。ChildはParentを保持しており、そのParentはChildを複数保持しています。したがって、Childが持つParentの関連もJOIN FETCH
で取得することでChildのN+1問題を解決できます。
SELECT c FROM Child c JOIN FETCH c.parent p JOIN FETCH p.children
2. @Fetchアノテーションを使用する
@Fetchアノテーションは下記のように使用します。
@Entity public class Parent { @OneToMany(mappedBy="parent") @Fetch(FetchMode.SUBSELECT) private List<Child> children; ... }
@Fetchには複数のFetchModeがあります。FechModeの種類によってSELECT文の発行数が異なります。
FetchMode | SELECT文の発行数 |
---|---|
SELECT | N+1 |
JOIN | 1 |
SUBSELECT | 2 |
FechModeの詳細は下記リンクを参照してください。
http://www.solidsyntax.be/lessons-learned/hibernate-fetchmode-explained-example/
ただし、FetchMode.JOINはJPQLを使用した場合は正しく動作しません。Criteriaを使用した場合は上記のように1回だけSELECT文を発行しますが、JPQLの場合はN+1回、SELECT文を発行してしまいます。これはHibernateの既知のバグのようです。
http://stackoverflow.com/questions/18891789/fetchmode-join-makes-no-difference-for-manytomany-relations-in-spring-jpa-reposi
@Fetchを使用するのであればFetchModeはSUBSELECTを選択した方が良いと思います。
ただし、SUBSELECTにも問題はあります。その問題はSUBSELECTを双方向関連で使用した場合に発生します。Many to Oneのオブジェクト(上記例ではChild)を取得しようとした場合、SUBSELECTを使用するとN+M+1回のSELECT文が発行されてしまいます。MはOne to Many(上記例ではParent)のレコード数です。
この現象はこのコードを使用して確認しました。おそらくそれぞれがお互いに関連を取得しようとしているのが原因だと思いますが、詳しくはわかりません。
まとめ
N+1問題を解決したい場合はJOIN FETCH
を使用すべきです。@FetchアノテーションはJPAでは定義されておらず、Hibernateのバグもあるので使用しない方が無難です。
また、可能であれば双方向関連のないクラス設計をする方が良いと思います。双方向関連は単方向関連に比べてN+1問題が発生しやすく、それを防ぐために考慮すべき箇所も増えるためコーディングが難しくなります。