spring-boot + 関連プロダクト 細かいけどはまった箇所

はまる度に追記予定

# spring-boot

## spring-boot 1.3.3からmessages.propertiesが必須になった理由

messages-*.propertiesだけだとエラーになる

理由は以下に書いてある
> https://github.com/spring-projects/spring-boot/issues/4930

## warプラグインを入れると`gradlew bootRepackage`でjarファイルが生成されなくなる

`providedCompile`を使いたい等の理由でwarプラグインを入れた場合上記の問題が起こる

### 解決策
> http://stackoverflow.com/questions/33432236/how-to-make-bootrepackage-depends-on-jar-not-war-when-using-gradle-war-plugin
> https://github.com/spring-projects/spring-boot/issues/3931

#### 追記
provided使いたいだけならpropdeps-pluginを入れたほうが良い
https://github.com/spring-projects/gradle-plugins/tree/master/propdeps-plugin

## fully-executable jar での起動方法

導入は下記リンクでできるけど、

http://docs.spring.io/spring-boot/docs/current/reference/html/deployment-install.html

`/usr/local/myapp`以下の作業のみで完結させたい（`/etc/init.d/`以下をあまりいじりたくない）場合さらにシンボリックリンクを作ってバージョン名付きのjarファイルを参照するようにする場合は

```bash:/etc/init.d/myapp
# jarファイルへのシンボリックリンクへのシンボリックリンク
/etc/init.d/myapp -> /usr/local/myapp/myapp.jar
```

```bash:/usr/local/myapp
# 古いjarファイル
myapp-0.0.9.jar
# myapp.conf だと設定が読み込まれない
# "最終的に参照されるjarファイル名".conf にする必要がある
myapp-1.0.0.conf
myapp-1.0.0.jar
myapp.jar -> /usr/local/myapp/myapp-1.0.0.jar
```

起動スクリプト自体はjarファイルに埋め込まれているので

```bash
head -n 200 myapp.jar
```

とかで参照できる

## メールのヘルスチェックが実行されてしまう

どのバージョンからからは自信なし（1.5.4.RELEASE以降かも）

### 原因

`org.springframework.boot.actuate.autoconfigure.HealthIndicatorAutoConfiguration.MailHealthIndicatorConfiguration`

を見れば分かるけど、

```yaml
spring:
  mail:
    host: myhost
    port: 25
```

が適当に設定されていた為（実際は`org.springframework.mail.javamail.JavaMailSenderImpl`を使わずにアプリケーション固有の設定値を使って接続していた）

### 対処方法

#### `spring.mail.*`を適切に設定する
spring-bootのやり方に乗るのがベスト

#### 設定を切る

`spring.mail.*`を除去

または

```yaml
management:
  health:
    mail:
      enabled: false
```

# spring-mvc + thymeleaf

## Long型を`@PathVariable`に持つコントローラーのURLを`${#mvc.url()}`で解決できない

```java
@Controller
@RequestMapping("/hoge")
public class HogeController {

  @RequestMapping(path = "{id}", method = RequestMethod.GET)
  public String detail(@PathVariable("id") Long id) {
    return "hoge/detail";
  }
}
```

みたいな（Long型を引数に持つ）コントローラーがある時に

```html
<a th:href="${#mvc.url('HC#detail').arg(0, __${id}__).build()}">xxx</a>
```

でURLを解決できない。

### 解決策

```html
<a th:href="${#mvc.url('HC#detail').arg(0, new Long(__${id}__)).build()}">xxx</a>
```

そもそもの話だけどクラス名のリファクタに弱いから`#mvc.url()`は使わないほうがいい（と後になって思った）

## `@ControllerAdvice`を付けたクラス内にある`@ModelAttribute`を付けたメソッドが複数回呼ばれる

### 原因

`@RestController`へのアクセスも`@ControllerAdvice`で処理されるため

### 解決策

```java
@ControllerAdvice("com.example.controller")
class GlobalMvcController {
}

@ControllerAdvice("com.example.api")
class GlobalRestController {
}
```

