【Flutter】UIのイベントをデバウンスする

こんにちは。エキサイト株式会社 モバイルアプリエンジニアの克です。

今回は、FlutterにおいてUIのイベントをデバウンスする手法についてです。

デバウンスについて

デバウンスとは、短時間に複数回のイベントが発生することを防ぐための手法です。
例えばイベントに対して100msのデバウンスを設定する場合、最後のイベントが発火してから100msが経過したタイミングで初めてそのイベントが通知されます。
100msが経過する前に次のイベントが発火した場合は、そのイベントからさらに100msが経過するまで通知は延期されていきます。

似たものとしてスロットリングがありますが、こちらはまず最初に発火したイベントを通知し、そこから一定時間が経過するまでに発火したイベントは無視するものとなります。

用途としては最新のイベントを重視する場合にはデバウンス、時間あたりのイベント数を制限したい場合にはスロットリングというように使い分けます。

まずはUIを実装する

デバウンスを適用するUIですが、今回はPageViewとSliderを連動させ、Sliderの操作時に選択されたページをPageViewに表示するというものにします。
これは画像のビューアーなどでよく使われる構成です。

まずはこれらのUIを実装していきます。
動作イメージとコードは下記の通りです。

class SliderPager extends HookWidget {
  const SliderPager({super.key});

  @override
  Widget build(BuildContext context) {
    final selected = useState(0.0);
    final pageController = usePageController();
    return Scaffold(
      body: Column(
        children: [
          Expanded(
            child: PageView.builder(
              controller: pageController,
              itemBuilder: (_, index) => Center(
                child: Text(index.toString()),
              ),
              itemCount: 100,
            ),
          ),
          Slider(
            value: selected.value,
            onChanged: (value) {
              pageController.jumpToPage(value.ceil());
              selected.value = value;
            },
            max: 100,
          ),
        ],
      ),
    );
  }
}

値の管理をシンプルにするためにflutter_hooksを使用しています。
選択中の位置はhooksを使いselected 変数で管理しています。

UIイベントにデバウンスを設定する

要件が単純な場合はこのままでも何も問題はないかと思います。
ただし、下記のようなケースではデバウンスを設定したほうがいい場合があります。

・アイテムの表示を計測しており、スライダーのシーク中は計測のイベントを送りたくない
・アイテムが表示された際に重い処理を実行しており、連続で実行されると困る

これらのケースを想定して、スライダーをシークした際のイベントにデバウンスを設定するようにします。

今回は公開されているライブラリのeasy_debounceを使用します。
内部的にはTimerを使用してデバウンスを実現しています。

コードを下記のように変更します。

EasyDebounce.debounce(
  'slider-debounce',
  const Duration(milliseconds: 100),
  () => pageController.jumpToPage(value.ceil()),
);

第1引数はデバウンスのタグで、この値を変えることで複数同時にデバウンスを設定することもできます。
第2引数はデバウンスの待機時間です。長くしすぎると操作時に違和感が出てしまうため調整したほうがいいでしょう。
第3引数が実際に実行する処理となります。

注意点として、selectedの更新にはデバウンスを設定しないようにします。
この値はSliderの表示に使用しているため、リアルタイムで更新する必要があります。

この変更による動作イメージは下記となります。

まとめ

デバウンスは便利な場面もありますが、多用するとUXに悪影響を及ぼします。
そのため、要件との折り合いを考えつつ手段の一つとしてスポットで活用するのがよさそうかなと思います。
様々な実装方法を組み合わせて使いやすいアプリを作っていきましょう。

採用情報

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを募集しております。
興味があれば是非ご連絡いただければと思います!

募集職種一覧はこちら www.wantedly.com

Docker 運用の Terraform に TFLint を導入する

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

運用している Terraform に TFLint を導入してみましたので、設定の方法や lint ルールの一例について紹介します。

TFLint とは

TFLint は、Terraform の lintです。 github.com

HashiCorp 非公式のサードパーティツールですが、Terraform の公式 Style Guide の中で紹介されています。 developer.hashicorp.com

Use a linter such as TFLint to enforce your organization's own coding best practices.

TFLint の設定

運用中の Terraform プロジェクトは、Docker で Terraform を実行していました。 TFLint の導入も Docker で行いました。

Docker の設定

まず、compose.yaml を説明します。

