MyBatisのArrayTypeHandler使用時の注意点

MyBatisには、org.apache.ibatis.type.ArrayTypeHandlerがあって、java.sql.Arrayとのマッピングを行ってくれますが、パラメータ設定時と、結果取得時でマッピングが異なるので注意が必要です。

実際のコードを見たほうがわかりやすいので、現時点のコードを引用します。

public class ArrayTypeHandler extends BaseTypeHandler<Object> {

  public ArrayTypeHandler() {
    super();
  }

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
    ps.setArray(i, (Array) parameter);
  }

  @Override
  public Object getNullableResult(ResultSet rs, String columnName) throws SQLException {
    Array array = rs.getArray(columnName);
    return array == null ? null : array.getArray();
  }

  @Override
  public Object getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    Array array = rs.getArray(columnIndex);
    return array == null ? null : array.getArray();
  }

  @Override
  public Object getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    Array array = cs.getArray(columnIndex);
    return array == null ? null : array.getArray();
  }
}

パラメータ設定時(setNonNullParameterメソッド)では、java.sql.Arrayを パラメータとして求めますが、結果取得時(getNullableResultメソッド)だと、java.sql.ArrayからgetArrayしたもの(たとえばPostgreSQLのintの配列(int[])だと、Javaのint[])が返却されます。 そのため、同じEntityを使ってInsertとSelectを行うということができません。

java.sql.Arrayを生成する際に、配列のタイプを文字列で指定する必要があり、汎用的なTypeHandlerを作るのが難しいのでこのようになってしまっているのではと思いますが、そのあたりの理由を知っていないと、嵌りそうですね。

同じEntityを使いたい場合には、ArrayTypeHandlerを使わずに、そのARRAYの型に応じたTypeHandlerを独自に作ることになりそうです。TypeHandler自体はとてもシンプルなので、実装も簡単です。 参考までに、List<Integer>と、PostgreSQLのint[]をマッピングするTypeHandlerの実装を以前作ったので、リンクしておきます。

Spring Bootのspring-boot-starter-webでTomcatではなくJettyを使う

Spring Bootのspring-boot-starter-webのデフォルトだとTomcatがサーブレットコンテナとして組み込まれますが、設定を変えればJettyに切り替えられます。

configurations {
    compile.exclude module: "spring-boot-starter-tomcat"
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE")
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    // ...
}

Tomcatを除外して、Jettyを依存関係に追加するといった手順なのですが、除外の設定はconfigurationsではなく、dependenciesでも書けます。

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE") {
        exclude module: 'spring-boot-starter-tomcat'
    }
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    // ...
}

ただ、この方法では注意が必要で、他の依存関係の設定でTomcatが指定されていると、除外できていないことになります。

たとえば、下記の例だと、spring-boot-starter-thymeleafspring-boot-starter-webに依存しているので、そこからのルートでTomcatを引き込んでしまいます。

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web:1.4.3.RELEASE") {
        exclude module: 'spring-boot-starter-tomcat'
    }
    compile("org.springframework.boot:spring-boot-starter-jetty:1.4.3.RELEASE")
    compile('org.springframework.boot:spring-boot-starter-thymeleaf')
    // ...
}

なので、除外するときには、Springの公式サイトにあるとおり、configurationsで書いたほうが確実です。

どのような依存関係になっているかは、dependenciesタスクで確認できますので、そちらで意図した形になっているか確認してみるのが良いかと思います。

Amazon Product Advertising APIを使ってISBNから書籍情報を取得する

ISBNから書籍の情報を取りたかったので、Amazon Product Advertising APIを使ってみました。

必要な情報

Amazon Product Advertising API を使うためには、下記の情報が必要になります。

  • アソシエイトタグ
  • アクセスキー
  • シークレットキー

アソシエイトのアカウントが必要で、取得方法は下記などが参考になります。

実装方法

より簡単にということで、makingさんの下記ライブラリを利用し、Javaで書いてみました。

mavenリポジトリで公開されているので、利用も簡単です。

build.gradleで下記のように依存関係を追加します。

apply plugin: 'java'

repositories { jcenter() }

dependencies {

    compile 'org.slf4j:slf4j-api:1.7.21'
    compile 'ch.qos.logback:logback-classic:1.1.7'

    compile group: 'am.ik.aws', name: 'aws-apa', version: '0.9.5'
}