みたいにbasePackageをちゃんと指定する。
他にも適用範囲を指定できるオプションが結構あるので、特定の複数のコントローラーのみで共通化の処理を書くのに便利

## コントローラー側でのURLの解決法

以下のようなコントローラーがあった場合

```java:ProductController.java
@RequestMapping(path = "/products/{id}", method = RequestMethod.GET)
public String detail(@PathVariable("id") Long id, Model model) {
  model.addAttribute("product", service.findProduct(id));
  return "product";
}
```

上記パスへリダイレクトさせるURLを生成するコードはだいたいこんな感じ（他にもやり方はあるけど）

```java
// パスの変更が不安
String.format("redirect:/products/%d", id);
// クラス・メソッド名の変更・後になって名前が重複するとかあるので危ない
"redirect:" + MvcUriComponentsBuilder.fromMappingName("PC#product").arg(0, id).build()
```

試行錯誤した結果、現時点での個人的にベターな方法が以下

```java:ProductController.java
@RequestMapping(path = "/products/{id}", method = RequestMethod.GET)
public ModelAndView detail(@PathVariable("id") Long id) {
  ModelAndView mav = new ModelAndView("product"); 
  mav.addObject("product", service.findProduct(id));
  return mav;
}
```

```java
// 長くてなんかイヤだけどMvcUriComponentsBuilderはstatic importすると短くなる
"redirect:" + MvcUriComponentsBuilder.fromMethodCall(MvcUriComponentsBuilder.on(ProductController.class).detail(id)).toUriString();
```

`MvcUriComponentsBuilder#fromMethodCall()`を使うことによる

- 利点
 - URL、クラス名、メソッド名の変更に強い
 - 引数の変更に強い
- 欠点
 - メソッドの戻り値に`ModelAndView`を使わざるをえない


# spring-data-jpa / Hibernate

## 共通

### like句に渡す文字列はエスケープする

```java
@Repository
public interface ProductRepository extends JpaRepository<Product> {
  @Query("select p from Product p where p.name like '%?1%'")
  public List<Product> findProducts(String q);
}

@Service
public class ProductService {

  // ...

  public List<Product> ng() {
    return repository.findProducts("アルコール_40%");
  }

  public List<Product> ok() {
    return repository.findProducts("アルコール\_40\%");
  }
}
```
### Date and Time APIを使う方法の変遷

#### Hibernate 5.1.x 以前
```java
// org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters
@EntityScan(basePackageClasses = {Jsr310JpaConverters.class})
```
または

```groovy:build.gradle
compile("org.hibernate:hibernate-java8")
```

#### Hibernate 5.2.x 以降
特に何もしなくてOK

## 静的クエリ（JPQL, HQL）

### なんか知らないけどcross joinするSQL文が生成されてる

#### 原因

```sql
select a from A a where a.deep.entity.reference.foo = ?1
```

みたいなJPQLクエリ。

#### 解決策

冗長に感じるけど以下のように明示的にjoinすればcross joinされなくなる

```sql
select 
  a from A a 
  join a.deep d 
  join d.entity e 
  join e.reference r 
where 
  r.foo = ?1
```

## 動的クエリ（Criteria API, Specification）

### 同一Entityに対して`From#join()`を複数回呼ぶと生成されるSQL文でも複数回Joinが入る

```java:UserSpecifications.java
/**
 * userを関連するdepartmentテーブルの内容で検索する
 */
public Specification<User> search(UserSearchCondition condition) {

  return (root, query, cb) -> {
    
    List<Predicate> predicates = new ArrayList<>();

    condition.departmentName()
      .map(dn -> cb.like(root.join(User_.department).get(Department_.name), '%' + dn + '%')
      .ifPresent(predicates::add); /* ここでdepartmentをjoin */

    condition.departmentAddress()
      .map(da -> cb.like(root.join(User_.department).get(Department_.address), '%' + da + '%')
      .ifPresent(predicates::add); /* ここでdepartmentをjoin (2回目) */

    // predicatesが空の時は本当は考慮しないといけないけど省略
    return cb.and(predicates.toArray(new Predicate[predicates.size()]));
  }
```

