Oracle 11 g + Spring boot を Vagrant + Docker compose で試してみたメモ

## やること

docker で javaのフレームワークの動作環境を試してみる。

## 動作環境

* windows10
* vagrant2.1.1
* virtualbox5.2.14
* ubuntu-18.04
* Docker version 18.05.0-ce, build f150324
* docker-compose version 1.21.2, build a133471


## ディレクトリ構造

```yaml
project
  + bin   # コマンド覚書用のシェルスクリプト
  + data  # dockerの永続化用ディレクトリ
  - docker
    - gradle
      - Dockerfile
    - oracle
      - 11.2.0.2
        - Checksum.xe
        - Dockerfile.xe
        - checkDBStatus.sh
        - rnOracle.sh
        - setPasword.sh
        - xe.rsp
        - oracle-xe-11.2.0-1.0.x86_64.rpm.zip # Oracleからダウンロードしたもの。.gitignoreで無視
      - buildDockerImage.sh
    - spring
      - Dockerfile
    - .env # docker-composer.yml で使用する環境変数設定
    - docker-compose.yml
  - src
    - hello # spring initializerで作成されたものをひながたに改変
      - build.gradle
      - settings.gradle
      - lib
        - ojdbc7.jar  # Oracleからダウンロードしたもの。.gitignoreで無視
      - src
        - main
          - java
            - hello
              - hello
                - model
                  - Staff.java
                - repository
                  - StaffRepository.java
                - HelloApplication.java
          - resouces
            - application.yml
        + test
```

## 設定ファイル

```:docker/.env
ORACLE_PWD=MY_DB_PASSWORD
USER_PASS=MY_DB_USER_PASSWORD
```

```yaml:docker/docker-compose.yml
version: '3'
services:
  dbserver:
    build:
      context: ./oracle/11.2.0.2/
      dockerfile: Dockerfile.xe
    volumes:
      - ../data/database:/u01/app/oracle/oradata
      - ./oracle/startup:/docker-entrypoint-initdb.d/startup
    ports:
      - 1521:1521
      - 8085:8080
    env_file: .env
    environment:
      - TZ=`ls -la /etc/localtime | cut -d/ -f8-9`
    shm_size: 1g
    restart: unless-stopped

  gradle:
    build: ./gradle
    user: gradle
    volumes:
      - ../src/hello:/app
      - cache1:/home/gradle/.gradle
    links:
      - dbserver
    environment:
      - TZ=`ls -la /etc/localtime | cut -d/ -f8-9`
    command: [echo, "no work"]

  spring:
    build: ./spring
    ports:
        - "8080:8080"
    volumes:
        - ../src/hello:/app
    links:
      - dbserver
    environment:
      - TZ=`ls -la /etc/localtime | cut -d/ -f8-9`
    command: [ "sh", "-c", "java $JAVA_OPTS -Djava.security.egd=file:/dev/./urandom -jar /app/build/libs/hello-0.0.1-SNAPSHOT.jar"]

volumes:
  cache1: 
```

## データベースの準備