services:
  terraform:
    image: hashicorp/terraform:1.7.5
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/ash"]
    tty: true

  tflint:
    image: ghcr.io/terraform-linters/tflint
    volumes:
      - .:/terraform-demo
    working_dir: /terraform-demo
    entrypoint: ["/bin/sh"]
    tty: true

ディレクトterraform-demo/ 以下で Terraform コードを管理します。 Terraform を実行する際は、下記のコマンドで Docker 環境にログインします。

docker compose up -d terraform
docker compose exec terraform /bin/ash

TFLint の設定

続いて、TFLint を実行するために .tflint.hcl に設定を記述していきます。 公式の設定を参考にしています。

github.com

plugin "terraform" {
  enabled = true
  preset  = "recommended"
}

plugin "aws" {
  enabled = true
  version = "0.31.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

AWS を使っているプロジェクトなので、AWS 用のプラグインを有効にします。

TFLint を実行する

下記で TFLint を実行します。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive;\
"

実行結果の例です。

docker compose run --rm tflint -c "\
                tflint --init;\
                tflint --recursive;\
        "
Installing "aws" plugin...
Installed "aws" (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.31.0)

....

Warning: Missing version constraint for provider "aws" in `required_providers` (terraform_required_providers)

  on main.tf line 107:
 107: resource "aws_route53_record" "record" {

Reference: https://github.com/terraform-linters/tflint-ruleset-terraform/blob/v0.7.0/docs/rules/terraform_required_providers.md

terraform_required_providers のルールについて警告されました。 このルールは、Provider にはバージョンを必須で指定することを求めています。 内容の詳細は、表示されたリンクから確認できます。

次に、--fix オプションをつけて実行します。 --fix オプションでは、いくつかのルールを自動で修正してくれます。

docker compose run --rm tflint -c "\
    tflint --init;\
    tflint --recursive --fix;\
"

--fix オプションで適用されたルールの例です。

  • terraform_deprecated_index
  • terraform_unused_declarations

terraform_deprecated_index のルールは、配列参照のレガシーな記法を指摘するものです。 --fix オプションでは、自動で角括弧の記法に修正します。

github.com

terraform_unused_declarations のルールは、使用されていない変数の宣言を指摘するものです。 --fix オプションでは、自動で変数の宣言を削除します。

github.com

注意点として、自動修正の内容によっては構文エラーが起きます。 例えば、 terraform_unused_declarations の場合、module で定義していた使われていない引数が削除されることがあります。module を参照していた側では、削除された引数を残したままになるため、構文エラーになります。 自動適用後の terraform plan の確認を行いましょう。

最後に

Docker 運用の Terraform プロジェクトに対する TFLint の設定と lint ルールの一例を紹介しました。 運用中のコードに対して lint を実行すると、様々な警告が指摘されると思います。 修正によってはエラーが起きていることがあるので、確認をしながら修正しましょう。

参考になれば幸いです。

採用アナウンス

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

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

ModelMapperの使い方

はじめに

こんにちは、新卒2年目の岡崎です。今回は、ModelMapperの使い方について紹介します。

環境

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

ModelMapperについて

知っている人も多いと思いますが、改めておさらいしていきたいと思います。ModelMapperでは、違う型同士の値をコピーし、反映することができます。

例えばAオブジェクトとBオブジェクトがあり、AオブジェクトからBオブジェクトへ値を反映させたいケースがあったとします。この時、ModelMapperを使って行うこともでき、とても便利です。

それでは、実際にコードで見てみましょう!

準備

build.gradleに以下の実装をしてください。

dependencies {
  // model mapper
  implementation 'org.modelmapper:modelmapper:{モデルマッパーの最新バージョン}'

  // lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
}

実装

以下のように、NovelDtoオブジェクトとNovelオブジェクトが存在していると仮定します。

@Data
@Accessors(chain = true)
public class NovelDto {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;
}
@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

    /**
     * ユーザーID
     */
    private Long userId;

    /**
     * 高評価数
     */
    private Long likeTotal;

    /**
     * 低評価数
     */
    private Long dislikeTotal;

    /**
     * 高評価したかどうか
     */
    private Boolean isLiked = false;

    /**
     * 低評価したかどうか
     */
    private Boolean isDisliked = false;

    /**
     * 自分の小説かどうか
     */
    private Boolean isMyNovel = false;

    /**
     * 作成日時
     */
    private LocalDateTime createdAt;

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

ほとんどのプロジェクトでは層によって使用するオブジェクトが決まっていると思います。例えば、Service層ではRepository層から受け取ったNovelDtoオブジェクトを、Novelオブジェクトへ反映します。

この時の実装例を以下に示します。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);

        // 中略

        final Novel novel = new Novel()
            .setid(dto.getId())
            .setUserId(dto.getUserId())
            .setLikeTotal(dto.getLikeTotal())
            .setDislikeTotal(dto.getDislikeTotal())
            .setIsLiked(dto.getIsLiked())
            .setIsDisliked(dto.getIsDisliked())
            .setIsMyNovel(dto.getIsMyNovel());

        return novel;
    }
}

