Beginner's Hack

復習用。誰かのためになれば...

SpringBootアプリケーションのHerokuへのデプロイ

概要

SpringBootアプリケーションをHerokuにデプロイする手順をまとめます。
heroku.ymlやコンテナレジストリによるコンテナデプロイを試したがうまくいかなかった為、
公式サイトの手順に沿ってデプロイしていこうと思います。

Heroku に Spring Boot アプリケーションをデプロイする | Heroku Dev Center

こんな丁寧な公式サイトがあるなら、初めからこの方法でやっとけって話ですよね。
でもせっかくDockerで開発環境を作ったからデプロイでもコンテナ使いたかったんです。。。

それでは早速作業に入ろうと思います。

手順

heroku CLIのダウンロード

難しいことは何もなし

SpringBootアプリを作成する

参考サイトではCLIからプロジェクトを作っているみたいですが、今回はあらかじめ作ってあるアプリケーションを使います。

ちなみにあらかじめ作成してるアプリの構成は以下の通りです。 JDKMavenがインストールされたコンテナを立ち上げて、
VS CodeのRemote - Container拡張機能でソース編集、デバッグを行うための環境です。

root
|_ .devcontainer
|      |_ devcontainer.json
|_ spring-boot-app
      |_app <- SpringBoot project
      |_docker-compose.yml

SpringBootアプリのローカルリポジトリを作成する

SpringBootプロジェクトのルートディレクトリ(pom.xmlがあるディレクトリ)で下記コマンドを実行する。

git init
git add .
git commit -m 'first commit'

このリポジトリをherokuにpushしてデプロイを行う。

Herokuにpushする

Heroku上にアプリ領域を準備して、そこへ先ほどコミットしたリポジトリをpushする。

heroku create アプリ名

アプリ名は一意であること。日付入れておけば大体OK

リモートリポジトリにherokuが登録されてるか確認する。

git remote -v

ルートディレクトリにsystem.propertiesファイルを作成することでjavaの実行環境を指定することができる。

java.runtime.version=17
javaのバージョンを17に指定

Herokuにpushする

git push heroku main

下記コマンドでブラウザを開きデプロイしたアプリのページに遷移する。

heroku open

SoringBoot - DB(MySQL)接続

目的

SpringBootプロジェクトからDBへアクセスする。

必要な依存関係(Maven)

Spring-data-jpa

<!-- https://mvnrepository.com/artifact/org.springframework.data/spring-data-jpa -->
<dependency>
    <groupId>org.springframework.data</groupId>
    <artifactId>spring-data-jpa</artifactId>
    <version>2.7.2</version>
</dependency>

mysql-connector-java

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.29</version>
</dependency>

application.propertiesの設定

別途、構築したMy SQL環境の情報を記述する。 多少過不足があるかもしれないが、概ね以下の通り。

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://mysql/sample_schema
spring.datasource.username=dev_usr
spring.datasource.password=dev_usr_pass
spring.jpa.database=MYSQL
spring.jpa.hibernate.ddl-auto=update

MySQL上にテーブル作成

上記のapplication.propertiesのdatabaseに指定したデータベースを作成する。

Entityの作成

Spring Data JPA ではテーブル構造に対応したEntityクラスが必要なので作成する。

@Entity /* 本クラスをエンティティとして扱うことができる。 */
@Data /* ゲッター、セッターを省略することができる。 */
public class Account { /* クラス名はテーブル名と揃える */
  /* カラム名をフィールドとして定義する。 */
  @Id /* PKカラムに付与する */
  private String account_id;

  private String email;

  private String password;

  private String userName;
}

Repositoryの作成

JpaRepositoryを継承させたインターフェイスを作成する。

@Repository
public interface AccountRepository extends JpaRepository<Account, String> {
  
}

ServiceからRepositoryのメソッドを呼び出す。

@Service
public class DbDemoService {
  @Autowired
  private AccountRepository accountRepo;

  /**
   * 全件取得
   * Accountテーブルの全レコードを取得する。
   * 
   */
  public List<Account> getAccountList() {

    List<Account> accountList = accountRepo.findAll();
    return accountList;
  }

  /**
   * ID検索
   * AccountテーブルからIDで検索する 
   * 
   */
  public Account getAccountById(String id) {
    Account account = accountRepo.findById(id).orElse(null);
    return account;
  }

  /**
   * 保存
   * 引数として渡されたAccountを保存する
   */
  public void saveAccount(Account account ) {
    accountRepo.save(account);
  }
  
}

JpaRepositoryには上記のような基本的なメソッドが用意されている他、 

Repositoryインターフェイスに独自のクエリを作成することが可能である。

Docker - ホストマシンからコンテナ上のDBに接続

目的

docker-composeで立ち上げたコンテナのに入り、DBを操作する。

コマンド

コンテナに入る

docker exec -it xxx /bin/bash

xxx はコンテナIDの上xx桁(任意:コンテナが特定できれば良い)

コンテナIDの調べ方はdocker psコマンド

MySQLに入る

mysql -u xxx -p

xxxはdocker-composeで記述した、DBのユーザ名

Java - Stream.findFirst()

JavaのStreamはfor文などを使用せずに、中間操作を組み合わせて配列などの抽出、データ編集ができて便利。

