Gradleマルチプロジェクトでは、Mavenリポジトリは絶対パスで指定すべきだった話

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

今回は、Gradleでマルチプロジェクトを構成している状態で、MavenのLocalRepositoryを指定する際にハマった内容を紹介します。

デフォルト以外のLocalRepository

今回は、repositoriesの設定で独自のRepositoryを指定することを目的としていました。

Maven Repositoryに上げられているようなグローバルに公開されているライブラリを利用する際には、

repositories {
    mavenCentral()
}

のように記述します。

ローカルにあるRepositoryを設定したい場合には

repositories {
    mavenLocal()
}

のように記述します。
デフォルトでは、LocalRepositoryは$USER/.m2/repositoriesにあります。

しかし、時にはプロジェクトごとに分けたいという理由で別のローカルリポジトリを指定したい場合もあります。

今回は、local_repositoryというディレクトリをプロジェクトルート直下に作成して、そこにライブラリを入れているとします。

この場合、build.gradleに以下のように記述することで、指定することができます。

repositories {
        maven {
            url "./local_repository"
        }
    }

urlには、サーバー上のリポジトリやS3のようなオブジェクトストレージにあるライブラリへのパスを記述できますが、ローカル環境にあるリポジトリの場所を指定することもできます。

以下のように、プロジェクトに依存関係を追加することができます。

dependencies {
    implementation "com.example.rh:sample:0.0.1-SNAPSHOT"
}

このように一つのプロジェクト内のみでの設定であれば問題ないのですが、allprojectssubprojectsを利用して複数のプロジェクトに対して設定を行おうとしたときにハマってしまいました。

以下が、ハマったときのプロジェクトルート直下のbuild.gradleの全体です。

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

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

java {
    sourceCompatibility = '21'
}

allprojects {
    repositories {
        mavenCentral()

        maven {
            url "./local_repository"
        }
    }
}

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

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

project(':sub_project') {
    dependencies {
        implementation "com.example.rh:sample:0.0.1-SNAPSHOT"
    }
}

allprojectsmavenCentralとプロジェクトルート直下の./local_repositoryからライブラリを取得することを指定しています。
また、:subprojectというサブプロジェクトに./local_repositoryから"com.example.rh:sample:0.0.1-SNAPSHOT"という依存関係を追加しています。

この状態で、gradleプロジェクトをビルドすると、以下のようなエラーが発生します。

:sub_project:main: Could not find com.example.rh:sample:0.0.1-SNAPSHOT.
Required by:
    project :sub_project

com.example.rh:sample:0.0.1-SNAPSHOTが見つからないというエラーのようですが、allprojectsブロックでrepositoriesは指定しているはずです。

これはどういうことでしょうか。
解決法はシンプルで、LocalRepositoryの指定を絶対パスにします。

allprojects {
    repositories {
        mavenCentral()

        maven {
            url "$rootDir/local_repository"
        }
    }
}

allprojectsでLocalRepositoryを指定した際に、その指定したプロジェクトから見たパスで全体に対して取り込まれると勘違いしていました。
しかし、実際には各プロジェクトから見たパスで参照しているようです。

したがって、subprojectというサブプロジェクトはdependency_testの配下にあり、dependency_test/subprojectにはlocal_repositoryというrepositoryが存在しないため、依存関係を解決できなかったようです。

仕様を理解していれば当然の結果ではありました。

終わりに

今回は、マルチプロジェクトにおけるLocalRepositoryを指定する際にハマった内容を紹介しました。

では、また次回。

SpringBootで複数のapplication.ymlの読み込んで、環境ごとの起動をラクにする

エキサイト株式会社エンジニアの佐々木です。SpringBootではapplication.ymlなどの設定情報を読み込む方法がいくつかあるのでまとめます。

前提

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

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

設定ファイル一覧

設定

下記の3ファイルになります。

- application.yml
- application-a.yml
- application-b.yml
- application-c.yml

コード

リクエストされたら、application.ymlで設定されてデータが返却されます。

@RestController
@RequestMapping("yml")
@RequiredArgsConstructor
@Slf4j
public class ApplicationYmlController {

    @Value("${jp.co.excite.name}")
    private String value;

    @GetMapping("")
    public String applicationYml() {
        return value;
    }

}

起動引数で設定される値を変える

  • 引数なし
java -jar yml-test.jar

curl http://localhost:8080/yml
default

引数がない場合は、application.ymlが使用されます。

  • 引数 --spring.profiles.active=a
java -jar yml-test.jar --spring.profiles.active=a

curl http://localhost:8080/yml
a

引数がある場合は、デフォルトのプロファイルと指定されたプロファイルが使用されます。 この場合は、application.ymlapplication-a.ymlが読み込まれます。

  • 引数 --spring.profiles.active=b