このコードの問題点は、setし忘れが起きてバグの原因になる可能性があることです。(もしかすると、このようなコードが煩雑に感じる人もいるかもしれません。)

ModelMapperを使えば、以下のようにコードを書けます。

@Service
@RequiredArgsConstructor
public class NovelService {
    private final NovelRepository novelRepository;
    private final ModelMapper modelMapper;

    public Novel getNovel(Long id) {
        final NovelDto dto = novelRepository.findNovel(id);
        
        return modelMapper.map(dto, Novel.class);
    }
}

一行で書くことができるので、コードはシンプルになり、setのし忘れは起きないようになります。

ちなみにListのオブジェクトの反映は、以下のように行います。

List.of(modelMapper.map(dtoList, 反映したいclass[].class));

補足

ModelMapperを使って開発していると、UnrecognizedPropertyExceptionが発生することがあります。

これはオブジェクト同士を比較した時、存在しない属性があることが原因です。

様々な解決方法があると思いますが、今回は@JsonIgnoreを使う方法を紹介します。

まずは、build.gradleに依存関係を追加します。

dependencies {
        implementation "org.mybatis.spring.boot:mybatis-spring-boot-starter:${springMybatisStarterVersion}"
}

そして、オブジェクト同士を比較して存在しない属性に対し、@JsonIgnoreを追加します。

@Data
@Accessors(chain = true)
public class Novel {
    /**
     * ID
     */
    private Long id;

   // 中略

    /**
     * オブジェクトが空かどうか判定する.
     *
     * @return オブジェクトが空かどうか
     */
    @JsonIgnore
    public Boolean isEmpty() {
        return Objects.isNull(this.commentId);
    }
}

これでエラーが発生しなくなりました。

最後に

今回は、ModelMapperの使い方を紹介しました。ModelMapperはとても便利なので、使ってみてください。

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

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

www.wantedly.com

PHPカンファレンス小田原で初スタッフに挑戦しました!

こんにちは!エキサイト株式会社のまさきちです。

先日、PHPカンファレンス小田原でスタッフしてきました。 今までカンファレンススタッフの経験は無くドキドキでしたが振り返っていきます。

PHPカンファレンス小田原とは

2024年4月13日(土)におだわら市民交流センターUMECOにて開催されたPHPカンファレンスです。(初開催)

PHP関連のイベントは色々な場所で開催されておりますが、小田原の魅力がいっぱい詰まったカンファレンスでした! phpcon-odawara.jp 私は会場&前夜祭お手伝いと、当日スタッフとして参加しました。

会場準備お手伝い

開催日の前日に小田原の会場に行き準備のお手伝いをしました。(実はこの時に初めて小田原に来た)

会場に荷物を運んだり、机と椅子を並び替えたり、配信機材の準備を行いました。 思ったよりやる事が多くて大変でしたが、これから始まるカンファレンスをみんなで作り上げている感じがとても楽しかったです。

前夜祭

前夜祭では受付と写真撮影を担当しました。

受付時にチケットの種類がいくつかあり、条件分岐に戸惑ってしまう場面もありましたが、スタッフで協力し合って乗り切りました。 参加者の皆様にもご配慮いただき助かりました。

受付後にPHPer同士でIRTを行い、写真撮影を担当したのですが、 皆さん盛り上がっていて楽しそうな写真がたくさん撮れて嬉しかったです。

カンファレンス当日

当日は準備や片付けに加え、主に「かま」会場スタッフ、LTタイムキーパーの役割を担当しました。

会場スタッフ

カンファレンスは主に、「かま」と「ぼこ」の二つの会場に分かれて行いました。 「かま」会場では、配信機材や照明の管理、トーク司会、誘導係の3つの役割分担を決めてローテーションしていました。

配信機材を触ったことが無いのでうまく出来るか不安でしたが、操作マニュアルを作っていただけたり、詳しい方が使い方を丁寧に説明してくださっていたので安心して取り組む事ができました。