Amazon Product Advertising API へ接続するための情報は、aws-config.propertiesに記載し、クラスパスに配置します。

aws.endpoint=https://ecs.amazonaws.jp
aws.accesskey.id=<Your Accesskey ID for AWS>
aws.secret.accesskey=<Your Secret Accesskey for AWS>
aws.associate.tag=<Associate Tag>

ISBNを指定して情報を取得する場合は下記のように書きます。 (ISBNの場合には、SearchIndexを指定する必要があります)

public class BookLookupClient {

    public Optional<Item> lookup(String isbn) {

        ItemLookupRequest request = new ItemLookupRequest();

        request.setSearchIndex("Books");
        request.getResponseGroup().add("Large");
        request.setIdType("ISBN");
        request.getItemId().add(isbn);

        AwsApaRequester requester = new AwsApaRequesterImpl();
        ItemLookupResponse response = requester.itemLookup(request);

        if (response.getItems().isEmpty()) {
            return Optional.empty();
        }

        return response.getItems().get(0).getItem().stream().findFirst();
    }
}
public class BookLookupClientTest {

    @Test
    public void testLookup() {

        BookLookupClient client = new BookLookupClient();

        Optional<Item> item = client.lookup("489471499X");

        assertTrue(item.isPresent());

        ItemAttributes itemAttributes = item.get().getItemAttributes();
        assertEquals("489471499X", itemAttributes.getISBN());
        assertEquals("Effective Java 第2版 (The Java Series)", itemAttributes.getTitle());
        assertEquals("ピアソンエデュケーション", itemAttributes.getPublisher());
    }
}

リクエストで設定すべき内容と、レスポンスの内容は、下記を参考にしました。

取りたい情報を辿るのがちょっと面倒でしたが、試すのはすごく簡単でした。

Spring BootでAssertJの3系を使う

Spring Boot(現在の最新の1.4系)はJava7もサポートしているため、spring-boot-starter-testで依存するAssertJは、Java8対応の3系ではなく、2系となっています。

3系に変えたい場合には、AssetJ3系を依存関係に追加するだけです。 (Gradleの依存関係の解決で、同じライブラリで複数のバージョンがあった場合は、デフォルトだと最新のものが利用される)

dependencies {
    testCompile('org.springframework.boot:spring-boot-starter-test')
    testCompile('org.assertj:assertj-core:3.6.1')
}

これでAssertJの3系で追加されたラムダ対応のメソッドが利用できます。

List<String> names = Arrays.asList("Taro", "Hanako", "Jiro");

assertThat(names)
    .filteredOn(x -> x.contains("ro"))
    .containsExactly("Taro", "Jiro");

なお、extractingは、AssertJの2系でもラムダ使えます。 これは、下記のようなメソッドが定義されていて、Extractorが1つしかメソッドを持たないインタフェースとなっているためです。(ラムダで書けるのは、抽象メソッドが1つだけ定義されたインタフェース)

public <V> ListAssert<V> extracting(Extractor<? super ELEMENT, V> extractor) 
public interface Extractor<F, T> {
  T extract(F input);
}

Spring Boot DevToolsを使って、コードを修正して確認といったサイクルを短くする

Spring Boot DevTools を使うことによって、コードを修正して確認といったサイクルを短くすることができます。

使い方

build.gradlepom.xmlで依存関係を追加するだけです。

dependencies {
    compile("org.springframework.boot:spring-boot-devtools")
}

Automatic restart

クラスパス上のファイルが変更されると、自動でアプリケーションを再起動してくれます。

Thymeleafのテンプレートなど、もともと再起動が必要ではないのが変更されても再起動されません。 なお、Spring Boot DevToolsを使うと、Thymeleafのキャッシュも無効(spring.thymeleaf.cache=falseと同じ)となりますので、再起動しなくてもキャッシュが効いていて最新にならない、、といったことはありません。

LiveReload

Automatic restartでアプリケーションが常に最新の状態になっても、ブラウザをリロードしないと表示内容は古いままです。

LiveReloadは、アプリケーションが再起動されたら、ブラウザも自動でリロードしてくれる機能です。 動作させるためには、ブラウザにアドオンを入れる必要があります。

アドオンを入れると、アイコンが追加されます。対象の画面を表示した状態でアイコンを押下すると、LiveReloadが有効になります。