java -jar yml-test.jar --spring.profiles.active=b

curl http://localhost:8080/yml
b

別のプロファイルを指定してもちゃんと読み込まれています。

  • 引数 --spring.profiles.active=a,b
java -jar yml-test.jar --spring.profiles.active=a,b

curl http://localhost:8080/yml
b

java -jar yml-test.jar --spring.profiles.active=b,a

curl http://localhost:8080/yml
a

複数指定する場合は、後勝ちになります。指定された順番で上書きしていくイメージです。

補足./gradlew bootRun の場合

gradle

 ./gradlew bootRun --args='--spring.profiles.active=a'

設定ファイルの中で指定する場合

設定ファイルの中でも、他の設定ファイルを読み込むことが可能です。spring.config.importを使用します。

spring:
  config:
     import: classpath:${プロファイルのファイル}

上記で指定できます。こちらも後勝ちなので、設定には注意が必要です。

まとめ

SpringBootでは、環境によって設定ファイルをリネーム、設定の書き換えなどの操作は、基本的に行わずに起動引数によって解決するようになっています。これはとても便利なので、多用していきたいところです。

さいごに

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

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

Spring AI に入門する

エキサイト株式会社エンジニアの佐々木です。詳細は話せませんがAI系の業務が発生したので、技術選定の一環でSpring AIを触ってみました。

前提

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

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

手順

Spring AI 公式サイトの手順に従って進めてみます。

1. Spring CLI をインストールする

Spring CLIをインストールします。ここではMacのコマンドになります。

brew tap spring-cli-projects/spring-cli
brew install spring-cli

2. Spring CLIでAI用のプロジェクトを作成する

spring-ai-sampleプロジェクトを作成します。

spring boot new --from ai --name spring-ai-sample

3. OpenAIのAPIキーを取得します

OpenAIのapi-keysにいき、APIキーを取得します。

4. 環境変数APIキーをセットします

export SPRING_AI_OPENAI_API_KEY=${取得したAPIキー}

5. SpringBootを起動します

下記コマンドを投入し、SpringBootを起動します。

cd spring-ai-sample
./mvnw spring-boot:run

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

2024-04-17T13:27:14.877+09:00  INFO 80604 --- [           main] o.s.a.o.samples.helloworld.Application   : Starting Application using Java 17.0.1 with PID 80604

6. リクエストしてみる

8080ポートでSpringBootが起動していますので、curlでリクエストしてみます。

curl http://localhost:8080/ai/simple --get --data-urlencode 'message=What is Java?'

{"generation":"Java is a high-level programming language developed by Sun Microsystems (now owned by Oracle) in 1995. It is known for its platform independence, meaning that Java programs can run on any device or operating system that has a Java Virtual Machine (JVM) installed. Java is widely used for developing a variety of applications, including desktop, web, mobile, and enterprise software. It is also popular for developing server-side applications and Android mobile apps."}

サクッと応答がしてもらえました。

内部実装

簡単にですが、内部実装をみていきます。

ライブラリ

ライブラリとしては、AI用としては下記の2つが入っています。

org.springframework.ai:spring-ai-bom
org.springframework.ai:spring-ai-openai-spring-boot-starter

コード

生成されていたコードとしては、下記のようなコードになっていました。

@RestController
public class SimpleAiController {

    private final ChatClient chatClient;  

    @Autowired     // ChatClientを使えるようにしている
    public SimpleAiController(ChatClient chatClient) {  
        this.chatClient = chatClient;
    }

    @GetMapping("/ai/simple")
    public Map<String, String> completion(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
        return Map.of("generation", chatClient.call(message));   // リクエストをなげている
    }
}

SpringBootを使用されている方には見慣れたコードですね。ChatClient、EmbeddedClientなどいくつかあるので、これから試してみようと思います。

Spring AIで使用できる一覧

Spring AIで使用できる各API一覧です。Claude3とかはないですが、一通りある感じです。

Chat Models

    OpenAI
    Azure Open AI
    Amazon Bedrock
    Google Vertex AI Palm
    Google Gemini
    HuggingFace - access thousands of models, including those from Meta such as Llama2
    Ollama - run AI models on your local machine
    MistralAI

----
Text-to-image Models
    OpenAI with DALL-E
    StabilityAI

----
Transcription (audio to text) Models
    OpenAI

----
Embedding Models

    OpenAI
    Azure OpenAI
    Ollama
    ONNX
    PostgresML
    Bedrock Cohere
    Bedrock Titan
    Google VertexAI
    Mistal AI

まとめ

Spring AIを触ってみましたが、かなり簡単にAPIに接続できました。手軽ですし、既存のサービスへのアドオンとしてはさっとできそうなのでよさそうです。。今後、試していこうと思います。