トーク司会では、登壇者の準備のお手伝いや、タイトル読み上げ、タイムキーパーなど行いました。 こちらも初めての経験で不安でしたが、トークスクリプトを用意してくださっていたので、迷う事なくこなす事ができました。

誘導係は、トークを終えたスピーカーの方を別会場へ案内したり、会場の扉を開け閉めしたり、サブ的な役割をこなしていました。

初めての事ばかりでしたが、コアスタッフの方々が念入りに準備してくださっていたので問題なく対応できました。

LTタイムキーパー

カンファレンス終盤のLTではタイムキーパーを担当しました。 LTには小田原のゆるキャラ「梅丸」が来てくれて大盛り上がり! 私はLT終了時間が来たら梅丸が持っているドラを叩きを担当。(梅丸、ドラ持つの大変だったと思うけどお疲れ様でした) 途中機材トラブルなどもありましたが、周りの方々の助けもあり、無事乗り切る事ができました。

おだわランチ

昼休憩はスタッフの方たちと小田原のランチを楽しみました。 スタッフがお勧めのお店をnoteに書いてくださったので、そちらのお店に行きました。美味しかったし、お店の人優しくて感動。

ふりかえり

カンファレンス終了後にスタッフでふりかえりを行いました。 360°Thanksとして自分を含めたメンバーそれぞれに感謝したり、GoodとMoreを出す作業をMiroを使って付箋に書き出しながら行いました。 振り返ってみると課題もたくさん見つかったので、Nextアクションに繋げていければいいなと思います。

まとめ

初めてのカンファレンススタッフで大変だったけど挑戦してよかった。 カンファレンスの裏側を知る事ができたし、機材や進行などを実体験できて学びが多かった。 PHPerの温かさに再度気づくことができました。 スタッフに興味がある方はぜひトライしてみてください。

Figmaのローカルバリアブルを使ってテキストにスタイルを適用してみる

こんにちは!SaaS・DX事業部デザイナーの鍜治本です!
担当しているサービス「KUROTEN」のUI設計をする際にFigmaを使っており、デザインシステムの構築を日々進めています。
そんな中、スタイルの定義を行う際に気がついた機能があるので、TIPSとして記事にしてみました。

Figmaのローカルバリアブル機能の概要

Figmaにはファイル内で色やフォントなどの情報をライブラリとして扱うことができる、「スタイル」「バリアブル」という機能があります。どちらの機能も他のファイルで引用でき、UI制作においては一貫した管理がしやすいメリットがあります。
細かい機能については、Figma公式のサイトからどうぞ🙆
バリアブルとスタイルの違い

文字列変数「String variable」がフォントのプロパティに対応できるように

これまでバリアブルの変数は、色・変数・文字列・ブーリアンの4種類があり、フォント以外のプロパティには概ね設定できていました。
アップデートによってテキストプロパティ(フォントサイズ・ウェイト・行間・文字間隔・段落間隔など)にも、変数を指定することができるようになり、スタイル機能だけでなく、バリアブル機能にも同様な管理ができるようになりました。

ローカルバリアブルを設定してみる

担当しているサービス「KUROTEN」でもスタイル機能を用いてフォントを管理しているのですが、日本語と英数字とでフォントを分けているためスタイル機能を解除することが起こります。
例えば「2024年5月10日」のような文字列の場合、数字部分はRoboto・漢字はNoto Sans JPとしたいため、Figmaで再現する場合はスタイル解除し直接プロパティを編集する必要があります。

合成フォントを想定しているが、Figma上はフォントごとに設定する手間が生じる
タイポグラフィとして定義はしているものの、常に覚えていられるものでもなく、毎度正しい値を確認して作業するのは手間になります。さらに、複数人で作業をするのであれば尚更、どれが正解かわからなくなってしまいますよね。
今回は、ローカルバリアブルにもタイポグラフィを管理できるように設定していきます。
かなり自己流で手探りなので、知見がある方いらっしゃればぜひアドバイスをお願いします🙇

①数値・文字列の登録をする

