GitHub Actionsでプルリクエストがマージされた時、自動的にGitタグをつける

はじめに

こんにちは。新卒2年目の岡崎です。今回は、GitHub Actionsでプルリクエストがマージされた時、自動的にGitタグをつける方法を紹介します。

また、この記事はGitHub Actionsのことがあまり分からない初心者向けの記事になっております。

実装

実装例を紹介します。

workflows配下にYAMLファイルを作成し、実装を行います。

.
├─ .github
    └─workfrows

実際に実装したYAMLファイルは以下です。

name: Git tag | Create
run-name: ${{ github.workflow }} 【${{ github.ref_name }}】

on:
  pull_request:
    branches:
      - main
    types: [closed]

jobs:
  build:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: '0'

      - name: Bump version and push tag
        uses: anothrNick/github-tag-action@1.67.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          DEFAULT_BUMP: ${{ ((contains(github.event.pull_request.head.ref, 'bugfix') || contains(github.event.pull_request.head.ref, 'hotfix')) && 'patch') || 'minor' }}
          WITH_V: true
          RELEASE_BRANCHES: main
          INITIAL_VERSION: v0.0.0

実装例の大まかな解説を行います。

name

nameではワークフローの名前を決定します。またrun-nameでは、ワークフロー実行履歴一覧のrunのタイトル部分を自由に指定することができます。

on

onでは、いつ実行するのか決定することができます。

今回は、プルリクエストがマージされたタイミングでGitタグを自動的に作成したいので、on以下の実装が

on:
  pull_request:
    branches:
      - main
    types: [closed]

となっています。

jobs

ここでは指定した処理を実行します。

jobは一つ以上のstepで構成します。今回の実装だと以下のようになります。

 steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: '0'

      - name: Bump version and push tag
        uses: anothrNick/github-tag-action@1.67.0
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          DEFAULT_BUMP: ${{ ((contains(github.event.pull_request.head.ref, 'bugfix') || contains(github.event.pull_request.head.ref, 'hotfix')) && 'patch') || 'minor' }}
          WITH_V: true
          RELEASE_BRANCHES: main
          INITIAL_VERSION: v0.0.0

Gitタグの付与を自動化する方法として様々な方法がありますが、今回はgithub-tag-actionを導入しました。

実装が終わったら、最後にプルリクエストをマージします。(今回のケースではプルリクエストのマージですが、YAMLファイルに行った実装に合わせて確認を行ってください。)

プルリクエストがマージされた時にGitタグが作れていれば、完了です。

ただし、Gitタグを最初に作る場合、手動で作る必要があるので注意をしてください。

最後に

今回は、GitHub Actionsでプルリクエストがマージされた時に自動的にGitタグをつける方法について紹介しました。いつも何気なく使っているものかもしれませんが、案外調べてみると知識が整理されました。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております。

興味があればぜひぜひ連絡ください!

www.wantedly.com

HTMXでリクエスト実行後にJavaScriptコードを実行する方法

こんにちは、エキサイト株式会社の平石です。

今回は、HTMXでhx-gethx-postを使ってリクエストを実行した後に任意のJavaScriptコードを実行する方法をご紹介します。

はじめに

HTMXでは、hx-gethx-postを利用して、リクエストを実行することができます。
そして、レスポンスとして返ってきたHTMLテンプレートで既存のテンプレートの全体または一部を置き換えることができます。

しかし、このリクエストの実行後に任意のJavaScriptコードを実行したい場合もあると思います。

今回は、その方法をブログ記事として残しておきたいと思います。

環境

以下の環境で動作確認をしています。

  • Java 21
  • Spring Boot v3.2.2
  • HTMX v1.9.10

ただし、基本的にはHTMX + (生の)JavaScriptであるため、サーバーサイドの言語がJava以外であっても動作するかと思います。

以下のようなControllerを考えます。

@Controller
@RequestMapping("sample")
public class SamplePageController {
    private final List<String> stringList = new ArrayList<>();

    @GetMapping("/index")
    public String sampleIndex(
            Model model
    ) {
        model.addAttribute("stringList", stringList);

        return "sample/index";
    }

    @PostMapping("/add")
    @ResponseStatus(HttpStatus.CREATED)
    public String sampleIndex(
            Model model,
            String string
    ) {
        if (string.isBlank() || string.length() > 10) {
            throw new ResponseStatusException(
                    HttpStatus.BAD_REQUEST, "bad request"
            );
        }

        stringList.add(string);

        model.addAttribute("stringList", stringList);

        return "elements/sample/strings";
    }
}

渡された文字列のパラメータをリストに追加し、その結果のリストをテンプレートに渡しています。
ただし、空文字列または文字数が11文字以上であった場合には、ステータスコード400でレスポンスを返します。

次に、テンプレートを記述します。
今回は、Thymeleafを利用しています。

resources/templates/sample/index.htmlを以下のように記述します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <title>サンプルページ</title>
    <script src="/webjars/htmx.org/1.9.10/dist/htmx.min.js"></script>
</head>
<body hx-boost="true">

<div id="stringList" th:insert="~{elements/sample/strings}"></div>

<form id="numberForm"
      hx-post="/sample/add"
      hx-target="#stringList"
      hx-swap="innerHtml">
    <input type="text" name="string">
    <input type="submit" value="追加">
</form>

</body>
</html>

また、resources/templates/elements/sample/strings.htmlを以下のように記述します。

<ul>
    <th:block th:each="string : ${stringList}">
        <li th:text="${string}"></li>
    </th:block>
</ul>

index.html内の<div id="stringList" th:insert="~{elements/sample/strings}"></div>の部分にstrings.htmlが挿入されます。

Spring Bootアプリケーションを起動してsample/indexにアクセスすると、以下のようなフォームが表示されます。

フォームに適当に文字列を入力して、「追加」ボタンを押すと上に入力した文字列が追加されます。

リクエスト後にJavaScriptコードを実行する

それでは、「追加」ボタンを押して文字列をリストに追加した後に任意のJavaScriptコードを実行してみます。
今回は、リクエスト後にアラートを出すような処理を追加します。

HTMXのEventsの中にある、htmx:afterRequestを利用します。

htmx:afterRequestイベントは、AJAXリクエストが終了したときにトリガーされるイベントです。

hx-onを使って、このイベントをトリガーにして実行する処理を記述します。

<form id="numberForm"
      hx-post="/sample/add"
      hx-target="#stringList"
      hx-swap="innerHtml"
      hx-on::after-request="alert('追加しました。')">
    <input type="text" name="string">
    <input type="submit" value="追加">
</form>

イベントはhtmx:afterRequestですが、hx-onで記述する際にはafter-requestとする必要があります。 また、htmx:afterRequestのようにHTMX固有のイベントでは、本来はhx-on:htmx:after-requestと書くところをhx-on::after-requestのように省略して記述することができます。

このコードを追加した上で、適当に入力して追加ボタンを押してみます。

アラートが表示され、リクエスト終了後にJavaScriptコードが実行されたことがわかります。

当然ながら、処理を関数として定義してその関数を呼び出すこともできます。

<form id="numberForm"
      hx-post="/sample/add"
      hx-target="#stringList"
      hx-swap="innerHtml"
      hx-on::after-request="displayAlert()">
    <input type="text" name="string">
    <input type="submit" value="追加">
</form>

<script>
    function displayAlert() {
        alert('失敗しました。');
    }
</script>

また、特別なシンボルとしてeventを利用することができます。
event.detailにはそのイベント固有の情報が入っています。
例えば、htmx:afterRequestでは実行されたリクエストのパラメータやパス、レスポンスの情報を参照することができます。

<form id="numberForm"
      hx-post="/sample/add"
      hx-target="#stringList"
      hx-swap="innerHtml"
      hx-on::after-request="displayAlert(event)">
    <input type="text" name="string">
    <input type="submit" value="追加">
</form>

<script>
    function displayAlert(event) {
        if (event.detail.xhr.status === 400) {
            const string = event.detail.requestConfig.parameters.string;

            alert("失敗しました。パラメータ`string: " + string + "`が不正です。");
        }
    }
</script>

displayAlert関数にeventを渡しています。

ここでは、event.detail.xhr.statusでレスポンスのステータスコードを取得しており、ステータスコード400でレスポンスが返ってきた時のみアラートを出すようにしています。
また、event.detail.requestConfig.parametersでリクエストに使用したパラメータを取得できるので、リクエストパラメータの情報を使ってアラートの内容を補足しています。

この状態で、例えばフォームにToo long stringと入力して「追加」ボタンを押すと以下のようなアラートが出ます。

なお、hx-onを利用せず、生のJavaScriptを利用して、document全体やHTML要素に対して直接イベントリスナを追加することもできます。