#### 解決策
Fromからjoinを検索し、なければjoinして返却
※ クラスのフィールドに`Join<?, ?>`をキャッシュさせる方法は使えない

`Jpa Static MetaModel`を使っている場合は以下のようなユーティリティメソッドを作る

```java
public static <X, Y> Join<X, Y> getOrCreateJoin(From<?, X> from,
    SingularAttribute<? super X, Y> attribute, JoinType joinType) {

  for (Join<X, ?> join : from.getJoins()) {

    boolean sameName = join.getAttribute().equals(attribute);

    if (sameName && join.getJoinType().equals(joinType)) {
      return (Join<X, Y>) join;
    }
  }

  return from.join(attribute, joinType);
}
```

> 以下を参考
> http://stackoverflow.com/questions/21791793/query-from-combined-spring-data-specification-has-multiple-joins-on-same-table
> 
> org.springframework.data.jpa.repository.query.QueryUtils

## spring-data-jpaで`@EmbeddableId`なEntityの保存で起こる問題

新規に永続化する際に`EntityManager#persist()`ではなく`EntityManager#merge()`がコールされる

### 原因
`org.springframework.data.jpa.repository.support.SimpleJpaRepository#save(S)`
↓
`org.springframework.data.repository.core.EntityInformation#isNew`
が`false`で返るため

### 対応方法

[DATAJPA-440](https://jira.spring.io/browse/DATAJPA-440) に記述があるが、
`org.springframework.data.domain.Persistable`を適切に実装する

が、以下のような実装にするとHibernate側の内部実装に起因するエラーでコケるので注意する（かなりハマった）

```java
@Entity
public class MyEntity implements Persistable<MyEntityId> {

  @EmbeddedId
  private MyEntityId;

  @Override
  public boolean isNew() {
    return false;
  }
}

// 作成時のコード
void create() {

  // trueを返せばいいのは作成時だけなので
  // 匿名クラスでオーバーライドすればいいじゃんという考え
  MyEntity e = new MyEntity() {
    @Override public boolean isNew() {
      return true;
    }
  };

  // ...

  reoisutory.save(e);
}
```

どうやら`@Transient`を使って`isNew()`の返り値を制御するのが正しいやり方らしい

```java
@Entity
public class MyEntity implements Persistable<MyEntityId> {

  @Transient
  private boolean isNew = false;

  @EmbeddedId
  private MyEntityId;

  @Override
  public boolean isNew() {
    return isNew;
  }
}

// 作成時のコード
void create() {

  MyEntity e = new MyEntity();
  e.isNew = true;

  // ...

  reoisutory.save(e);
}
```

# Flyway

## 本番環境(非Windows)で取得したダンプをローカル(Windows)にインポートするとその後のbootRunで落ちる

### 原因
プラットフォームでchecksumの算出結果が異なる為

### 解決策
3.2.1以前なら手動でchecksumを入れ直す

> 4.0.0で修正されるらしい
> https://github.com/flyway/flyway/issues/253

# Test

## `ApplicationEventPublisher`を`@MockBean`できない

### 原因
（現在の）spring-frameworkの制約（らしい）

### 解決策
テスト時に以下のような設定を読み込む（つまり1.3.x時代と同じ）

```java
@Configuration
public class MockEventPublisherConfig {

  @Bean
  @Primary
  public ApplicationEventPublisher eventPublisher() {
    return Mockito.mock(ApplicationEventPublisher.class);
  }
}
```

```java
@SpringBootTest(classes = {MockEventPublisherConfig.class, ...})
public class MyTest {
  
  @test 
  public void test() {
    ...
  }
}
```

### 参考

- 対応されるかもしれないissue
  - https://jira.spring.io/browse/SPR-14335

### 追記
- spring-data-commonsが内部的に依存している`ApplicationEventPublisher`には適用されなかった
  - `org.springframework.data.repository.core.support.EventPublishingRepositoryProxyPostProcessor`
- そもそもApplicationEventPublisherをモックするよりはリスナー側をモックにして呼び出し確認する方が良いと思ったので方針を変更した