[DockerでOracleデータベース11g XEを構築][*1]を参考にデータベースを設定した。
oracle/docker-images の [11.2.0.2](https://github.com/oracle/docker-images/tree/master/OracleDatabase/SingleInstance/dockerfiles/11.2.0.2)ディレクトリをコピー。
同じディレクトリに[oracle-xe-11.2.0-1.0.x86_64.rpm.zip](http://www.oracle.com/technetwork/jp/database/database-technologies/express-edition/downloads/index.html)のlinux版を配置。
また、javaからDBにつなげるための[ojdbc7](https://www.oracle.com/technetwork/database/features/jdbc/jdbc-drivers-12c-download-1958347.html)ドライバーをダウンロードしておく。

### githubからcloneするもの

* oracle/docker-imagesの OracleDatabase/SingleInstance/dockerfiles/11.2.0.2ディレクトリ
  * docker/oracle/11.2.0.2に配置

### Oracleからダウンロードするもの

* oracle-xe-11.2.0-1.0.x86_64.rpm.zip
  * docker/oracle/11.2.0.2/oracle-xe-11.2.0-1.0.x86_64.rpm.zipに配置
* ojdbc7.jar
  * src/hello/lib/ojdbc7.jar に配置

### データベース起動

```
docker-compose up -d dbserver
```

### sqlplusでの接続

データベースを立ち上げた状態でないと接続できない。
かならず、`docker-compose up`した後に行うこと。

```bash:bin/sqlplus.sh
#!/bin/bash

bin_dir=$(cd $(dirname $0) && pwd)
container_name=dbserver

# 環境変数読み込み
. $bin_dir/../docker/.env

cd $bin_dir/../docker && docker-compose exec $container_name sqlplus sys/$ORACLE_PWD@localhost:1521/XE as sysdba
```

シェルを実行して接続を試みる。

```
vagrant@vagrant[master]:/vagrant/tutorial/lesson/spring$ ./bin/sqlplus.sh
```

立ち上がっていない状態でアクセスすると以下のようなエラーがでる。

```
7df144c6f135        docker_dbserver              "/bin/sh -c 'exec $O…"   19 seconds ago      Up 18 seconds (health: start
ing)   0.0.0.0:1521->1521/tcp, 0.0.0.0:8085->8080/tcp   docker_dbserver_1
WARNING: The JAVA_OPTS variable is not set. Defaulting to a blank string.

SQL*Plus: Release 11.2.0.2.0 Production on Sun Jul 15 15:08:51 2018

Copyright (c) 1982, 2011, Oracle.  All rights reserved.

ERROR:
ORA-12528: TNS:listener: all appropriate instances are blocking new connections
```

 Enterを2回押して一旦終了させる。

```
Enter user-name:
ERROR:
ORA-12547: TNS:lost contact


Enter user-name:
ERROR:
ORA-12547: TNS:lost contact


SP2-0157: unable to CONNECT to ORACLE after 3 attempts, exiting SQL*Plus
```

もう一度sqlplusでの接続を試みる。

```
vagrant@vagrant[master]:/vagrant/tutorial/lesson/spring$ ./bin/sqlplus.sh
```

```
7df144c6f135        docker_dbserver              "/bin/sh -c 'exec $O…"   25 seconds ago      Up 23 seconds (health: start
ing)   0.0.0.0:1521->1521/tcp, 0.0.0.0:8085->8080/tcp   docker_dbserver_1
WARNING: The JAVA_OPTS variable is not set. Defaulting to a blank string.

SQL*Plus: Release 11.2.0.2.0 Production on Sun Jul 15 15:08:55 2018

Copyright (c) 1982, 2011, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Express Edition Release 11.2.0.2.0 - 64bit Production

SQL> 
```

sqlplusの立ち上げ方が悪いのだろうか？
もしうまい方法知っているかたがいれば教えていただけると助かります。

### ユーザの準備

sqlplusでつなぐことができたら、javaから接続するためのユーザの作成を行う。
今回は起動してみるテストだけで公開しないのですべての権限を持つユーザを作成。

```sql
CREATE TABLESPACE my_data DATAFILE '/u01/app/oracle/oradata/MY_DATA.dbf' SIZE 200M  SEGMENT SPACE MANAGEMENT AUTO;
CREATE USER testuser IDENTIFIED BY "my_pass" DEFAULT TABLESPACE my_data TEMPORARY TABLESPACE temp;
GRANT DBA TO testuser ;
quit;
```

※ここで設定したmy_passは`docker/.env`のMY_DB_USER_PASSWORDのこと。

一旦ログアウトしたら、作成したユーザでの接続を試みる。

```bash:bin/sqlplus.sh
#!/bin/bash

bin_dir=$(cd $(dirname $0) && pwd)
container_name=dbserver
. $bin_dir/../docker/.env
cd $bin_dir/../docker && docker-compose exec $container_name sqlplus testuser/$USER_PASS@localhost:1521/XE

```

### テスト用のテーブルの作成

```sql
create table STAFF (
    EMP_ID     number primary key,
    STAFF_NAME varchar2(100)
);
insert into STAFF (EMP_ID, STAFF_NAME) values (1, 'Jasmine');
```

## ビルドツールとソース

### ビルドツールのdockerファイル
```dockerfile:docker/gradle/Dockerfile
FROM gradle:4.8.1-jdk8
WORKDIR /app
```
### ソースの準備

[Dockerで始めるSpring Boot][*3]を参考に、[Spring Initializer](https://start.spring.io/)を使ってプロジェクトを作成。srcフォルダ以下に配置する。
今回はDBと接続したいため、DBに関する設定を追記。

#### 設定ファイル

```gradle:src/hello/build.gradle
buildscript {
	ext {
		springBootVersion = '2.0.3.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'hello'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
	mavenCentral()
}


dependencies {
  compile('org.springframework.boot:spring-boot-starter-web')
  compile('org.springframework.boot:spring-boot-starter-data-jpa')
  compile files('lib/ojdbc7.jar')
  testCompile('org.springframework.boot:spring-boot-starter-test')
}
```

```yaml:src/hello/src/main/resouce/applicatin.yaml
spring:
  datasource:
    url: jdbc:oracle:thin:@//dbserver:1521/XE
    username: testuser
    password: pass
    driverClassName: oracle.jdbc.driver.OracleDriver
    testWhileIdle: true
    validationQuery: SELECT 1
  jpa:
    showSql: true
    hibernate:
      ddlAuto: create-drop
      naming:
        implicitStrategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyHbmImpl
        physicalStrategy: org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
    properties:
      hibernate:
        dialect: org.hibernate.dialect.Oracle10gDialect
```

#### プログラム

```java:src/hello/src/main/java/hello/hello/model/Staff.java
package hello.hello.model;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Column;

@Table(name="staff")
@Entity
public class Staff {

  @Id 
  @Column(name="emp_id")
  private long id;

  @Column(name="staff_name")
  private String name;

  public void setId(long id){
    this.id = id;
  }
  public long getId(){
    return this.id;
  }

  public void setName(String  name){
    this.name = name;
  }

  public String getName(){
    return this.name;
  }
}
```

```java:src/hello/src/main/java/hello/hello/repository/StaffRepository.java
package hello.hello.repository;

import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

import hello.hello.model.Staff;

public interface StaffRepository extends CrudRepository<Staff, Long> {
  Optional<Staff> findById(long id);
}
```

```java:src/hello/src/main/java/hello/hello/HelloApplication.java
package hello.hello;

import java.util.Optional;
import java.util.Collections;
import java.util.Map;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import hello.hello.model.Staff;
import hello.hello.repository.StaffRepository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

@SpringBootApplication
@RestController
public class HelloApplication {
  @Autowired StaffRepository repository;

  @PersistenceContext
  private EntityManager entityManager;

  @RequestMapping("/")
  public String home() {
      return "Hello World from Docker";
  }
  @RequestMapping("/staff")
  public String staff() {
      Optional<Staff> optional =  repository.findById(1);
      Staff staff = optional.orElseGet(() -> new Staff());
      return "Hello " + staff.getName();
  }

  @RequestMapping("/query")
  public String query() {
      List<Staff> results = entityManager
            .createNativeQuery("select * from staff where emp_id = :id ", Staff.class)
            .setParameter("id",1)
            .getResultList();
      String name = "";

      if(results.size() != 0){
        name = results.get(0).getName();
      }
      return "Hello " + name;
  }
	public static void main(String[] args) {
		SpringApplication.run(HelloApplication.class, args);
	}
}
```

#### ビルドの実行

```sh:bin/clean-build.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)
container_name=gradle
cd $bin_dir/../docker && docker-compose run $container_name  gradle clean build
```

## 起動

```dockerfile:docker/spring/Dockerfile
FROM openjdk:jdk-alpine
WORKDIR /app
ENV JAVA_OPTS=""
```

```bash:bin/up.sh
#!/bin/bash

# このシェルスクリプトのディレクトリの絶対パスを取得。
bin_dir=$(cd $(dirname $0) && pwd)

composeFile=${1:-"docker-compose.yml"}
cd $bin_dir/../docker && docker-compose -f $composeFile up $@
```

`./bin/up.sh`で起動。

http://<仮想環境のIP>:8080/にブラウザでアクセスして表示を確認。

[この時点でのソース](https://github.com/hibohiboo/develop/blob/d06194abd971670e3f06040d61199d6d66fbee8e/tutorial/lesson/spring/)


## 参考

[DockerでOracleデータベース11g XEを構築][*1]
[dockerでOracle DB 11gを動かす][*2]
[Dockerで始めるSpring Boot][*3]
[表領域][*4]
[oracle spring sample][*5]
[auto increment][*6.1]
[SpringBootの開発環境をdockerでつくる][*7]
[sample][*8]
[Spring Data JPA でのクエリー実装方法まとめ][*10]
[【JavaEE】今からでも間にあうJPA入門][*11]
[初めてのJPA--シンプルで使いやすい、Java EEのデータ永続化機能の基本を学ぶ][*12]
[ Invalid number format for port number][*13]
[Gradle使い方メモ][*14]
[Dockerで- /etc/localtime:/etc/localtime:ro がMount Deniedを出すやつ][*15]
[docker-compose][*16]
[oracleでautoincrement][*17]


[*1]:http://ryoichi0102.hatenablog.com/entry/2017/12/14/183046
[*2]:http://tmegos.hatenablog.jp/entry/docker-oracle-11g
[*3]:https://qiita.com/ken0909/items/a3f8594ce677bbc7c4c2
[*4]:https://www.projectgroup.info/tips/Oracle/SQL/SQL000007.html
[*5]:https://github.com/shawn-mcginty/spring-boot-oracle-example
[*6.1]:https://stackoverflow.com/questions/11296361/how-to-create-id-with-auto-increment-on-oracle
[*6]:https://stackoverflow.com/questions/10461861/use-database-command-on-sql-plus-oracle-11gr1
[*7]:https://qiita.com/wataling/items/fa8b74fa50d80b88aea3
[*8]:https://github.com/springframeworkguru/spring-boot-oracle-example
[*9]:https://web-dev.hatenablog.com/entry/spring-boot/intro/jpa
[*10]:https://qiita.com/tag1216/items/55742fdb442e5617f727
[*11]:http://www.dcom-web.co.jp/technology/jpa1/
[*12]:https://builder.japan.zdnet.com/sp_oracle/35067018/
[*13]:https://confluence.atlassian.com/bitbucketserverkb/io-error-invalid-number-format-for-port-number-when-connecting-to-oracle-database-939939845.html
[*14]:https://qiita.com/opengl-8080/items/4c1aa85b4737bd362d9e
[*15]:https://remicck.hatenablog.com/entry/2018/02/23/165032
[*16]:https://docs.docker.com/compose/reference/up/
[*17]:https://stackoverflow.com/questions/9733085/auto-increment-for-oracle