色やタイポグラフィを定義している「Foundations」とボタンやフォームなど管理する「Components」
前提として、KUROTENのデザインシステムをFigmaに反映させるために、2つのファイルをそれぞれ用意しています。色やタイポグラフィなどを定義している「Foundations」と、ボタンなどパーツ化されたものを保管している「Components」です。
今回設定したいフォントのスタイルは文字列に関する変数であるため、Foundationsのローカルバリアブルに値を設定していきます。
ローカルバリアブルへの登録手順
1. はじめにローカルバリアブルを開き、モーダル一番下にある「バリアブルを作成」から『文字列』のメニューを選択します。
2. Tアイコンの入った行が追加されるので、[名前]の列には管理する上でわかりやすい名前を、[値]の列にはフォント名を入力します。フォント名はFigma上にあるものと同じでないと反応しないため、間違えないよう確認してください🙆
今回は[名前]に「Noto Sans」、[値]に「Noto Sans JP」と入力しました。

ちょっとした余談ですが、[名前]列に入力する際、『Typograhpy/Noto Sans』のように「/(スラッシュ)」を入れることで、Typography部分をグループ名とした階層構造を作成できます。フォント以外にフォントサイズや行間、HeadingやBodyなど種類違いで管理する場合は、グループごとに設定することで管理がしやすいです。

②テキストスタイルにバリアブルを設定する

テキスト箇所へバリアブルを設定していく手順
①で設定したバリアブルを、テキストのスタイルに適用していきます。
3. 適用させたいテキストを選択し、プロパティのフォント名をクリックして、メニューを表示します。
4. フォント選択メニューの右上に「バリアブル」アイコンが表示されているので、クリックしてバリアブル適用メニューを表示させます。
5. バリアブルメニューの中には、先のローカルバリアブル登録手順で登録した文字列変数(今回はNoto Sans)が表示されているので、クリックします。
6. バリアブルを設定したテキストのフォント箇所を見ると、フォント名が表示されry箇所がチップのように表示されます。
この手順で、テキストにバリアブルを設定できました!

またフォント名だけでなく、数値など駆使することで、ウェイト・サイズ・行間・文字間隔・段落間隔までバリアブルの指定ができるようになります。

フォント以外の項目もバリアブルを設定してみた図(文字間隔・段落間隔以外)
現状全てに対応する必要性はそこまで見出せていないものの、バリアブルを設定すると作業効率があがるUI(Webサイト制作や一括で切り替えたい場面など)においては、かなり有力な機能ではないでしょうか。

バリアブルの表示範囲が指定できるように

バリアブルの設定では、変数を表示させられる範囲が指定できます。

適用範囲を指定するとプロパティ内で候補の制限ができる
例えば色であれば、塗りだけに適用させたいものがあったり、テキストに適用させてシェイプ・フレームには適用させないルールを設けている場合がありますね。
数値のバリアブルでの適用範囲設定
ローカルバリアブルでは、こういった適用範囲も細かく指定できます。

行間・文字間隔・段落間隔のローカルバリアブルが出てこない原因

テキスト関連のプロパティを設定していた際に遭遇した落とし穴について共有します。
先ほど数値の変数も適用範囲が指定できる話をしましたが、「行間」「文字間隔」「段落間隔」の項目に『テキストコンテンツ』の適用範囲を指定しても、候補に表示がされない事象に遭遇しました。

行間にも「テキストコンテンツ」の適用範囲を指定すると、プロパティの候補に表示されない
サイズやウェイトでは問題なく候補に表示されるのですが、「行間」「文字間隔」「段落間隔」のみ適用範囲を制限すると候補として表示されなくなってしまいます。

解決方法

解決方法は、適用範囲を設けず「サポートされているプロパティの全てで表示」を選択するだけで回避できます。

バリアブルの適用範囲を修正し、テキストプロパティの候補に表示を確認
候補件数が増えてしまうので探しにくくなってしまいますが、「①数値・文字列の登録をする」の中でお伝えしたグループ化や、変数名の検索などで改善できそうです。

まとめ

今回はローカルバリアブル機能を活用して、テキストへのスタイル適用についてまとめさせていただきました。
最初の設定がやや手間にはなりつつも、ある程度ルールを設けることでアウトプットのしやすさ改善につながったり、複数人で作業する場面での齟齬を未然に防げると思います。
またテキストのプロパティにバリアブルを設定する箇所の思わぬ落とし穴についても、回避術共有になれば嬉しいです🙌

おわりに

エキサイトといえばtoC向けサービスが印象的ですが、新規事業としてtoB領域のプロダクトも展開しています🙆
エンジニアはもちろん、デザイナーも新卒・中途を問わず採用募集していますので、カジュアル面談からでもお話ししませんか?
ご興味があればぜひお気軽にご連絡ください🙌

www.wantedly.com

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がその対応をしてくれると良いですね。

では、また次回。

参考文献