f:id:onozaty:20170108233403p:plain

Thymeleafのテンプレートなど、Automatic restartの対象とならないものでも、変更されたタイミングで検知してくれます。

これでコードを書けば勝手にブラウザに最新の状態のものが表示されるようになります。便利ですね!

2016年の振り返り

すでに2017年を迎えてしまいましたが、、、2017年を迎えるにあたって、まずは2016年を振り返りたいと思います。

ダメだったこと

  • Blogで毎週1エントリを目標にしていたけど、7月で途絶えてしまった
  • 週に1度はGitHubにコミットすることを目標にしていたけど、3分の2程度しか出来なかった
  • 体重が5キロも増えた(お腹まわりがヤバイ)

良かったこと

  • Redmineの稚拙Plugin(View customize)の利用者が目に見えて増えてきていて、カスタマイズ例も貯まってきた
  • 社外の勉強会で30分も話すという経験ができた (社内はLTで2回)
  • 好きなアーティストのコンサートに2つも(5 Seconds Of Summer, Justin Bieber)行けた
  • 個人の時間を使って新しいことを試して、それを仕事でも生かすことができた

2017年に向けて

アウトプットを継続できるように、今年もがんばっていこうと思います。まずはSpringで出来ることをもっと知りたいので、そこを引き続き勉強しようと思っています。

あと本厄なので、いろいろ気をつけようと思います。(前厄の時点でいろいろ起きたので…)

今年もよろしくお願いします。

MyBatisのMapperはGroovyで書くことにした

MyBatisのMapperでSQL書くにあたって、複数行に渡るSQL書くのにヒアドキュメントが使いたくて、まずはKotlinを試しました。

ただ、ElicpseでKotlinを書こうとすると、importの自動補完が効かず、ちょっと面倒だったので、Groovyを試しました。(本当は、KotlinのままIntelliJ IDEAが導入できればいいんですが、、)

package com.example.repository

import org.apache.ibatis.annotations.Delete
import org.apache.ibatis.annotations.Insert
import org.apache.ibatis.annotations.Mapper
import org.apache.ibatis.annotations.Param
import org.apache.ibatis.annotations.Select
import org.apache.ibatis.annotations.SelectKey
import org.apache.ibatis.annotations.Update
import org.springframework.stereotype.Repository

import com.example.domain.Customer

@Repository
@Mapper
public interface CustomerRepository {

    @Select("SELECT * FROM customers ORDER BY id")
    public List<Customer> findAll()

    @Select("SELECT * FROM customers WHERE id = #{id}")
    public Customer findOne(@Param("id") Integer id)

    @Insert("INSERT INTO customers(first_name, last_name, address) VALUES(#{firstName}, #{lastName}, #{address})")
    @SelectKey(statement = "call identity()", keyProperty = "id", before = false, resultType = int.class)
    public void insert(Customer customer)

    @Update("UPDATE customers SET first_name = #{firstName}, last_name = #{lastName}, address = #{address} WHERE id = #{id}")
    public void update(Customer customer)

    @Delete("DELETE FROM customers WHERE id = #{id}")
    public void delete(@Param("id") Integer id)

    @Select('''
      SELECT
        *
      FROM
        customers
      WHERE
        first_name LIKE '%${firstName}%'
      ORDER BY id''')
    public List<Customer> findByFirstName(@Param("firstName") String firstName)

    @Delete("DELETE FROM customers")
    public void deleteAll()
}

全体のコードは下記にあります。

ほとんどJavaで書いたときと同じになるのと、importの補完も効くので、今後MyBatisのMapperはGroovyで書こうと思っています。

ちなみに、他のファイル(MyBatisでも別ファイルにSQL書けます)にSQLを書くのではなく、ソース上に書きたいのは、やはり情報は一箇所にまとまっていたほうがわかりやすいと思っているからです。他のファイルに書くことによって、SQLの変更が楽になる(IDEでハイライトが利くとか)、、ってような話もありますが、きっと実行したSQLを貼り付けると思うので、どこに書いても正直同じだと思いますし、だったらコードと一緒に見えたほうがわかりやすいと思っています。(Javaがヒアドキュメント使えていれば、他のファイルにSQL書くような流れって、もっとすくなかったのでは)

DomaはIDE上でメソッドに対応するSQLファイルに飛べるので、そういったストレスは少なくていいですね。