全てのHTMXリクエストの後に実行したい処理があるといった場合は、こちらを利用しても良いかもしれません。

<script>
    document.addEventListener('htmx:afterRequest', function (evt) {
        if (evt.detail.failed) {
            alert("失敗しました。");
        }
    });
</script>

おわりに

今回は、HTMXでhx-gethx-postを使ってリクエストを実行した後に任意のJavaScriptコードを実行する方法をご紹介しました。

では、また次回。

参考文献

Flywayを使ったマイグレーションで利用されるhistoryテーブルの名前を変更する

こんにちは、エキサイト株式会社の平石です。

私の担当するサービスでは、Spring Boot (Java)を利用していますが、ローカルではFlyway + MyBatis GeneratorでDB環境および、JavaからDBへの接続環境を構築しています。

その際、複数のデータベースに接続しようとすると、生成されるMapper名(Bean扱い)が重複してしまい、エラーになってしまいました。

その問題の解決方法を紹介したいと思います。

FlywayとMyBatis Generatorについて

Flywayはデータベースマイグレーションツール、MyBatis GeneratorはDB内のテーブルからModel(Entity)やMapperを自動生成してくれるツールです。 生成したModelやMapperを利用して対応するテーブルにJavaからアクセスすることができます。

  1. SQLファイルにテーブル作成や変更のSQLを記述
  2. Flywayを利用して、マイグレーション
  3. MyBatis Generatorを利用して、マイグレーションで生成されたテーブルからModelやMapperを生成

ここでは、それぞれの仕組みや導入方法の詳細は紹介しませんので、以下の記事をご覧ください。

tech.excite.co.jp

tech.excite.co.jp

生成されたMapper名がコンフリクトする

Flywayでは、デフォルトではflyway_schema_historyという名前のテーブルでマイグレーションの履歴を管理しています。

私が担当するプロジェクトでは2つのデータベースにアクセスする必要があり、以下のブログ内の方法で設定しました。

tech.excite.co.jp

この時、この2つのデータベースはFlyway + MyBatis Generatorで環境設定を行なっており、それぞれのデータベースにflyway_schema_historyというテーブルが存在することになります。

この状況で、MyBatis Generatorを実行するとFlywaySchemaHistoryMapperというMapperが2つ生成されてしまいます。
MapperはBeanの扱いとなるため、この状況でSpring Bootアプリケーションを起動すると、同じ名前のBeanが複数あることになりエラーが発生します。

2024-01-21T13:57:45.841+09:00 ERROR 80082 --- [  restartedMain] o.s.boot.SpringApplication               : Application run failed

org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'flywaySchemaHistoryMapper' for bean class [com.example.sample_project.persistence.schema2.mappergen.FlywaySchemaHistoryMapper] conflicts with existing, non-compatible bean definition of same name and class [org.mybatis.spring.mapper.MapperFactoryBean]

私の調べた限りでは、特定のテーブル(ここでは、flyway_schema_history)のみ生成対象からを除外することはできないようです。
また、以下のブログで紹介されている方法をうまく利用すれば回避できそうですが、全てのEntityやMapperにSuffixやPrefixがついてしまうためあまり実行したくありません。

tech.excite.co.jp

historyテーブルの名前を変更する

色々と調べていますと、どうやらマイグレーションの履歴を管理するテーブルの名前を変更することができるようです。

Table - Flyway - Product Documentation

Flywayの設定の方法は複数ありますが、設定ファイルを利用する場合は以下のような内容を記述することになります。

Configuration - Flyway - Product Documentation

〜〜urlやuser, passwordの情報が記述されている〜〜

flyway.table=schema1_flyway_schema_history

これで、マイグレーションの履歴を管理するテーブルの名前がschema1_flyway_schema_historyになり、生成されるMapperの名前が重複しないようになりました。

(今回は、マイグレーションの履歴テーブルのMapperファイルという、実際には不要なBeanの名前被りが原因のエラーでした。しかし、複数のデータベースに同じ名前のテーブルが多く存在してしまうことが原因の場合には、Suffixのように名前被りを排除するような設定する必要があります。)

終わりに

今回は、FlywayとMyBatis Generatorを利用しており、複数のデータベースに接続する必要がある場合に発生するエラーとその対処法を紹介しました。

最も綺麗な方法は、特定のテーブルを生成対象から除外する設定を追加してくれることですので、MyBatis Generatorがその対応をしてくれると良いですね。

では、また次回。

参考文献

MyBatis Generatorを利用して、Model(DTO)やMapperを自動生成する

こんにちは、エキサイト株式会社の平石です。

今回はMyBatis Generatorを利用して、ModelやMapperを自動生成する方法をご紹介します。

はじめに

DBからデータを取得するためのSQLや、DBから取得したデータを格納しておくDTO (Data Transfer Object) をいちいち作成するのは面倒です。

特に、単純なCRUDを実行したい場合や、一つのテーブルから多くのカラムを取得したい場合では、それぞれのテーブルについてSQLやコードを書いていると開発の効率が落ちてしまいます。

本ブログでは、このような負担を軽減するために、MyBatis Generatorを利用してModelやMapperを自動生成する方法を紹介します。

導入

環境

Java 21

Gradle 8.5

MyBatis Generator Version 1.4.2

DB:MySQL 8.2(ローカルのDockerで起動、ポート番号 : 3306、スキーマ : sample_schema)

実際にやってみる

今回は、Javaプログラムから実行する方法とサードパーティのGradle Pluginを利用する方法の2つをご紹介します。 どちらもビルドツールにGradleを利用します。
また、build.gradleはgroovyで記述していきます。

Javaプログラムから実行する方法

1つ目の方法はJavaプログラムから実行する方法です。

依存関係

まずは、依存関係を追加します。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // Lombok
    compileOnly 'org.projectlombok:lombok:1.18.30'

    // 以下の4つの依存関係を追加
    // MyBatisGenerator
    implementation 'org.mybatis.generator:mybatis-generator-core:1.4.2'
    // MySQL接続用
    implementation "com.mysql:mysql-connector-j:8.2.0"

    // 自動生成されるファイルで利用
    implementation "org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.5.0"
    implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3"
}

tasks.named('test') {
    useJUnitPlatform()
}

コメント部分以外は、spring initilizrで生成したものです。

なお、PostgreSQLに接続する場合には、以下の依存関係を追加します。

implementation "org.postgresql:postgresql:42.7.1"

設定ファイル

次に、設定ファイルを記述していきます。
MyBatis Generatorの設定ファイルはXML形式のファイルであり、公式ドキュメントにサンプルの設定ファイルが掲載されています。
これをベースに、設定をカスタマイズすると良いでしょう。

今回は以下のように設定しました。

<!DOCTYPE generatorConfiguration PUBLIC
        "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
    <context id="id" targetRuntime="MyBatis3DynamicSql">
        <commentGenerator>
            <property name="suppressDate" value="true" />
        </commentGenerator>

        <jdbcConnection driverClass="com.mysql.cj.jdbc.Driver"
                        connectionURL="jdbc:mysql://localhost:3306/sample_schema"
                        userId="sample-user"
                        password="sample-password">
            <property name="nullCatalogMeansCurrent" value="true"/>
        </jdbcConnection>

        <javaTypeResolver>
            <property name="useJSR310Types" value="true" />
        </javaTypeResolver>

        <javaModelGenerator targetPackage="com.example.mbgenerator_test.mbgenerator.modelgen"
                            targetProject="./src/main/java"/>

        <javaClientGenerator targetPackage="com.example.mbgenerator_test.mbgenerator.mappergen"
                             targetProject="./src/main/java"/>

        <table tableName="%"/>
    </context>
</generatorConfiguration>