さいごに

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

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

Thymeleafでハイパーリンクを実装する

はじめに

こんにちは、新卒2年目の岡崎です。今回は、Thymeleafでハイパーリンクを実装する方法を紹介します。

環境

  • Thymeleaf 3.3.0

前提

以下のようなオブジェクトが存在することを仮定します。

@Data
public class Test {
    private Long id;
    
    private String name;
    
    // 以下略
}

オブジェクトのリストを表示する場合、以下のコントローラーを使います。

    @GetMapping("list/test")
    public String getTestList(
            Model model
    ) {
            final List<Test> testList = testUseCase.getTestList();

            model.addAttribute("testList", testList);

            return "test/list/index";
    }

また、オブジェクトを取得する場合は、idから表示します。

    @GetMapping("test")
    public String getTest(
            @ModelAttribute Model model,
            TestRequestDto request,
    ) {
            final Test test = testUseCase.getTestById(request.id(), request.type());

            model.addAttribute("test", test);

            return "test/index";
    }
public record TestRequestDto(
        @NotNull @Positive
        Long id,

        @Nullable 
        String type
) {
}

test/list/index.htmlを用意します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <title>Test List</title>
    <meta charset="utf-8"/>
</head>

<body>

</body>
</html>

test/index.htmlを用意します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
    <title>Test</title>
    <meta charset="utf-8"/>
</head>

<body>
   <div>sample</div>
</body>
</html>

実装

前提で紹介したリストの表示を行います。

test/list/index.htmlを変更します。

<body>

   <th:block th:each="test : ${testList}">
       <tr>
           <td th:text="${test.getId()}"></td>
           <td th:text="${test.getName()}"></td>
                // 以下略
       </tr>
   </th:block>

</body>
</html>

idをタップすると、詳細画面を見ることができるようにするため、ハイパーリンクを作成します。

<th:block th:each="test : ${testList}">
    <tr>
        <td>
          <a th:href="@{/test(id=${test.getId()})}" th:text="${test.getId()}"></a>
        </td>
        <td th:text="${test.getName()}"></td>
                // 以下略
    </tr>
</th:block>

th:href=@url(パラメーター名=${変数名})と書くことで、変数の値をクエリーパラメーターとして指定することができます。

ハイパーリンクに、パスのクエリーパラメーターを使う方法

実際のケースで、ハイパーリンクに、パスのクエリーパラメーターを使いたい場合がありました。この時の実装例を紹介します。

下記のようなパスが存在していると仮定します。

http://localhost8080/test/list?type=TEST

<div th:with="type=${param.type}">
  <a th:href="@{/test(id=${test.getId(), type=${type})}" th:text="${test.getId()}"></a>
</div>

この場合、<div th:with="クエリーパラメーター=${param.クエリーパラメーター}"> 〜 </div> と実装すれば、そのdivタグの中でクエリーパラメーターを使うことができます。

最後に

今回は、Thymeleafでハイパーリンクを実装する方法について紹介しました。

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

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

www.wantedly.com

JavaでURIを作る方法

はじめに

こんにちは、新卒2年目の岡崎です。今回は、JavaURIを作る方法について紹介します。

前提

openjdk version "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)
  • Spring Boot
  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.1)

実装

色々な方法があると思いますが、今回はUriComponentsBuilderを使って実装する方法を紹介します。

UriComponentsBuilderは、Spring Bootで用意されているコンポーネントです。これにより、文字列結合をしなくても、URIを作ることができます。

final String uri = UriComponentsBuilder
          .fromUriString("http://localhost:8080/test")
          .queryParam("service", service)
          .fragment("test1")
          .encode()
          .build()
          .toUriString();

作られるURI

http://localhost:8080/test?service=service#test1

fromUriString

fromUriString(”パス名”)で、URIのパスを指定することができます。

queryParam

ここでは、クエリーパラメーターを指定することができます。queryParam(”パラメーター名”, パラメーター)と実装します。

例えば、

         .fromUriString("http://localhost:8080/test")
          .queryParam("service", "DEMO")

と実装した場合、http://localhost:8080/test?service=DEMOというURIが作られます。

fragment

URIのフラグメントを指定することができます。fragment(文字列)と実装することで、指定した文字列のフラグメントが作られます。

例えば、

         .fromUriString("http://localhost:8080/test")
          .queryParam("service", "DEMO")
          .fragment("demo1")

と実装した場合、http://localhost:8080/test?service=DEMO#demo1 というURIが作られます。

最後に

今回は、JavaURIを作る方法について紹介しました。

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

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

www.wantedly.com

SpringBootの階層の深いapplication.ymlをrecordで取り出す

エキサイト株式会社メディア事業部エンジニアの佐々木です。Javaにrecord型が登場し、SpringBootでも結構いろいろなところで使用できるようになりました。今回は、application.ymlからrecordを使用して取り出す方法になります。