その中で、配列の中から一つのオブジェクトを抽出する方法として、findFirst(),findAny()がある。 ただし、これらのメソッドで取得できるのはOptional型なので、注意が必要である。

Optional型から元のオブジェクトを取り出すには、get()があるが使用方法を誤ると例外を発生させてしまう。

    List<String> strList = Arrays.asList("aaa", "bbb", "ccc" );

    String str1 = strList.stream().findFirst().get(); /* "aaa" */

    String str2 = strList.stream().filter(str -> "xxx".equals(str)).findFirst().get(); /* NoSuchElementException */

これを避けるためには、orElse()などでOptionalに値がない場合の初期値を与えた方が良い。

    String str3 = strList.stream().filter(str -> "xxx".equals(str)).findFirst().orElse("yyy"); /* "yyy" */

Java - 配列の比較(==, equals, deepEquals)

配列の比較方法には、タイトルのように3つの方法がある。

この3つの違いを自分なりにまとめてみたいと思う。

==

参照先を比較する為、見た目の値が一緒でも参照先が異なればfalseとなる。

    String[] array1 = {"aaa", "bbb", "ccc" };

    // 元の配列を代入 → 値も参照先も同じ    
    String[] asArray1 = array1;
    // 元の配列をコピー → 値は一緒だが参照先は異なる
    String[] copyArray1 = Arrays.copyOf(array1, 3);
    // 元の配列を同じ値を代入 → 値は一緒だが参照先は異なる
    String[] likeArray1 = {"aaa", "bbb", "ccc" };


    System.out.println(array1 == asArray1); /* true */
    System.out.println(array1 == copyArray1); /* false */
    System.out.println(array1 == likeArray1); /* false */

equals, deepEquals

参照先ではなく、値を比較する。

    String[] array1 = {"aaa", "bbb", "ccc" };

    String[] asArray1 = array1;
    String[] copyArray1 = Arrays.copyOf(array1, 3);
    String[] likeArray1 = {"aaa", "bbb", "ccc" };

    // equals
    System.out.println(Arrays.equals(array1, asArray1)); /* true */
    System.out.println(Arrays.equals(array1, copyArray1)); /* true */
    System.out.println(Arrays.equals(array1, likeArray1)); /* true */

    // deepEquals
    System.out.println(Arrays.deepEquals(array1, asArray1)); /* true */
    System.out.println(Arrays.deepEquals(array1, copyArray1)); /* true */
    System.out.println(Arrays.deepEquals(array1, likeArray1)); /* true */

equalsとdeepEqualsの違い

この2つの違いは、多次元配列の比較の際に現れる。

    // 二次元配列の場合
    String[][] array2 = {{"aaa", "bbb", "ccc" }, {"ddd", "eee", "fff"}};

    String[][] asArray2 = array2;
    String[][] copyArray2 = Arrays.copyOf(array2, 2);
    String[][] likeArray2 = {{"aaa", "bbb", "ccc" }, {"ddd", "eee", "fff"}};

    // ==
    System.out.println(array2 == asArray2); /* true */
    System.out.println(array2 == copyArray2); /* false */
    System.out.println(array2 == likeArray2); /* false */

    // equals
    // equalsは配列の中の参照先が同じか判断している。
    // likeArray2は"配列の中の配列"の参照先が異なるため、falseとなる
    System.out.println(Arrays.equals(array2, asArray2)); /* true */
    System.out.println(Arrays.equals(array2, copyArray2)); /* true */
    System.out.println(Arrays.equals(array2, likeArray2)); /* false */

    // deepEquals
    // "配列の中の配列"の値が同じか判断するため、全てtrueとなる。
    System.out.println(Arrays.deepEquals(array2, asArray2));  /* true */
    System.out.println(Arrays.deepEquals(array2, copyArray2));  /* true */
    System.out.println(Arrays.deepEquals(array2, likeArray2));  /* true */

Arrays.copyOf()メソッドで複製した配列であれば問題なさそうだが、全く別の多次元配列の値を比較する際は、deepEquals()を使用する必要があるため注意していきたい。

private static finalに惑わされた- Java

private static final String xxx = "YYY";

たまに見かけるこのコード。
privateでクラス内からしかアクセスできないのに、staticにする必要ってあるんか? と疑問に思ったので少し調べてみました。

private

アクセス修飾子 | Javaコード入門

現在のクラスからだけアクセスできる

private修飾子についての認識は合っており、特に問題なし。

static

static修飾子 | Javaコード入門

クラスのメンバーにstatic修飾子を付与すると、クラスをインスタンス化しなくてもアクセスできるようになります。具体的には「クラス名.メンバー名」の形式でアクセスできます。

static修飾子についても、特に認識は間違っていない気がする…

static final

static final修飾子 | Javaコード入門

定数であることはfinal修飾子だけで表現できますが、static修飾子を付けるのがお作法です。static修飾子を付与することで、インスタンス生成のたびに同じ値をインスタンスにコピーするのを防ぐことができます。

なるほど。finalと組み合わせることで、インスタンス化しなくてもアクセスできると言う点じゃなくて、同じ値がインスタンス毎にコピーされるのを回避すると言う点でメリットがあったみたい。