jdbcConnection要素にはDBへの接続情報を記述します。
こちらは、接続するDBスキーマのURL、ユーザー名、パスワードと接続に利用するドライバを設定します。
今回は、MySQLへの接続のためのcom.mysql.cj.jdbc.Driverを利用します。(PostgreSQLの場合は、org.postgresql.Driver

ここでは、nullCatalogMeansCurrentプロパティを設定しています。
これは、バージョン8.xのcom.mysql.cj.jdbc.Driverを利用している場合、information_schemaperformance_schema内のテーブルからもModelやMapperを生成してしまうため、この挙動を無効化するために設定する必要があるようです。

javaModelGeneratorjavaClientGeneratorでは、それぞれModelとMapperを生成するプロジェクトやパッケージを設定することができます。
生成の対象ではなく、生成したファイルを置く「場所」を指定します。

ここでは、それぞれ./src/main/javaプロジェクト内の、com.example.mbgenerator_test.mbgenerator.modelgencom.example.mbgenerator_test.mbgenerator.mappergenを設定していますが、ご自身のお好みのプロジェクト、パッケージを指定してください。

基本的な設定はこれで十分ですが、他にも細かい設定を行うためのXML要素が用意されています。
例えば、ここで指定しているjavaTypeResolveruseJSR310Typesプロパティはtrueに設定することで、テーブル内のDATE, TIME, TIMESTAMP型をJavaの型に変換する際にjava.timeのデータ型を利用するようにできます(デフォルトではjava.util.Dateを利用する)。

他の設定項目は公式ドキュメントを参照してください。

PostgreSQLを使う際の補足

PostgreSQLでは、table要素内のschema属性で自動生成ファイルを生成したいテーブルが存在するスキーマを指定する必要があります。
さもないと、pg_catalogスキーマ内のシステムテーブル等による自動生成ファイルも生成されてしまいます。

<table schema="public" tableName="%"/>

実際に生成する

ここまでで設定は終わりましたので、実際に生成してみます。

以下のようなmainメソッドを持つクラスを作成します(設定ファイルのパス以外は、コピぺで問題ありません)。

import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import org.mybatis.generator.api.MyBatisGenerator;
import org.mybatis.generator.config.Configuration;
import org.mybatis.generator.config.xml.ConfigurationParser;
import org.mybatis.generator.exception.InvalidConfigurationException;
import org.mybatis.generator.exception.XMLParserException;
import org.mybatis.generator.internal.DefaultShellCallback;

public class MbGeneratorExec {
    public static void main(String[] args) throws XMLParserException, IOException, InvalidConfigurationException, SQLException, InterruptedException {
        List<String> warnings = new ArrayList<String>();
        boolean overwrite = true;
        File configFile = new File("./src/main/resources/config/generatorConfig.xml");
        ConfigurationParser cp = new ConfigurationParser(warnings);
        Configuration config = cp.parseConfiguration(configFile);
        DefaultShellCallback callback = new DefaultShellCallback(overwrite);
        MyBatisGenerator myBatisGenerator = new MyBatisGenerator(config, callback, warnings);
        myBatisGenerator.generate(null);
    }
}

ここまでで、全てのパッケージやディレクトリ構成は以下のようになっています。
(あくまで、参考なので自由に設定して問題ありません。)

このmainメソッドを実行すれば、アクセス先のスキーマにあるテーブルのModelクラスとMapperが生成されます。

動作確認

実際に、動作が確認できなければ意味がありません。
生成したModelやMapperがJavaから利用できることを確認します。

sample_tableは以下のようなテーブルであったとします。

CREATE TABLE `sample_table` (
  `sample_id` bigint NOT NULL AUTO_INCREMENT,
  `sample_column` varchar(16) COLLATE utf8mb4_general_ci DEFAULT NULL,
  PRIMARY KEY (`sample_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

この時、以下のようなControllerを作成して、指定したURLにアクセスしてみます。

import java.util.Optional;

import com.example.mbgenerator_test.mbgenerator.mappergen.SampleTableMapper;
import com.example.mbgenerator_test.mbgenerator.modelgen.SampleTable;
import lombok.RequiredArgsConstructor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@MapperScan(basePackages = {"com.example.mbgenerator_test.mbgenerator.mappergen"})
@RequestMapping()
@RequiredArgsConstructor
public class SampleController {
    private final SampleTableMapper sampleTableMapper;

    @GetMapping("/index")
    public String index() {
        Optional<SampleTable> sampleTable = sampleTableMapper.selectByPrimaryKey(1L); 

        return sampleTable.map(data -> data.getSampleColumn()).orElse("何もありません");
    }
}

すると、以下のように表示されMapperがうまく機能していることがわかります。

サードパーティのGradle Pluginを利用する方法

今回は、com.qqviaja.gradle.MybatisGeneratorプラグインを利用します。

依存関係

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
    id "com.qqviaja.gradle.MybatisGenerator" version "2.5"
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

repositories {
    mavenCentral()
}

mybatisGenerator {
    configFile = 'src/main/resources/config/generatorConfig.xml'
    dependencies {
        mybatisGenerator "com.mysql:mysql-connector-j:8.2.0"
        mybatisGenerator "org.mybatis.generator:mybatis-generator-core:1.4.2"
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

    // Lombok
    compileOnly 'org.projectlombok:lombok:1.18.30'
    
    implementation "org.mybatis.dynamic-sql:mybatis-dynamic-sql:1.5.0"
    implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3"

    // 生成されたMapperの動作確認用
    implementation "com.mysql:mysql-connector-j:8.2.0"
}

tasks.named('test') {
    useJUnitPlatform()
}

1つ目の方法との違いはmybatisGeneratorという独自のブロックがあり、そこで設定ファイルのパスおよびファイル名と依存関係を指定しているところです。
設定ファイルの記述は、「Javaプログラムから実行する方法」と同じなのでそちらを参照してください。

実際に生成する

この方法では、プロジェクトのルートから以下のコマンドを実行するだけです。

./gradlew mbgenerator

これで、ModelファイルとMapperファイルが生成されました。

終わりに

今回は、MyBatis Generatorを利用して、ModelやMapperを自動生成する方法をご紹介しました。

では、また次回。

参考文献

個人的・テックブログの続け方

こんにちは。 エキサイト株式会社の三浦です。

エンジニアであれば、「テックブログ」の存在を知っている方も多いのではないでしょうか(かくいうこのブログ自体もテックブログです)。

中には、実際に「テックブログを始めてみた」という方もいるかも知れません。

ですが、個人的な印象では、単発で書くことは出来ても継続するのは難しいというのが実情だと思います。

今回は、私が実際にテックブログを継続するために行ってきたことを説明していきます。

ブログを継続するために行ったこと

常にネタを探す

テックブログを書こうと思っても、何を書けば良いかわからない…というパターンは結構多いのではないでしょうか。

実際私も、書こうと思ったタイミングでネタを考え始めても、なかなか思いつかないことが多くありました。

ではどうすればよいかと言うと、常にネタを探しましょう。

普段から「これはブログのネタにならないかな」と思いながら仕事をしていると、案外書けそうなことが多かったりします。

そういったものを普段から探し、見つけたらネタ帳なりタスク管理ツールなりに書いていけば、実際にブログを書く際にネタに困ることが少なくなるはずです。

簡単なものから始める

テックブログと言えば丁寧な文章、画像による説明など、長くてきちんとしているものが多いから、自分も書く時はちゃんとやらなければ…と思う方もいるかも知れません。

ですが、いきなりそんなブログを書こうと思っても、慣れていなければ難しいものです。

また、慣れにかかわらずそのような大作のブログは作成に時間がかかってしまうでしょう。

そのため、最初のうちは短めで簡単なブログから書くと良いのではないでしょうか。

慣れてきて、時間があるタイミングで、大作のブログに挑戦してみると良いでしょう。

書くタイミングを決める

最初のうちは、ブログを書くつもりでいても、「後で良いや」と思ってしまったり、気づいたら一週間・二週間とブログを書き忘れてしまうことは多々あると思います。

それを回避するためには、ブログを書くタイミングを決めてしまいましょう。

例えば「**曜日の午前中はブログを書く時間にする」などです。

なんとなく仕事に集中できない時間帯が普段からあるのであれば(例えば週明け月曜日の午前中は、まだギアが入らないなど)、その時間をブログを書く時間にするとちょうどいいかもしれません。

習慣化する

上記を繰り返していくと、そのうちブログを書くことが習慣化されていきます。

そうすれば勝ったも同然で、ここまでくれば逆にブログを書かないと気持ち悪くなってくるでしょう。

厳しくしすぎない

色々と書きましたが、とはいえ何かしらの事情で設定したタイミングにブログを書けない場合もあるでしょう。

その際は、あまり自分を責めすぎず、数日ずらしてブログを書いたり、思い切ってその週は休みとしてしまうのも一つの手です。

厳しくしすぎてしまうと一度失敗しただけで嫌になって辞めてしまう原因にもなりうるので、適度に自分を甘やかしましょう。

とはいえ、甘やかしすぎると今度はサボり癖となってしまうので、できる時はちゃんとやるようにしましょう。

最後に

テックブログは、書けば書くほど自分の資産になっていきます。

それは、自分の知見を保存しておけるという点はもちろん、人目に触れる部分にブログが存在するため、社内外問わず自身の評価が上がっていき、市場価値を上げることにもつながるためです。

最初は続けるのが難しいかもしれませんが、簡単なものからで良いと思うので、ぜひやってみてはいかがでしょうか。

VScodeでThymeleafファイルの相対パスを開く方法

こんにちは。エキサイト株式会社 デザイナーの小野寺です。

VScodeでThymeleafファイルの作業中、読み込んでいるファイルへの移動(いつもならcmd + クリック)がデフォルトで出来ず、詰みかけたところ見つけたプラグインで救われたので、今日はそのお話をシェアさせていただきます

「Thymeleaf Navigation」というプラグインを使いました。

いきなり結論です。こちらを使います。

プラグインをインストール

marketplace.visualstudio.com

②「Thymeleaf Navigation」の設定画面を開いて

 

③パスを補完する設定を記述します。

今回エンジニアが作成したファイル内ではフルパスで読み込みファイルを指定していないので、補完するパスを設定に追加します。 (エンジニアはIntelliJ 、デザイナーはVScodeと違うツールを使用しているので、よろしくやってくれる度合いに差分がある状態からのスタートでした)

④ファイルのパスに下線がつき、クリック、もしくはcmd + クリックでファイルを開けるようになります。

フルパスになるように指定する

③に何を書けばいいの?となってる方は 「設定に書かれたパス」+「ファイル内で書かれているパス」でフルパスになるように設定すると良いと思います。

例) web/woman/controller/src/main/resources/templates/elements/pc/headerLine.html

開けない(パスが通らない)ファイルもあるんじゃないの?

上の例でいうところの「/templates/」下以外に置かれてるファイルは開けません。 が、デザイナーの作業範囲上、辿るのが困難なパスはそれ以外なさそうなので一旦よしとしました!

Metabaseをコンテナで立ち上げてみた

こんにちは。 エキサイト株式会社の三浦です。

今回は、Metabaseをコンテナで立ち上げてみた話をしていきます。

Metabaseとは

MetabaseはBIツールの一つで、社内のデータを可視化するのに役立ちます。

www.metabase.com

  • Dockerを使って簡単に起動できる
  • 様々なデータソースと接続できる

など、使いやすいツールです。

今回はこのMetabaseを、実際にコンテナで立ち上げてみます。

コンテナで立ち上げる

それでは、実際に立ち上げてみます。

とは言っても、 docker compose を使えば以下の設定だけで完了です。

version: '3.8'

services:
  metabase:
    image: metabase/metabase:v0.49.0
    environment:
      MB_DB_TYPE: postgres
      MB_DB_HOST: postgres
      MB_DB_DBNAME: bi_tool
      MB_DB_PORT: 5432
      MB_DB_USER: bi_tool
      MB_DB_PASS: bi_tool_pw
    ports:
      - "3000:3000"

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: bi_tool
      POSTGRES_USER: bi_tool
      POSTGRES_PASSWORD: bi_tool_pw
    ports:
      - "5432:5432"

細かく見ていきます。

本質的には、Metabaseの設定は以下だけです。

version: '3.8'

services:
  metabase:
    image: metabase/metabase:v0.49.0
    ports:
      - "3000:3000"

実はこれだけで、Metabaseを立ち上げることが出来ます。

ただしこの場合、Metabaseの設定はローカルに保存されることとなります。 コンテナとして立ち上げる場合、コンテナを立ち上げ直すたびに設定が消えてしまうのは使い勝手が悪いので、設定をローカル以外の場所に保存する必要があります。

今回はPostgreSQLに保存するようにしており、そのために残りの設定をしています。

version: '3.8'

services:
  metabase:
    image: metabase/metabase:v0.49.0
    environment:
      MB_DB_TYPE: postgres
      MB_DB_HOST: postgres
      MB_DB_DBNAME: bi_tool
      MB_DB_PORT: 5432
      MB_DB_USER: bi_tool
      MB_DB_PASS: bi_tool_pw
    ports:
      - "3000:3000"

  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: bi_tool
      POSTGRES_USER: bi_tool
      POSTGRES_PASSWORD: bi_tool_pw
    ports:
      - "5432:5432"

postgres サービスにて、PostgreSQLのコンテナを立ち上げています。 DB名やユーザ名・パスワードは任意のもので構いません。

そして、 metabase サービスの environment にて、立ち上げたPostgreSQLに設定を保存するようにしています。

各種データは、先に立ち上げたPostgreSQLの設定に合わせるようにしましょう。

これで設定は完了です! 実際に立ち上げてみます。

http://localhost:3000/

無事、立ち上げることができました!

もちろん、Metabaseのコンテナのみ削除・再立ち上げしても、設定は失われません。

最後に

BIツールによるデータ可視化は、サービスを運営していく上で非常に有用です。

Metabaseは上記のように非常に簡単に立ち上げることができるので、ぜひ使ってみると良いのではないでしょうか。

参考

[ データ可視化ツール]MetabaseをDocker上で構築してRedshiftへ接続する | DevelopersIO

docker composeでmetabaseを構築する | mebee

Metabaseの設定情報をPostgreSQLに保存 - suzuki-navi’s blog

SpringBoot3 x Thymeleaf で標準のレイアウトを使用する

エキサイト株式会社メディア事業部エンジニアの佐々木です。SpringBoot3でMPAアプリケーションを開発する場合に、Thymeleafテンプレートを使用することは、ほぼデファクトになるかと思います。今回はThymeleafのフラグメントを使用した簡単なレイアウトファイルの作成をご紹介します。

前提

$ java --version
openjdk 21.0.2 2024-01-16 LTS
OpenJDK Runtime Environment Corretto-21.0.2.13.1 (build 21.0.2+13-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.2.13.1 (build 21.0.2+13-LTS, mixed mode, sharing)

$ ./gradlew --version

Welcome to Gradle 8.6!
------------------------------------------------------------
Gradle 8.6
------------------------------------------------------------

Build time:   2024-02-02 16:47:16 UTC
Revision:     d55c486870a0dc6f6278f53d21381396d0741c6e

Kotlin:       1.9.20
Groovy:       3.0.17
Ant:          Apache Ant(TM) version 1.10.13 compiled on January 4 2023
JVM:          21.0.2 (Amazon.com Inc. 21.0.2+13-LTS)
OS:           Mac OS X 12.5 aarch64

./gradlew bootRun

> Task :bootRun

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.3)

設定ファイル

build.gradleは下記にように設定されています。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.3'
    id 'io.spring.dependency-management' version '1.1.4'
}

group = 'jp.co.excite'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

bootRun {
    sourceResources sourceSets.main
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // Thymeleafを使用するモジュール
    implementation 'org.springframework.boot:spring-boot-starter-web'  // Webが表示できるようにするモジュール
}

tasks.named('test') {
    useJUnitPlatform()
}

今回のファイル構成

下記は、Thymeleafに必要な部分のみになります。

project_root
└ src
   └ main        
     └ resoureces
       └ templates
         └ index.html
         └ layoutFile.html  

エンドポイントの設定

下記のようなコントローラーを設定します。リクエストがきたら、index.htmlテンプレート返却するような処理になります。

@Controller
@RequestMapping("")
public class RootController {

    @GetMapping("")
    public String index(){
        return "index";
    }
}

レイアウトHTMLを作成する

レイアウト用のHTMLを作成します。ファイル名は、layoutFile.htmlとします。レイアウトファイルでは、 th:fragment を使用して構築していきます。

layoutFile.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"  th:fragment="layout(title, content, header, footer)"> <!--  th:fragment を宣言します -->
<head>
    <meta charset="UTF-8">
    <title th:replace="${title}">Title</title> <!-- th:fragmentで宣言したフラグメント名layoutの title を利用する -->
</head>
<body>
<header>
    <h1 th:replace="${header}">ヘッダーです</h1> <!-- th:fragmentで宣言したフラグメント名layoutの header を利用する -->
</header>
<hr/>
<main>
    <article>
        <h2>メインのコンテンツです。</h2>
        <div th:replace="${content}"> <!-- th:fragmentで宣言したフラグメント名layoutの content を利用する -->
            demo用のコンテンツです
        </div>
    </article>
</main>
<hr/>
<footer>
    <b>フッターです</b>
    <div>
       <p th:insert="${footer}">Copyright &copy; excite.co.jp demo</p>  <!-- th:fragmentで宣言したフラグメント名layoutのfooterを利用する -->
    </div>
</footer>
</body>
</html>

th:fragment="layout(title, content, header, footer)として4つの引数を取ることができますが、この値を置き換えたいHTMLタグにth:replaceth:insertを使用し設定します。

テンプレートを利用する側のHTML

続いては利用する側のHTMLを書いていきます。ファイル名は index.html とします。 利用する側は、 th:replace を使用します。このときに 前項で作成したlayoutFile.htmlを読み込みながら、layoutフラグメントをそれぞれの値で書き換えいくように設定します。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" th:replace="~{layoutFile::layout(~{::title}, ~{::#content}, ~{::index_h1}, _)}">
<head>
    <meta charset="UTF-8">
    <title>sample sample</title>
</head>
<body>
<header>
  <h1 th:fragment="index_h1"> index.htmlのh1です </h1>
</header>
<div id="content">
    <p>コンテンツの中身です。</p>
</div>

</body>
</html>

th:replace="~{layoutFile::layout(~{::title}, ~{::#content}, ~{::index_h1}, _)}" これの説明をします。

  1. th:replaceで置き換える指示をします。
  2. ~{layoutFile::layout(〜〜〜) layoutFile.htmlのlayoutフラグメントを呼び出します。下記は、layoutの引数の中身を解説します。
    1. ~{::title} titleタグを引数に入れます
    2. ~{::#content} HTML内の id="content" を引数に入れます
    3. ~{::index_h1} HTML内の th:fragment="index_h1"を引数に入れます
    4. _ トークンなし引数を渡します。Thymeleafとして処理せずに記述してあるlayoutFile.htmlに書いてあるHTMLをそのまま出力してくれます。

出力は下記になります。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>sample sample</title>
</head>
<body>
<header>
    <h1>index.htmlのh1です</h1>
</header>
<hr/>
<main>
    <article>
        <h2>メインのコンテンツです。</h2>
        <div id="content">
    <p>index.htmlのコンテンツの中身です。</p>
</div>
    </article>
</main>
<hr/>
<footer>
    <b>フッターです</b>
    <p>Copyright &copy; excite.co.jp demo</p>
</footer>
</body>
</html>

layoutFile.htmlの指定した場所のみの書き換えができました。

まとめ

Thymeleafは標準レイアウトのみで、上記のようなレイアウトファイルを使用した作りが可能です。参照もHTMLタグでもidでも指定可能なので割と柔軟性はあるかと思います。MPAで開発する際には活用したいところです。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

OpenAPI Generatorを利用して自動生成したJavaのAPIクライアントをローカルリポジトリに保存する

こんにちは、エキサイト株式会社の平石です。

今回は、OpenAPI Generatorを利用して自動生成したJavaAPIクライアントを独自のローカルリポジトリに保存する方法をご紹介します。

なお、今回の方法は例として「OpenAPI Generatorを利用して自動生成したJavaAPIクライアント」を挙げているだけで、それ以外の自作のライブラリ等でも利用できます。

はじめに

OpenAPI Generatorを利用してJavaAPIクライアントを自動生成する基礎については、以下のブログ記事でまとめていますので、こちらをご覧ください。

tech.excite.co.jp

実際のサーバー環境上で運用する際の問題点

上で紹介したブログ記事では、生成したクライアントをローカルのMaven Repositoryにインストールし、利用したいプロジェクトで依存関係を追加して利用しました。

ローカルのMaven Repositoryはデフォルトでは、$USER/.m2/repositoryに存在しています。
ローカルで開発している間は良いのですが、サーバー環境上にデプロイした際にはこのRepositoryをそのまま使うことはできませんので、何か策を講じなければなりません。

ここからは、上記の問題を解決していきます。

独自のMaven Repositoryを利用する

この問題の解決方法としては、独自のMaven Repositoryを利用するように設定することが挙げられます。

そもそも、Maven Repositoryは以下のような構成になっていれば、どこに配置していても利用することが可能です。

/{repository_root}/{group ID}/{artifact ID}/{version}/

group ID, artifact ID, versionは、pom.xmlを利用して設定や依存関係を記述している人なら馴染み深いでしょう。
例えば、OpenAPIをSpring Bootで利用するための、Springdocというライブラリを追加する際には、pom.xmlに以下のように記述します。

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

つまり、この設定はMaven Repositoryの

/{repository_root}/org/springdoc/springdoc-openapi-starter-webmvc-ui/2.3.0

配下にあるライブラリを依存関係として追加するということを表しているのです。

そして、この配下に主に以下の4つのファイルが配置されています。

  • {artifact ID}-{version}.pom
  • {artifact ID}-{version}.jar
  • {artifact ID}-{version}-sources.jar
  • {artifact ID}-{version}-javadoc.jar

逆に言えば、この構成を好きな場所に再現して利用すれば良いのです。

この「好きな場所」は、別のサーバー上やAWSのS3のようなオブジェクトストレージでも可能です。

build.gradleから参照する際には、以下のように記述しておけば、ライブラリを探すときにこの中を見てくれるようになります。

repositories {
        mavenCentral()

        maven {
            url {利用したいMaven RepositoryのURL}
        }
}

なお、ライブラリを探す際には、repositoriesで指定した順番で探しに行くようです。
したがって、mavenCentral()から取得するライブラリが多い場合には、こちらを上に記述することで、ビルド時間を短縮することが可能です。

独自のライブラリを多く利用する必要がある場合には、別のサーバー上やS3でMaven Repositoryを管理するメリットがあります。
しかし、当分今回作成したAPIクライアントしか利用する予定がないような場合には、サーバーやS3に保存しておく方法は作成や管理の手間がかかってしまいます。
そのような場合には、Maven Repositoryをプロジェクト内に含めて、まとめてデプロイしてしまう方法も選択肢に入ります。
JARファイルは圧縮されているため、ファイルサイズ的には問題にはならないでしょう。

今回は、こちらの方法をご紹介します。
といっても、やることは簡単です。

基礎編では、以下のコマンドでLocal RepositoryにAPIクライアントをインストールしていました。

mvn clean install

先ほど述べたように、このままではデフォルトのローカルリポジトリである$USER/.m2/repository配下にインストールされてしまいます。

そこで、-Dmaven.repo.localオプションを付与することで、一時的にLocal Repositoryの場所を変更することができます。

mvn clean install -Dmaven.repo.local=./{path_to_local_repository}

{path_to_local_repository}の部分にご自身が設定したいlocal_repositoryの場所を指定してあげることで、その場所にAPIクライアントがインストールされます。

ただし、このままでは生成したAPIクライアント以外にも不要なライブラリもインストールされてしまいます。 (このAPIクライアントのpom.xmlで設定されているライブラリか?)

これらは、実際にAPIクライアントを利用する上では不要なので、APIクライアントを含んだディレクトリだけをコピーして適当なディレクトリにペーストしておきます。

例えば、以下のようなディレクトリ構成となっていおり、clientgenディレクトリにAPIクライアントが自動生成されているとします。

この場合には、以下のようなコマンドを実行することで、local_repositoryにプロジェクトで利用するAPIクライアントのみを含んだMaven Repositoryを構成することができます。
あらかじめ、local_repository配下にcom/exampleという空ディレクトリを作成しておいてください。 (現在、プロジェクトルートにいるとします。)

cd clientgen
mvn clean install -Dmaven.repo.local=./local_repository_temp
cp -rf ./local_repository_temp/com/example/openapi-java-client ./../local_repository/com/example

次に、ライブラリを取得する先のMaven Repositoryを設定します。

repositories {
    mavenCentral()
    maven {
        url "$rootDir/local_repository"
    }
}

依存関係を追加します。

dependencies {
    〜〜 略 〜〜

    implementation "com.example:openapi-java-client:0.0.1-SNAPSHOT"
}

これで、プロジェクト内でAPIクライアントを利用できるようになりました。
自動生成したAPIクライアントそのもの(clientgen配下)やlocal_repository_tempディレクトリは.gitignoreに記述する(Gitを利用している場合)か、削除しておくと良いでしょう。

おわりに

今回は、自動生成したAPIクライアントをプロジェクト内の自作のMaven Repositoryに保存してサーバー環境上でも利用できるようにするための設定方法をご紹介しました。

では、また次回。

LEFT JOINでハマった話

こんにちは、エキサイト株式会社の平石です。

今回は、初歩的な内容でありながら、SQLLEFT JOINを利用した際にハマったことを記事にしたいと思います。

なお、本ブログのSQLMySQL 8.2で動作確認をしています。

以下のような3つのテーブルがあるとします。

  • sample_userテーブル・・・ユーザーIDとユーザ名を管理
  • prefectureテーブル・・・都道府県IDと都道府県名を管理
  • user_prefectureテーブル・・・ユーザーの居住する都道府県を管理

そして、これらのテーブルにはそれぞれ以下のようなレコードが入っているものとします。

sample_userテーブル

user_id name
1 佐藤
2 鈴木
3 高橋
4 太田
5 山本
6 中川

prefectureテーブル

prefecture_id prefecture_name
1 東京
2 神奈川
3 岡山

user_prefectureテーブル

user_id prefecture_id
1 1
4 1
5 2
6 3

INNER JOINの場合

まずは、関東圏に住んでいるユーザーのみを、都道府県名とともにSELECTすることを考えます。

そのためのSQLは、例えば以下の通りです。

SELECT 
    sample_user.user_id, 
    sample_user.name, 
    prefecture.prefecture_name

FROM sample_user

INNER JOIN user_prefecture
    ON user_prefecture.user_id = sample_user.user_id

INNER JOIN prefecture
    ON prefecture.prefecture_id = user_prefecture.prefecture_id

WHERE prefecture.prefecture_id IN (1, 2);

都道府県を登録していないユーザーは無視して良いのでINNER JOINを利用してsample_userテーブル、user_prefectureテーブル、prefectureテーブルを結合しています。
そして、WHERE句で関東圏の都道府県(ここでは、東京と神奈川)を指定しています。

結果は以下の通りです。

user_id name prefecture_name
1 佐藤 東京
4 太田 東京
5 山本 神奈川

先ほどは、都道府県の条件をWHERE句で指定しましたが、prefectureとの結合条件に含めることで見通しが良くなります。

SELECT 
    sample_user.user_id, 
    sample_user.name, 
    prefecture.prefecture_name

FROM sample_user

INNER JOIN user_prefecture
    ON user_prefecture.user_id = sample_user.user_id

INNER JOIN prefecture
    ON prefecture.prefecture_id = user_prefecture.prefecture_id
    AND prefecture.prefecture_id IN (1, 2);
user_id name prefecture_name
1 佐藤 東京
4 太田 東京
5 山本 神奈川

LEFT JOINの場合

ここからが本題です。
次は、関東圏に住んでいるユーザーと都道府県を登録していないユーザーをSELECTします。
ただし、都道府県を登録しているユーザーはその都道府県名とともに取得するとします。

そのためのSQLは、INNER JOINLEFT JOINに変更すれば良さそうです。

SELECT 
    sample_user.user_id, 
    sample_user.name, 
    prefecture.prefecture_name

FROM sample_user

LEFT JOIN user_prefecture
    ON user_prefecture.user_id =sample_ user.user_id

LEFT JOIN prefecture
    ON prefecture.prefecture_id = user_prefecture.prefecture_id
    AND prefecture.prefecture_id IN (1, 2);

しかし、このSQLではうまくいきません。

user_id name prefecture_name
1 佐藤 東京
2 鈴木
3 高橋
4 太田 東京
5 山本 神奈川
6 中川

user_id = 6の中川は岡山県在住ですが、都道府県を登録していないユーザーであるかのように取得してしまっています。
INNER JOINの場合は結合条件に記述することでうまくいきましたが、これは一体どういうことでしょうか。

とはいえ、この理由はLEFT JOINの定義から明らかです。
LEFT JOINは結合条件(ON以下の文)に合致したレコード同士を結合した上で、合致しなかった場合も結合の「左側」のテーブルのレコードは全て残します。
この時、結合の「右側」のテーブルのカラムは全てnullとして扱います。

この例の場合は、「鈴木」、「高橋」は

user_prefecture.user_id = sample_user.user_id

となるレコードがuser_prefectureに存在しません。
また、「中川」も2つ目の結合で結合条件prefecture.prefecture_id IN (1, 2)に合致しないため、prefectureテーブルのカラムがnullになった上で選択されます。

prefecture.prefecture_id IN (1, 2)が先に実行され、prefectureテーブル内のprefecture_idが1, 2のレコードが残った状態でLEFT JOINが実行されると、勝手に勘違いしてしまっていました.....。

では、「中川」が選択されないようにするためにはどうすれば良いのでしょうか。
今回の場合は、WHERE句に条件を記述する必要があります。
当然ながらINNER JOINの時と同様に

WHERE prefecture.prefecture_id IN (1, 2)

と記述すると、都道府県を登録していないユーザーが取得されません。

都道府県を登録していない場合と、都道府県を登録している場合で場合分けをして条件を記述する必要があります。

WHERE prefecture.prefecture_id IS NULL
    OR prefecture.prefecture_id IN (1, 2);
user_id name prefecture_name
1 佐藤 東京
2 鈴木
3 高橋
4 太田 東京
5 山本 神奈川

CASE式を利用することもできます。

WHERE CASE
          WHEN prefecture.prefecture_id IS NOT NULL
          THEN prefecture.prefecture_id IN (1, 2)
          ELSE TRUE
      END;

終わりに

今回は、LEFT JOINでハマった内容をブログとして残しました。
定義に立ち返ると何ということはない話なのですが、INNER JOINと同じ感覚で使ってしまうと思わぬバグを生むことになりそうです。

では、また次回。

PHP アプリケーションの FCM HTTP v1 API 移行手順

エキサイト株式会社の@mthiroshiです。

アプリのプッシュ通知は、Firebase Cloud Messaging を用いて実装できます。サーバー環境から FCM 実装でプッシュ通知を送る方法の一つとして、FCM HTTP API があります。現在、FCM HTTP API は、新しいAPIへの移行のアナウンスがされています。2024年6月にはレガシーAPIが利用できなくなります。

firebase.google.com

今回は、PHP の実装における FCM HTTP API v1 への移行について紹介します。

公式の移行ガイドによると、主な変更点は下記の3点です。

作業手順に沿ってこの3点を変更していきます。

サービスアカウントの準備

まず、送信リクエストの認可の方法が変更されています。 Firebase と連携しているGCPコンソールからからサービスアカウントを作成し、秘密鍵を生成します。

GCPコンソールでサービスアカウントの秘密鍵を生成する

秘密鍵を生成すると、JSONファイルがローカルにダウンロードされます。このJSONファイルには、秘密鍵が含まれているため、安全に管理してください。

そして、このサービスアカウントに対して、Firebase Admin SDKのロールを割り当てます。

サービスアカウントに Firebase Admin SDK のロールを割り当てる

JSON ファイルを PHP アプリケーションが動作しているサーバーに設置し、環境変数 GOOGLE_APPLICATION_CREDENTIALS にそのパスを設定します。

export GOOGLE_APPLICATION_CREDENTIALS="/home/demo_user/service-account-file.json"

環境変数を設定することで、後述する Google API クライアントが認可する際に、環境変数から JSON ファイルを読み込んでくれます。

Google が提供するAPIやライブラリを利用する際には、アプリケーションのデフォルト認証情報(ADC)と呼ばれる仕組みが使われます。今回の環境変数の設定は、ADCに則った方法です。 cloud.google.com

PHP アプリケーションの対応

プッシュ通知実装の実体として、Google API を使って FCM サーバーにリクエストを行います。 Google API を利用するため、PHPGoogle API クライアントのライブラリを composer を使ってインストールします。 github.com

下記が Google API クライアントを使ったプッシュ通知実装のサンプルコードです。

<?php

require_once 'vendor/autoload.php';

$client = new Google_Client();
$client->useApplicationDefaultCredentials();
$client->addScope('https://www.googleapis.com/auth/firebase.messaging');
$httpClient = $client->authorize();

$params = [
    'message' => [
        'topic' => 'demo_topic',
        'notification' => [
            'title' => 'demo_title',
            'body' => 'demo_body',
        ],
        'data' => [
            'demo_contents_id' => '12345',
        ],
    ],
];

$project = 'demo_firebase_project_id';
$response = $httpClient->post("https://fcm.googleapis.com/v1/projects/$project/messages:send", 
    ['json' => $params]
);

GoogleClient クラスをインスタンス化します。

次に、useApplicationDefaultCredentials() によって先程の環境変数を読み込みます。

Cloud Messagingを利用するため、firebase.messaging のスコープを追加して、 FCM へのアクセスを認可します。

authorize() で認証を実行し、GuzzleHttp の ClientInterface を返却します。

$params は送信リクエストのペイロードです。こちらも移行の変更点でして、以前の API から JSON のフォーマットが変更されています。詳しくは、下記のドキュメントをご覧ください。

REST Resource: projects.messages  |  Firebase Cloud Messaging REST API

最後に、GuzzleHttp クライアントである $httpClient から POST リクエストをして、プッシュ通知のリクエストを行います。 このAPIエンドポイントも変更点となっていまして、パスに Firebase プロジェクトの ID が含まれています。

最後に

PHP実装におけるFCM HTTP v1 API の移行手順について紹介しました。サービスアカウントにロール割り当てができていない場合、プッシュ通知をリクエストした際に権限不足のエラーが出ることがありますので、注意が必要です。

2024年6月にレガシーAPIは利用できなくなりますので、計画的な移行をおすすめします。 参考になれば幸いです。

採用情報

エキサイトではエンジニアを随時募集しております。ご興味ございましたら、下記の募集一覧ページをご覧ください!

www.wantedly.com

参考記事

以前の HTTP から HTTP v1 に移行する  |  Firebase Cloud Messaging

Google API PHP Client - Firebase Cloud Messaging Service v1 example · GitHub

Amazon Aurora MySQLの証明書を更新した話

こんにちは。 エキサイト株式会社の三浦です。

AWSのAurora MySQLにはサーバー証明書が組み込まれていますが、以前デフォルトとして設定されていた rds-ca-2019 がもうすぐ有効期限を迎えます。

今回は、この証明書を更新した話をしていきます。

準備

実際に更新する前に、まず以下の2点を確認しました。

アプリケーションコードで検証のために証明書を使っていないか

アプリケーションコードからDBに接続する際、アプリケーションコード側で証明書を使うことにより、厳密にDBの証明書をチェックをすることができます。

この厳密な証明書チェックをしている場合は、DBの証明書の更新に合わせてアプリケーションコード側で使用している証明書も更新しておく必要があります。

なお、例えばJavaのDB接続ライブラリであるJDBCでは、デフォルトは以下の動作になっています。

  • DBの証明書の有効期限が切れていないかチェックする
  • ただし、わざわざアプリケーションコード側で証明書を使ってまで厳密にはチェックしない

そのため、デフォルトの使い方であればアプリケーションコード側の証明書の更新はする必要がありません。

docs.aws.amazon.com

dev.mysql.com

アプリケーションコードでどのような証明書チェックをしているのかを確認し、適切に対応しましょう。

証明書更新時、DBの再起動が必要かどうか

証明書を更新する際、DBの種類やバージョン等によって再起動の必要性が変わってくるようです。

docs.aws.amazon.com

「証明書の更新」ページから必要性がチェックできます。

再起動が必要な場合は、再起動時にサービスに問題が生じないよう適切に対処しましょう。

証明書更新実行

上記の2つのチェックが完了したので、続いて実際に証明書を更新しました。

準備段階をしっかりしていればそこまで気にすることはありませんが、1つ迷いそうな箇所がありました。

証明書の更新はインスタンス単位

証明書は、クラスタ単位ではなくインスタンス単位で更新する必要があります。

クラスタから変更しようとしても変更項目にないので注意しましょう。

以上で証明書更新は終了です!

最後に

証明書の更新、やってみると意外と簡単でした。

特に準備段階で、アプリケーションコードで証明書を使っておらず、かつ更新時に再起動が不要であれば、実質ポチポチと更新するだけで終わります。

いつかはやらなくてはならないものなので、思い切って早めに終わらせてしまいましょう!

Spring Bootで@CacheEvictを使ってキャッシュを削除する

こんにちは、エキサイト株式会社の平石です。

今回は、Spring Bootで一度作成したキャッシュをTTLが過ぎる前に明示的に削除する方法をご紹介します。

キャッシュを削除したいとき

キャッシュという仕組みでは、DBなどの情報源にアクセスした結果を高速にアクセスできる領域に保存しておきます。
そして、同じアクセスが来たときにもう一度情報源に問い合わせるのではなく、結果を保存した領域からデータを取得します。
このようにすることで、情報源の負荷を軽減したり、レスポンスを高速にしたりすることができます。

便利な仕組みですが、DB等内の情報が更新されてもキャッシュが削除されるまでは同じデータを返し続けてしまうという欠点もあります。
特に、普段は滅多に更新されないデータは、TTL(キャッシュを保持する期間)を長めにしておくことが多く、その場合情報源での更新がなかなか反映されません。

このような場合に対応するために、キャッシュを明示的に削除する方法をご紹介します。

なお、今回はデータへの変更を加える操作もJavaで行われるという前提で使える方法をご紹介します。

環境

今回のブログにおけるソースコードはSpring Boot v3.2.2, Java 21で動作確認をしています。

以下のような、ニュース記事のIDとその記事のタイトルを管理する場合を考えます。
なお、例をシンプルにするためDB等は使わずにデータはハードコーディングしています。

@Service
@RequiredArgsConstructor
public class NewsArticleServiceImpl implements NewsArticleService {

    private final Map<Integer, String> newsArticleTitleMap = new HashMap<>() {
        {
            put(1, "〇〇株式会社が上場");
            put(2, "〇〇地方で大雪");
            put(3, "〇〇が電撃結婚");
        }
    };

    @Override
    @Cacheable(cacheNames = "newsCache")
    public String putCache(final Integer newsId) {
        return newsArticleTitleMap.getOrDefault(newsId, "");
    }
}

putCacheメソッドをnewsId引数と共に呼ぶと、対応するnewsIdを持つ記事のタイトルが返され、設定した「データ領域」にキャッシュが行われます。(詳細は省きますが、今回は裏でRedisを使っています。)

テストのために、3つの記事タイトルのキャッシュをするためのエンドポイントを用意して、呼び出してみます。

@RestController
@RequestMapping("cache/sample")
@RequiredArgsConstructor
public class CacheSampleController {
    private final NewsArticleService newsArticleService;

    @GetMapping("put")
    public void putCache() {
        newsArticleService.putCache(1);
        newsArticleService.putCache(2);
        newsArticleService.putCache(3);
    }
}

valueはSerializeされ、かつ一覧では一部しか表示されていませんが、確かにキャッシュがされているようです。
keyで、newsCache::の後にある数字はnewsIdで、以後例えばnewsId=1でアクセスがあった場合には、newsId::1のエントリからデータを取得してメソッドの返り値として返します。

@CacheEvictでキャッシュを削除する

それでは、キャッシュを削除してみます。
キャッシュを削除するには、@CacheEvictというアノテーションを利用します。
@Cacheableを利用するために追加する'org.springframework.boot:spring-boot-starter-cache'と同じライブラリに含まれているため、追加で依存関係を設定する必要はありません。

基本的な使用法

@Service
@RequiredArgsConstructor
public class NewsArticleServiceImpl implements NewsArticleService {

    〜〜 略 〜〜

    @Override
    @Cacheable(cacheNames = "newsCache")
    public String putCache(final Integer newsId) {
        return newsArticleTitleMap.getOrDefault(newsId, "");
    }

    @Override
    @CacheEvict(cacheNames = "newsCache")
    public void deleteCache(final Integer newsId) {
    }
}

@CacheEvictcacheNamesに削除したいキャッシュのキー名を指定します。
newsId=1deleteCacheメソッドを呼び出すとnewsId::1のエントリが削除されます。

キャッシュを全て削除する

先ほどは、メソッドの引数newsId=1に対応するエントリのみが削除されました。
メソッドの引数に関わらず、newsCacheのすべてのエントリを削除したい場合には、allEntries = trueを指定します。

    @Override
    @CacheEvict(cacheNames = "newsCache", allEntries = true)
    public void deleteCache(final Integer newsId) {
    }

メソッドの引数とkeyが異なる場合

次に、ニュース記事のタイトルを変更する以下のようなメソッドを考えます。
(正確には、すでにタイトルが登録されている場合には更新し、登録されていない場合は新たに追加するメソッドですが。)

    @Override
    @CacheEvict(cacheNames = "newsCache")
    public void updateNewsArticleTitle(final Integer newsId, final String title) {
        newsArticleTitleMap.put(newsId, title);
    }

この時、newsId=2を指定したとしてもnewsCache::2のエントリは削除されません。
なぜなら、@Cacheable@CacheEvictなどのアノテーションは、デフォルトですべての引数をキャッシュに含めようとするためです。
この場合は、newsCache::2,{title引数で指定した文字列}でキャッシュを削除しに行こうとします。
当然、そのようなエントリは存在しません。

titleの値に関わらずキャッシュを削除するには、以下のようにkey属性を指定します。

    @Override
    @CacheEvict(cacheNames = "newsCache", key = "#newsId")
    public void updateNewsArticleTitle(final Integer newsId, final String title) {
        newsArticleTitleMap.put(newsId, title);
    }

keyには、キャッシュのキーに利用する「パラメータや値」を指定します。
複数のパラメータを指定する場合には、"{#newsId, #title}"のように指定できます。

この状態で、newsId=2, title="〇〇地方で大雨"で、updateNewsArticleTitleメソッドを呼び出すと、newsCache::2のエントリが削除されます。

おわりに

キャッシュをする場面と比較して、キャッシュを明示的に削除したい場面は少ないかもしれません。 しかし、もし必要になった場合には、利用してみてください。

では、また次回。

SpringdocでAPIの情報を補足する際、リクエストパラメータには@Parameterを使うべきという話

こんにちは、エキサイト株式会社の平石です。

今回は、SpringdocでAPIのリクエストに対して、付与するアノテーションをご紹介します。

なお、今回のソースコードは以下の環境で動作確認をしています。

  • 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0'
  • Java 21
  • SpringBoot 3.2.2

はじめに

SpringdocはOpenAPI仕様のAPIドキュメントを自動で生成してくれるライブラリです。
ドキュメントを書く手間も省けますし、Swagger UIを利用すればAPIの動作確認もできるので、事業部でも積極的に活用しています。
Springdocでは、Controllerを記述するだけで最低限のドキュメントを作成してくるのですが、ライブラリで提供される専用のアノテーションを付与することで、情報を補足することもできます。
特に、Controllerを書くだけでは「どのような動作をするAPIなのかの説明」「それぞれのパラメータやレスポンスの詳細な説明」といった情報は表示されませんので、アノテーションを利用して補足する必要があります。

例えば、以下のようなControllerを作成したとします。

@RestController
@RequestMapping("sample")
@Tag(name = "sample")
public class SampleController {
    @GetMapping
    @Operation(summary = "サンプルのAPI")
    @ApiResponses(value = {
            @ApiResponse(
                    responseCode = "201",
                    description = "正常に処理が終了した場合"
            ),
            @ApiResponse(
                    responseCode = "500",
                    description = "API内部でエラーが発生した場合"
            )
    })
    public String sample() {
        return "Hello, Excite";
    }
}

この時、Swagger UIを確認すると以下のように反映されています。

サンプルのAPI正常に処理が終了した場合といった文言はアノテーションで補足しないと反映されない内容です。

@Schemaを乱用して発生した問題

これまで、私の所属するチームではSpringdocでAPIの情報を補足する際に、リクエストにもレスポンスにも@Schemaのみを付与していました。

@SchemaはOpenAPI仕様上でデータモデルを定義するために使われるアノテーションです。
データモデルと言いつつも、単一のフィールドやパラメータを表現するのにも使用することができます。

しかし、@Schema単体ではリクエストの際に適切に型が反映されないという問題がありました。

例えば、以下のようなControllerを考えます。

@RestController
@RequestMapping("sample")
@Tag("sample")
public class SampleController {
    @GetMapping
    public String sample(
            @ParameterObject SampleRequestDto requestDto
    ) {
        return "Hello, Excite!";
    }
}

SampleResponseDtoには、これから様々なフィールドを記述していきます。

リクエストの場合

APIのリクエストは、Controller内のメソッドの引数にStringIntegerの状態で直接渡すこともできますし、全てのパラメータをまとめたDTOを渡すこともできます。

今回は、DTOを作成してみます。
Javaのレコード・クラスを利用すると便利です。

public record SampleRequestDto(
        String stringField,
        Integer integerField,
        Long longField
) {
}

この状態でSwagger UIを確認してみます。

各パラメータが認識され、型もJava側で宣言した型からOpenAPI仕様で定義されている型に自動で変換してくれています。

ちなみに、()内に$マークと一緒に記述されているint32date-timeformatです。
integer($int64)integerというtype(型)の$int64というフォーマットであることを表しています。
int64はLong型のことなので、Javaの型がOpenAPI仕様で利用できる適切な型(+ フォーマット)に自動的に変換されていることがわかります。

では、リクエストに、descriptionexampleを記述して情報を補足してみます。

public record SampleRequestDto(
        @Schema(description = "文字列のパラメータ", example = "文字列")
        String stringField,
        @Schema(description = "Integer型のパラメータ",  example = "1")
        Integer integerField,
        @Schema(description = "Long型のパラメータ", example = "2")
        Long longField
) {
}

Swagger UIを確認してみます。

おや?確かに、Descriptionでコメントによる補足は反映されていますが、型が全てstringになってしまっています。

Springdocではリクエストパラメータでは@Schemaで補足情報を加えようとすると、型を自動で変換することができず全てstringになってしまうようです。

この仕様が謎であったため、公式ドキュメントを眺めているとリクエストパラメータの時には@Parameterというアノテーションを利用すると良いようです。
@Parameterでもdescriptionexampleをそのまま指定することが出来ます。

public record SampleRequestDto(
        @Parameter(description = "文字列のパラメータ", example = "文字列")
        String stringField,

        @Parameter(description = "Integer型のパラメータ",  example = "1")
        Integer integerField,

        @Parameter(description = "Long型のパラメータ", example = "2")
        Long longField
) {
}

Swagger UIを確認してみます。

今度は、型が反映されています。

@Parameterではパラメータを入れる場所(ヘッダやクエリパラメータ等)を指定するin、空の値を許可するかどうかを示すallowEmptyValueなどのように、リクエストパラメータに特化した属性値をセットすることもできるため、今後はこちらを利用していきたいと思います。

おわりに

今回は、Springdocでアノテーションを利用して情報を付加するときに、リクエストパラメータには@Parameterを利用すべきであることを紹介しました。

では、また次回。

参考文献

社内カンファレンス「TechCon2024」の裏側!ハイブリット開催の配信設定について

こんにちは。エキサイト株式会社の Hiroshi Sakai です。 2024年も無事TechConが開催されました!開催にあたっての記事は以下をご覧ください。

tech.excite.co.jp

このブログでは、TechCon2024で初めて行われたオフライン、オンラインのハイブリット開催の配信設定について記載します。
専門的な機材や配信を本業とされている方と同じような機材を利用したわけではないので、様々な方が真似できるような構成になっているかと思います。
ハイブリット開催の参考になったら幸いです。

機材配置、配線等について

機材配置は、下記の通り

機材配置図

それぞれ説明していきます。

配信PC (OBS, Zoom, meet)について

配信PC (今回はMacを使用) 1台にOBS, Zoom, meetを起動しておきます。

OBSは、スイッチャーに取り込んだビデオカメラの映像やマイクの音声などを拾いつつ、Zoomに画面共有する画面を作成しています。
OBSのソースに「発表タイトルや登壇者名」「発表資料」「ビデオカメラの映像」「Zoomのコメント欄」「休憩中のCM映像」などを作成しています。
下記のオーバーレイをデザイナーの方に作っていただき、それぞれ必要な素材を配置していってます。
*本番では、モーション付きのオーバーレイ動画を用意していただき、それをOBSで読み込んでいました。
OBSでは、「仮想カメラ開始」を選択しておきます(ZoomにOBSの画面を映すために必要です)

Zoomは、ウェビナー主催者としてスケジュールを作成し、参加URLを視聴者に共有しております。次に、Zoomの画面共有で「第二カメラのコンテンツ」を選択します。選択後、OBSで設定している画面がZoomのウェビナー画面に画面共有されていればOKです。

meetは、登壇者が発表資料を画面共有するために利用しました。登壇者は、meetで発表資料を画面共有してもらいます。
OBSのソース選択でウィンドウキャプチャでmeetを選択肢、meetで画面共有されている内容をOBS上に映します。
OBS, Zoom, meetの設定は、以上になります。OBSでもっと配信ぽくしたり、コメントを横流しにしたり色々できますので、お試しください。

スピーカー、マイクについて

今回の構成では、マイクから聞こえる音は、スピーカーからオフラインの会場に流れるのと、オンラインに流れるのと二つに流しております。オンラインに流す方法としては、スイッチャーにライン出力ケーブルを差し込み、スイッチャーから配信PC(OBS)に取り込むようにすることでマイクの音をオンラインに流しています。
*本来は、オーディオインターフェイスなどがあれば良いかと思います。

利用したスイッチャーについて

ATEM Miniというスイッチャーを利用しています。
ソフトウェアなどもダウンロードでき、ソフトウェア上で音の調整など可能です。(ハウリング防止などは、主にOBS上で設定していました。)

この構成のメリット・デメリット

  • メリット

    • 比較的簡単にできます。(社内にない可能性が高いのは、スイッチャーくらい)
    • 配信が止まらず、安定配信を提供できる(OBSが長時間配信してても落ちない。)
    • 登壇者は、登壇者自身のPCで画面共有しながら発表できるので、オンラインで画面共有しているのと変わらない操作で発表できる
    • Zoomのコメント欄表示、ビデオカメラ表示など技術カンファレンスぽいことができる
  • デメリット

    • 発表資料をOBSに映す時に画角がずれる
    • 配信PC, 登壇者PCなどから音声を流してもオンライン側に音声が載らない

オフラインの配信風景

やってみての感想

今回は、初めてのハイブリット開催ということもあり、最小構成で配信設定してみました。最小構成でありながらオフライン、オンラインハイブリット開催を実現できたのは、よかったと思います。また、私はOBSを少しだけ触っていましたが、より配信周りの知見が広がったのは個人的にも嬉しかったです。
拙い文章で申し訳ないですが、以上とさせていただきます。
今回の記事は、実際にやってみないとわからない点などが多いかと思いますので、実際にテスト配信等をしてみていただけると良いかと思います。