前提

環境は下記になります。

$ 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

------------------------------------------------------------
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

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

application.yml

取り出したいapplication.ymlの情報は下記になります。アプリケーション内で使用する国情報みたいなデータです。

jp:
  co:
    excite:
      country:
        - name: japan
          lang:
            - ja
          data:
            population: 126000000
            capital: tokyo
            currency: yen
            timezone: Asia/Tokyo
        - name: united states
          lang:
            -  en
          data:
            population: 328000000
            capital: washington
            currency: dollar
            timezone: America/New_York

コード

コードとしては、下記になります。

@ConfigurationProperties(prefix = "jp.co.excite")
public record LangListConfig (List<CountryType> country) {
    record CountryType(String name, List<String> lang, CountryData data) {
        record CountryData(String population, String capital, String currency, String timezone) {}
    }
}

// 出力
LangListConfig[
  country=[
    CountryType[name=japan, lang=[ja], data=CountryData[population=126000000, capital=tokyo, currency=yen, timezone=Asia/Tokyo]],
    CountryType[name=united states, lang=[en], data=CountryData[population=328000000, capital=washington, currency=dollar, timezone=America/New_York]]
  ]
]

SpringBoot3系だと、この行数でタイプセーフに記述することが可能です。設定ファイルがいくらネストしていても、同じようにrecordをネストすれば取り出せます。

まとめ

SpringBootで利用しているライブラリは、SnakeYAMLで結構いろいろできます。どんどんタイプセーフにしていきましょう。

最後に

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

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

htmxのhx-swap-oobとhx-select-oobを理解する

エキサイト株式会社エンジニアの佐々木です。htmxのhx-swap-oobとhx-select-oobを理解します。

前提

HTML内にhtmxをロードしてください。

<script src="https://unpkg.com/htmx.org@1.9.11" integrity="sha384-0gxUXCCR8yv9FM2b+U3FDbsKthCI66oH5IA9fHppQq9DDMHuMauqq1ZHBpJxQ0J0" crossorigin="anonymous"></script>

hx-swap-oob

hx-swap というものがあります。これは、戻り値をどのような形で呼び出し元のHTMLに適用するかというものになります。hx-swap-oobは、そこでは記述できないときに使用するアトリビュートになります。

では、hx-swap-oob はどこに記述すればいいか?ということですが、これはレスポンス側に記述します。

リクエスト側のHTMLには、通常通りサーバのエンドポイントを指定するのと、レスポンスされたHTMLのどの要素を使うかを指定します。(指定しなければ、戻り値のHTMLがすべて適用されます)

<!-- リクエスト側のHTML -->

<div>
  フォーム
  <button hx-get="sendRequest" hx-target="OuterHTML" hx-select="#response">送信ボタン</button>
</div>

<div id="status">結果: 未送信</div>

レスポンス側のHTMLでは、 id="status" の更新も行いたいとしましょう。その場合は、 hx-swap-oob="true"アトリビュートに定義すると、リクエスト側のHTMLの中身が書き換わります。

<!-- レスポンス側のHTML -->

<div id="reponse">レスポンスを受け付けました</div>

<div id="status" hx-swap-oob="true">結果: OK</div>

hx-select-oob

hx-selectは、戻り値のHTMLのどの属性を使用するかというアトリビュートになります。hx-swap,hx-target,hx-selectは協調して動作します。そので指定できない他の要素をhx-select-oobで記述することができます。hx-select-oob の記述箇所は、リクエスト側のHTMLになります。レスポンス側のHTMLの中身をCSSセレクターを使用して取り出す機能になります。

リクエスト側のHTMLには、hx-getで通常通りサーバのエンドポイントを指定するのと、hx-selectでレスポンスされたHTMLのどの要素を使うかを指定します。hx-select-oobでは、レスポンスされたHTMLのどの部分を使うかを指定できます。

<!-- リクエスト側のHTML -->

<div>
  フォーム
  <button hx-get="sendRequest" hx-target="OuterHTML" hx-select="#response" hx-select-oob="status">送信ボタン</button>
</div>

<div id="status">結果: 未送信</div>

レスポンス側のHTMLは特別なことは何も行いません。

<!-- レスポンス側のHTML -->

<div id="reponse">レスポンスを受け付けました</div>

<div id="status">結果: OK</div>

まとめ

hx-swap-oobhx-select-oobは、同じような機能でリクエスト側とレスポンス側に記述するかの違いくらいです。リクエスト側で判定できるのであればhx-select-oob、レスポンス側で判定するならhx-swap-oobを使用する感じです。主にメッセージ処理等で便利に使えますので利用していければと思います。

最後に

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

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

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