[htmx] hx-triggerを使ってinfinitScrollを実装する方法[Java/Spring Boot]

はじめに

こんにちは、新卒2年目の岡崎です。今回はhx-triggerを使ってinfinitScrollを実装する方法を紹介します。

環境

  • gradle
------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

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.3 aarch64
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)

今回は、htmxを使っているため、以下の依存関係をbuild.gradleに追加します。

implementation "org.webjars.npm:htmx.org:1.9.10"

実装

hx-triggerを使うことで、infinitScrollを簡単に実装できます。

hx-triggerの詳細は、公式ドキュメントをご覧ください。

htmx.org

Controller

まずは、Controllerにエンドポイントのを作成します。

  • /scrollは、最初にアクセスした際の画面表示用エンドポイントです。
  • /scroll/newは、スクロール時に呼び出されるエンドポイントです。
@Controller
@RequestMapping("scroll")
public class ScrollController {
    @GetMapping
    public String get(
            Model model
    ) {
        final List<String> list = List.of(
                "test1",
                "test2",
                "test3",
                "test4",
                "test5",
                "test6",
                "test7",
                "test8",
                "test9",
                "test10"
        );

        model.addAttribute("list", list);

        return "scroll/index";
    }

    @GetMapping("new")
    public String getNew(Model model) {
        final List<String> list = List.of(
                "newTest1",
                "newTest2",
                "newTest3",
                "newTest4",
                "newTest5",
                "newTest6",
                "newTest7",
                "newTest8",
                "newTest9",
                "newTest10"
        );

        model.addAttribute("list", list);
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        return "scroll/index :: scroll";
    }
}

html

  • scroll/layout.html

レイアウトファイルを作成しました。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja"
      th:fragment="layout(content)"
>
<head>
    <title>Demo</title>

    <script src="/webjars/htmx.org/1.9.10/dist/htmx.min.js"></script>
</head>
<body>
<header>
    header
</header>

<div th:replace="${content}">
    <p>Page content goes here</p>
</div>

<footer>
    footer
</footer>
</body>
</html>
  • scroll/index.html

画面にlistの内容を表示します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="ja" th:replace="~{scroll/layout::layout(~{::content})}">
<body>
    <th:block th:fragment="content">
        <th:block th:insert="~{scroll/index::scroll}"></th:block>
    </th:block>

    <th:block th:fragment="scroll">
        <div class="container text-center mt-5">
            <th:block th:each="row : ${list}">
                <div th:text="${row}"></div>
            </th:block>
        </div>
        <div>
            <span hx-indicator="#indicator"
                  hx-target="closest div"
                  hx-trigger="revealed"
                  hx-swap="outerHTML"
                  th:hx-get="@{/scroll/new}">
                Loading......
            </span>
        </div>
    </th:block>
</body>
</html>

下記の部分がスクロール部分の実装です。

            <span
                  hx-target="closest div"
                  hx-trigger="revealed"
                  hx-indicator="#indicator"
                  hx-swap="outerHTML"
                  th:hx-get="@{/scroll/new}">
                Loading......
            </span>

今回は、スクロール時にリクエストを発火させたいので、hx-triggerrevealed を指定しました。これにより、指定された div タグがスクロールで表示されると、/scroll/new が呼び出されます。

また、hx-indicator="#indicator" を使用してローディング処理を実装しました。

最後に、スクロール時の挙動を確認できたら、完了です。

最後に

今回はhx-triggerを使ってinfinitScrollを実装する方法を紹介しました。みなさんもぜひ実装してみてください。

第11回テクデザBeer Bashを開催しました

こんにちは、エキサイトでエンジニアをしている吉川です。 先日3/14(金)に社内イベントの「テクデザBeer Bash」を開催したので、運営視点でレポートを書いていきます。

テクデザBeer Bashとは

Beer Bashとはbeer(ビール)+ bash(にぎやかなパーティー)を合わせた造語で、真面目な部分を残しつつ、カジュアルな雰囲気で交流を行うイベントです。 年に3、4回社内カフェスペースで開催しており、同じチーム内の人はもちろん、業務ではあまり関わることがない他部署の人たちとも繋がる場になっています。

当日のコンテンツ

前半は以下2つのメインコンテンツを発表し、後半はフリートークの時間にしました。

  • 録音分析の技術スタック紹介
  • 電話占いの文字起こしと要約

録音分析の技術スタック紹介

エキサイト電話占いやお悩み相談室については、サービスの向上のため通話内容の分析を行なっています。 サービス本体とは別に分析用のAWSアカウントを発行しており、ここでは複数AWSアカウントを使った分析の全体像を発表していただきました。

電話占いの文字起こしと要約

上のコンテンツのさらに細かい部分で、文字起こしと要約を具体的にどんな技術を使って実装しているのかを発表いただきました。 どちらもAIの技術を使っていますが、通話時間に比例して音声ファイルの容量も大きくなるため、分析の難易度や料金コストも高くなります。できるだけ低コスト・短時間で処理が完了するように工夫している点についてご紹介いただきました。

また実装はPythonで行なっていますが、uv・Ruff・Pydanticなど最近の主要パッケージを使っているので、それについてもご紹介いただきました。

運営視点の振り返り

来年度にはオフィス移転があるため、現オフィスでは最後のイベントとなりました。 今回のコンテンツは技術的に気になる点が多かったためか、質疑応答が活発でした。コンテンツを決める際にこの辺りも今後強く意識して行きたいと思います。

また後半のフリートークではいくつかの机にトークテーマを設定し、最初はそれについて話してもらうようにしました。 トークテーマを決めるのは大変ですが、場が盛り上がりやすい仕掛けにはなるので、引き続きブラッシュアップしていこうと思いました。

まとめ

最後まで読んでいただき、ありがとうございました。 テクデザBeer Bashは社内イベントではありますが、少しでもイベントの雰囲気が伝わっていたら幸いです。 25年度もテクデザBeer Bashは開催予定ですので、引き続きイベントを盛り上げて行きます!!

Tailwind CSSでbackground-sizeとbackground-positionの両方を任意の値で設定する方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Tailwind CSSbackground-sizebackground-positionの両方を任意の値で設定する方法をご紹介します。

実現したいこと

冒頭、実現したいことを整理します。

Tailwind CSSでは、background-sizebackground-positionを指定する場合は、どちらもbg-で始まるユーティリティクラスを使用します。

<!-- background-size: cover; -->
<div class="bg-cover"></div>

<!-- background-position: center; -->
<div class="bg-center"></div>

以下のようにユーティリティクラスの用意のない任意の値(Custom value)を指定したいとします。

div {
  background-size: 50% auto;
  background-position: bottom 10px right 20px;
}

この際、bg-[<value>]の構文で設定しようとするとうまくいきません。

<!-- syntax error: bg-[<value>]ではいっぺんに設定できない -->
<div class="bg-[50%_auto_bottom_10px_right_20px]"></div>

一方で今回実現したいのは、background-sizebackground-position両方を任意の値で設定することです。

background-sizeとbackground-positionの両方を任意の値で設定する方法

bg-[<value>]の構文ではなく、background-sizebg-[length:<value>]を、background-positionbg-[position:<value>]を使用することで区別して任意の値を設定できます。

プロパティ 任意の値を設定
background-size bg-[length:<value>]
background-position bg-[position:<value>]

tailwindcss.com

tailwindcss.com

なお、どちらか一方の場合は従来通りbg-[<value>]でも指定できます。

先にお示しした実現したいCSSを表現すると次のようになります。

<div class="bg-[length:50%_auto] bg-[position:bottom_10px_right_20px]"></div>

これで、background-sizebackground-position両方を任意の値で設定できます。

まとめ

今回は、Tailwind CSSbackground-sizebackground-positionの両方を任意の値で設定する方法をご紹介しました。

どちらも接頭辞がbg-のため、困惑される方も多いのではないでしょうか。

Tailwind CSSを使用される方の一助となれば幸いです。ご精読ありがとうございました。

参考文献

【Flutter】flutter_flavorizrを用いて環境分けをする

こんにちは。エキサイトでアプリエンジニアをしている岡島です。

今回は、Flutterプロジェクトで環境ごとに設定を分けて開発・ビルドしたい場合に便利なツール、flutter_flavorizrを用いた環境構築方法を紹介したいと思います。

本記事では、開発(dev)、ステージング(stg)、本番(prod)の3環境を想定し、flutter_flavorizrを使ってiOSAndroid両方の設定を自動で生成する方法を解説していきます。

使ってみた感想としては、yamlファイルを書くだけで環境分けができ、IDEの環境設定も自動で行ってくれるのでとても便利だと感じました。

flutter_flavorizrとは?

flutter_flavorizr は、Flutterプロジェクトにおける マルチフレーバー対応を簡単に行うためのコード生成ツールです。

以下のようなタスクを自動で生成・設定してくれます。

  • AndroidiOS両方のビルド設定(flavor)の自動生成
  • 環境ごとのアプリアイコン設定
  • Firebaseの設定ファイル(GoogleService-Info.plist / google-services.json)の環境別配置

これにより、開発者はyamlファイルに定義を書くだけで、煩雑な手作業なしに複数環境の構築が可能になります

今回は簡単に環境分けの部分を紹介します。

注意点

flutter_flavorizrの自動生成ファイルによって、既存ファイルが上書きされるケースがあるため、既存のプロジェクトの導入時には注意が必要かもしれません。

実装手順

flutter_flavorizr を追加

pubspec.yamlに flutter_flavorizr を追加し、 pub getします。

dev_dependencies:
  flutter_flavorizr: ^2.3.0
flutter pub get

flavorizr.yamlに環境を設定する

ide: "idea"
flavors:
  dev:
    app:
      name: "MyApp Dev"
    android:
      applicationId: "com.example.myapp.dev"
    ios:
      bundleId: "com.example.myapp.dev"
  stg:
    app:
      name: "MyApp Staging"
    android:
      applicationId: "com.example.myapp.stg"
    ios:
      bundleId: "com.example.myapp.stg"
  prod:
    app:
      name: "MyApp"
    android:
      applicationId: "com.example.myapp"
    ios:
      bundleId: "com.example.myapp"

各項目の説明

ide: "idea"

これは使用しているIDEを指定する設定です。

"idea" を指定すると、Android Studio (IntelliJベースのIDE) 向けに .idea ディレクトリ配下の設定ファイルが適切に更新されるようになります。

flutter_flavorizr は IDE に応じてフレーバーごとの「Run Configuration」を自動生成してくれます。

他にも "vscode" も指定可能です。

flavors:(各フレーバー定義)

この中で複数のフレーバー(環境)を定義します。上記では dev、stg、prod の3つを定義しています。

app.name

各環境のアプリ名です。 F.title などとして参照したり、Android/iOSアプリ上の表示名として反映されたりします。

例:開発用には "MyApp Dev"、本番用には "MyApp"。

applicationId , bundleId

applicationIdはAndroid, bundleIdはiOSのアプリ識別子です。 通常、com.example.myapp.dev のように、環境名で末尾を変えることが多いです。

自動生成コマンドの実行

flutter pub run flutter_flavorizr

最後に

flutter_flavorizrを利用すれば、アプリアイコンやFirebaseプロジェクトを環境ごとに用意することも可能です。ほとんど自動で必要な処理が行われるのでお手軽だと思います。IDEのRun Configurationの作成もできるのでとても嬉しいなと思いました。

参考リンク

flutter_flavorizr

htmxを使ってbuttonを押した際に別のページを表示する方法

はじめに

こんにちは、新卒2年目の岡崎です。今回は、htmxを使ってbuttonを押した際に別のページを表示する方法を紹介します。

環境

  • gradle
------------------------------------------------------------
Gradle 8.5
------------------------------------------------------------

Build time:   2023-11-29 14:08:57 UTC
Revision:     28aca86a7180baa17117e0e5ba01d8ea9feca598

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.3 aarch64
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)

今回は、htmxとBootstrapを使っているため、以下の依存関係をbuild.gradleに追加します。

implementation "org.webjars.npm:htmx.org:1.9.10"
runtimeOnly("org.webjars:bootstrap:5.3.3")

実装

まずは、エンドポイントを作成します。/demoからbuttonを押すと、/newDemoに遷移するようにするため、2つのエンドポイントを用意しました。

@Controller
@RequestMapping("/")
public class DemoController {
    @GetMapping("demo") 
    public String demo() {
        return "index";
    }

    @PostMapping("newDemo") 
    public String newDemo() {
        return "newDemo";
    }
}

次に、resources配下にHTMLファイルを作成します。

  • index.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX Example</title>
    <script src="/webjars/htmx.org/1.9.10/dist/htmx.min.js"></script>
    <link rel="stylesheet" href="/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.css">
    <link rel="stylesheet" href="/webjars/bootstrap/5.3.3/css/bootstrap.min.css">
</head>
<body>
        <div class="container text-center mt-5">
        <div>demo 1</div>
        <button class="btn btn-primary"
                th:hx-post="@{/newDemo}"
                th:hx-push-url="true"
                hx-target="body"
        >
            投稿
        </button>
        </div>
</body>
</html>

  • newDemo.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HTMX Example</title>
    <script src="/webjars/htmx.org/1.9.10/dist/htmx.min.js"></script>
    <link rel="stylesheet" href="/webjars/bootstrap/5.3.3/css/bootstrap.min.css">
</head>
<body>
    <div class="container text-center mt-5">
        <p>新しいページです</p>
    </div>
</body>
</html>

以下で各機能を解説します。

hx-target

この属性は、どの部分を更新するかを指定します。ここでは、画面全体を更新したいのでbodyを指定しています。

hx-target="body"

参考:

htmx.org

hx-get

指定したエンドポイントにアクセスし、現在のHTMLページの内容が入れ替わります。

hx-getのみでは、HTMLの内容だけが置き換わり、URLはそのままです。

        <button class="btn btn-primary"
                th:hx-get="@{/newDemo}"
                hx-target="body"
        >
            投稿
        </button>

以下の画像に、実際の挙動を示しました。

参考:

htmx.org

hx-push-url

hx-push-urltrueにすると、ページの遷移に合わせてURLを変更できます。また、ブラウザの履歴にも記録できます。

        <button class="btn btn-primary"
                th:hx-get="@{/newDemo}"
                th:hx-push-url="true"
                hx-target="body"
        >
            投稿
        </button>

以下のスクリーンショットで、実際の挙動を確認できます。buttonを押すと、遷移先のURLに変わりました。

ブラウザの履歴にも、遷移先のエンドポイントが追加されました。

参考:

htmx.org

最後に

今回は、htmxを使ってbuttonを押した際に別のページを表示する方法を紹介しました。少しでも参考になれば幸いです。

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

募集職種一覧はこちらになります。

www.wantedly.com

PHPで特殊文字をエスケープする方法

PHP特殊文字エスケープする方法

初めまして、新卒2年目の岡崎です。今回は、PHPでHTMLタグなどに使われる特殊文字エスケープする方法を紹介します。

なぜ特殊文字エスケープする必要があるのか

プログラミングでは、特定の文字(<>&"'等)が特別な意味を持つため、そのまま使用すると問題が発生する可能性があります。

例えば、HTMLやSQLでは"'はデータの境界を示し、<script>はブラウザで実行されるコードとして解釈される可能性があります。

特殊文字エスケープすることで、これらの文字を「単なる文字」として扱い、意図しない動作やセキュリティリスクを防ぐことができます。

PHP特殊文字エスケープする方法

PHP特殊文字エスケープする場合、htmlspecialchars関数を使います。htmlspecialchars関数は、PHP 4以降で使えます。

$input = '<script>alert("test");</script>';
echo htmlspecialchars($input, ENT_QUOTES, "UTF-8");

出力結果

&lt;script&gt;alert(&quot;test&quot;);&lt;/script&gt;

コードの解説

htmlspecialchars関数は、HTMLで特別な意味を持つ文字をエスケープし、表示できるように変換します。

  • 第一引数:エスケープしたい文字列
  • 第二引数:エスケープの対象となる文字を指定するフラグ
  • 第三引数:文字コード(通常は "UTF-8" を指定)

第二引数でよく使われる入るフラグを紹介します。

フラグ 説明
ENT_QUOTES ダブルクォート (") はエスケープするが、シングルクォート (') はそのまま
ENT_NOQUOTES クォートをエスケープしない
ENT_SUBSTITUTE 文字エンコーディングUTF-8 など)に存在しない文字を?に置き換える
ENT_HTML401 HTML4.01のエンティティエンコードルールに従う

htmlspecialchars関数の公式ドキュメントはこちらです。

www.php.net

最後に

今回は、PHPでHTMLタグ等に使われる特殊文字エスケープする方法を紹介しました。適切にエスケープを行うことで、予期しない動作を防いだり、セキュリティ対策をすることができます。

皆さんの何かのお役に立てれば幸いです。

【Flutter】Chopperを用いてGitHubのAPIから検索結果を取得してみる

こんにちは。エキサイトでアプリエンジニアをしている岡島です。今回は業務でChopperというライブラリを使用したので、Chopperの基礎を説明しながら、GitHubAPIで検索結果を取得するまでを記事にしていこうと思います。GitHubAPIの仕様については省略します。

Chopperとは

Chopperは Retrofit(Kotlin/AndroidのHTTPクライアント)のように、アノテーションを使ってAPIを定義できます。Chopper では、API 通信のコードを自動生成 されるので、URL や HTTP メソッド、パラメータをアノテーションを用いて直感的に定義できる のが特徴です。

環境

dependencies:
  chopper: ^8.1.0

dev_dependencies:
  build_runner: ^2.4.9
  chopper_generator: ^8.1.0

APIサービスを定義する

lib/github_service.dartファイルを作成し、以下のコードを追加します。

import 'package:chopper/chopper.dart';

part 'github_service.chopper.dart';

@ChopperApi(baseUrl: '/search/repositories')
abstract class GithubService extends ChopperService {
  @GET()
  Future<Response> searchFlutterRepos(
    @Query('q') String query,
    @Query('per_page') int perPage,
  );

  static GithubService create([ChopperClient? client]) =>
      _$GithubService(client);
}

github_service.dartを追加したら、 ターミナルで以下のコマンドを実行し、Chopperのコードを生成します。

dart run build_runner build

@ChopperApi(baseUrl: '/search/repositories')

GitHub APIリポジトリ検索エンドポイントである /search/repositoriesを設定しています。

後述するChopperClientのbaseUrlをhttps://api.github.comに設定すると、

https://api.github.com/search/repositories

にリクエストが送信されます。

@GET()アノテーション

@GET()アノテーションを付与することで、このメソッドがGETリクエストを送信するAPIメソッドになります。 @GET, @POST, @PUT, @PATCH, @DELETE, @HEADが用意されています。

クエリパラメータの設定

@Query('q')と記述すると、クエリパラメータの設定ができます。今回の例では、queryという変数をqというパラメータ名として設定しています。

APIクライアントの初期化

続いてクライアントの初期化を行います。

  final client = ChopperClient(
    baseUrl: Uri.parse('https://api.github.com'),
    services: [GithubService.create()],
    converter: const JsonConverter(),
  );

  final service = GithubService.create(client);

baseUrlについて

baseUrlは、API通信の際にリクエストを送る「基準となるURL」を設定します。 この場合、GitHub APIの基本URLであるhttps://api.github.com を指定しています。

共通のAPIのベースURLはChopperClientで設定するのが良さそうです。

services: [GithubService.create()]について

services:で指定すると、指定したGithubService.create()が使用できるようになります。

services:GithubService.create()を登録することで、以下のようにsearchFlutterReposメソッドを呼び出すことができます。

final service = GithubService.create(client);
final response = await service.searchFlutterRepos('flutter', 10);

converter: const JsonConverter()

converterのオプションは、API のレスポンス(受信したデータ)を適切なフォーマットに変換するために使用されます。 JsonConverterを指定すると、Chopperが自動的にJSONをパースしてくれます。

検索結果を表示

以下にmain.dartの全コードを載せておきます。

void main() async {
  final client = ChopperClient(
    baseUrl: Uri.parse('https://api.github.com'),
    services: [GithubService.create()],
    converter: const JsonConverter(),
  );

  final service = GithubService.create(client);

  final response = await service.searchFlutterRepos('flutter', 10);

  if (response.isSuccessful) {
    print(response.body);
  } else {
    print('Error: ${response.statusCode}');
  }
}

まとめ

今回はChopperの基礎についてGitHub APIを触りながら説明しました。この記事が誰かのお役に立てれば幸いです。

使用ライブラリ

https://pub.dev/packages/chopper

Tailwind CSSに標準搭載されているリセットCSSを無効にする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Tailwind CSSに標準搭載されているリセットCSSを無効にする方法をご紹介します。

実現したいこと

冒頭、実現したいことを整理します。

Tailwind CSSではリセットCSSとして、Preflightが標準搭載されています。

tailwindcss.com

Preflightは@import "tailwindcss";に含まれ、baseレイヤーに組み込まれるようになっています。

なお、Tailwind CSSのクラスは@layerを用いて順位付けされており、

  1. theme レイヤー
  2. baseレイヤー
  3. componentsレイヤー
  4. utilitiesレイヤー

の順で上書きされていきます。(p-*などのユーティリティクラスが最優先となる)

Preflightはブラウザ依存の振る舞いを取り除いて、ブラウザ間の表現の不一致を防ぐ役割を担います。

一方で、すでにプロジェクト内でリセットCSSを採用している場合に、Preflightによって意図しない描写を招くことがあります。

今回実現したいのは、Tailwind CSSに標準搭載されているリセットCSS(Preflight)を無効にして、独自のリセットCSSに置き換えることです。

Preflightを無効にする方法

結論から申し上げると、Tailwind CSSのインポートを書き換えることでPreflightを無効にできます。

Tailwind CSSを使用する場合、設定ファイルのCSS@import "tailwindcss";を宣言します。

@import "tailwindcss";には以下のようなCSSが含まれています。

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/preflight.css" layer(base);  // これがPreflight
@import "tailwindcss/utilities.css" layer(utilities);

@import "tailwindcss/preflight.css" layer(base);が読み込まれないようにするには、@import "tailwindcss";を用いずに次のように各種import宣言を直に記述します。

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

これで、Tailwind CSSのセットからPreflightのみを取り除けます。

独自のリセットCSSを設定する

独自のリセットCSS、すなわちタグに対するデフォルトのスタイルを設定したい場合は、baseレイヤー内に記述します。

例えば、「h1タグの文字色は赤色」というデフォルトスタイルを設定する場合は、以下のようにします。

@layer theme, base, components, utilities;

@import "tailwindcss/theme.css" layer(theme);
@import "tailwindcss/utilities.css" layer(utilities);

@layer base {
  h1 {
    color: red;
  }
}

これで、h1タグの文字色は赤色になります。

<!--- デフォルトで文字色は赤色になる --->
<h1>見出し</h1>

冒頭に申し上げた通り、baseレイヤーの優先順位は低いため、ユーティリティクラスを付与すると上書きされます。

<!--- ユーティリティクラスが最優先になるため、文字色は黒色になる --->
<h1 class="text-black">見出し</h1>

まとめ

今回は、Tailwind CSSに標準搭載されているリセットCSS(Preflight)を無効にする方法をご紹介しました。

既存のプロジェクトにTailwind CSSを乗せる場合に、リセットCSSは据え置きにしたいモチベーションが生まれることがあるかと思います。

ただし、Tailwind CSSのユーティリティクラスはPreflightをベースに設計されていますので、その点は注意が必要です。

まっさらなプロジェクトを筆頭に、基本的にはPreflightをそのまま使用することをおすすめします。

Tailwind CSSを使用される方の一助となれば幸いです。ご精読ありがとうございました。

参考文献

SpringBootのBeanスコープを@Scopeで使い分ける

エキサイト株式会社エンジニアの佐々木です。SpringBootのBeanスコープのデフォルトはSingletonですが、@Scopeを利用して使い分けていこうと思います。

Beanスコープとは?

SpringBootはDIコンテナを保有していて、起動時に多くのBeanを生成します。そのBeanがどのように生成され、どこで再利用されるかを@Scopeで制御できます。

singleton

Beanのデフォルトのスコープになります。DIコンテナの中で最初の1つのインスタンスしか作られません。1つのインスタンスをアプリケーション全体で利用するのでメモリ消費は少なくパフォーマンスがいいですが、インスタンスフィールドは、マルチスレッドでの利用は注意する必要があります。

// デフォルトsingleton
@Component
class SingletonData {
   ... 
}

request

リクエストごとに1つのインスタンスが作られます。1つのリクエスト内で共有してもいいようなデータの管理に使用します。

@Component
@RequestScope
class ReuestData {
   ... 
}

session

HTTPSessionごとに1つのインスタンスが作られます。SpringSessionを利用し、同一のセッション内で共有してもいいようなデータの管理に使用します。

@Component
@SessionScope
class SessionData {
   ... 
}

prototype

DIコンテナから取り出される毎にインスタンスが生成されるスコープになります。Listの中に含まれてるようなものに使用します。

@Component
@Scope("prototype")
class ProtoTypeData {
   ... 
}

singletonスコープ、prototypeスコープの違い

今回はsingletonスコープ、prototypeスコープ、requestスコープの検証を行います。(sessionスコープは、SpringSessionの導入などがあり煩雑なので割愛します)

今回は、ApplicationContextを使用したDIの方法で検証します。

singletonスコープ

singletonスコープのデータクラスは下記になります。

@Component
@Data
class SingletonScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

テストコードは下記になります。ApplicationContextを使用して、2回SingletonScopeDataを呼び出していますが、同じインスタンスが返っています。

@SpringBootTest(classes = {SingletonScopeData.class})
class SingletonScopeDataTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testSingleton() {
        SingletonScopeData bean1 = applicationContext.getBean(SingletonScopeData.class);
        SingletonScopeData bean2 = applicationContext.getBean(SingletonScopeData.class);
        Assertions.assertSame(bean1, bean2);   // true
    }
}

requestスコープ

requestスコープのデータクラスは下記になります。

@Component
@Data
@RequestScope
class RequestScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

リクエスト単位でインスタンスが作成されますので、下記のようなコントローラーを用意して、このコントローラーに対してテストコードを書きます。

@RestController
@RequestMapping("scope")
@Slf4j
public class ScopeController {

    @Autowired
    private RequestScopeData requestScopeData;

    @GetMapping("request")
    public String requestScode() {
        return requestScopeData.toString();
    }
}

テストコードは下記になります。1回目のリクエストと2回目のリクエストで異なる値が返却されています。

@WebMvcTest(controllers = ScopeController.class)
@Import(RequestScopeData.class)
@Slf4j
class ScopeControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void testRequest() throws Exception {
        String result1 = mockMvc.perform(
                get("/scope/request"))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();
        String result2 = mockMvc.perform(
                        get("/scope/request"))
                .andExpect(status().isOk())
                .andReturn()
                .getResponse()
                .getContentAsString();

        log.info("result1: {}", result1);  // RequestScopeData(id=-1265028686, name=555946a3-9dbf-4833-9d73-a41c3474bcf5)
        log.info("result2: {}", result2); // RequestScopeData(id=415725629, name=9fe5ad43-6a22-43b1-89f8-272077d1f448)
        Assertions.assertNotEquals(result1,result2);  // true
    }
}

prototypeスコープ

@Component
@Data
@Scope("prototype")
class PrototypeScopeData {
   private int id = new SecureRandom().nextInt();
   private String uuid = UUID.randomUUID().toString();
}

テストコードは下記になります。PrototypeScopeを2回呼び出して比較をしていますが、同一インスタンスでもなく、値も異なります。

@SpringBootTest(classes = {PrototypeScopeData.class})
class PrototypeScopeDataTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testPrototype() {
        PrototypeScopeData bean1 = applicationContext.getBean(PrototypeScopeData.class);
        PrototypeScopeData bean2 = applicationContext.getBean(PrototypeScopeData.class);
        assertNotEquals(bean1, bean2);  // true
        assertNotSame(bean1, bean2);  // true
    }
}

補足:ApplicationContextについて

ApplicationContextは、Springの中のDIコンテナのコア部分で、Beanライフサイクル(オブジェクトの生成、破棄)を管理しているクラスになります。データクラスやDTOなどでprototypeスコープで利用する場合に便利です。

@SpringBootTest(classes = {PrototypeScopeData.class})
class PrototypeScopeDataTest {
    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testPrototype() {

        // prototypeの場合は、新しいインスタンスが返却される
        PrototypeScopeData prototype = applicationContext.getBean(PrototypeScopeData.class);

        // singletonの場合は、すでにインスタンス化されたオブジェクトが返却される
        SingletonScopeData singleton = applicationContext.getBean(SingletonScopeData.class);
    }
}

まとめ

Beanスコープを理解しながら使えば、単体テストが書きやすくより保守性の高いコードがかけると思います。 staticメソッドなどの濫用も防げます。検索してもあんまりでてこなかったので、簡単にですがまとめておきます。

Google Ad Manager(GAM)でサードパーティクリエイティブのクリック数が取得できない場合の対処法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Google Ad Managerでサードパーティクリエイティブのクリック数が取得できない場合の対処法をご紹介します。

前提

冒頭、前提を整理します。

Google Ad Manager(以下、GAM)は、サイトに対して広告の配信をしたり収益の管理ができるツールです。

GAMでは配信する広告の内容、例えばリンク付きのバナーなどは「クリエイティブ」と呼ばれます。

クリエイティブの種類の一つである「サードパーティ」を使用して、以下のようなHTMLを配信したいとします。

<a href="https://example.com/">
  <img alt="..." src="..." />
</a>

実現したいこと

GAMではクリエイティブのインプレッションやクリック数が確認できます。

今回実現したいのは、サードパーティクリエイティブのクリック数を取得することです。

クリックをトランキングするにはマクロの挿入が必要

先にお示ししたHTMLのままでは、GAMがクリックをトランキングできずに、実際にはクリックされていてもクリック数が0になってしまいます。

aタグを用いて遷移させる場合に、クリック(リンクを踏んだか)をトランキングするにはマクロの挿入が必要です。

具体的にはGAMのヘルプページでも示されている通り、クリエイティブを設定する際にhref属性に指定したURLの先頭にクリックマクロである%%CLICK_URL_UNESC%%を記載します。

support.google.com

<!-- 遷移先URLの先頭にマクロを追加する -->
<a href="%%CLICK_URL_UNESC%%https://example.com/">
  <img alt="..." src="..." />
</a>

クリックがトラッキングできているかチェックする

クリックがトラッキングできているかチェックするには、クリエイティブのプレビュー画面で設定したバナーをクリックします。

次のようなページに遷移すればトラッキングできています。

クリックのトラッキングが成功している場合に表示されるページ

まとめ

今回は、Google Ad Managerでサードパーティクリエイティブのクリック数が取得できない場合の対処法をご紹介しました。

クリック数が取得できない事態は、私がGAM使いはじめた時に実際に遭遇した現象です。

本稿が、同じ現象に遭遇している方の一助となれば幸いです。

ご精読ありがとうございました。

参考文献

【iOS/Android】環境別にAdMobのネイティブ広告バリデーターを非表示にする方法

こんにちは。エキサイトでアプリエンジニアをしている岡島です。 今回は、Google AdMobのネイティブ広告のバリデーターについて、環境別に表示非表示を切り替える方法を共有したいと思います。

本記事では環境設定に、Android は productFlavors を使用し、iOS は xcconfig を用いる場合 の設定方法を紹介していきます。

Google AdMobのネイティブ広告バリデーターについて

Google AdMobのネイティブ広告バリデーターは、開発中に広告のレイアウトや表示形式が適切かを確認するためのツールです。

デフォルトでこのバリデーターは表示されるようになっているため、開発の際に、dev環境やstage環境などで表示されていました。開発時以外の動作確認など、このポップアップが不要な場合があります。そこで環境別にこのバリデーター表示の表示・非表示を切り替えたいと思ったので、環境別に設定する方法について調べてみました。

AdMob validator

Androidで表示・非表示を切り替える

Android では、app/build.gradle で manifestPlaceholders を環境ごとに設定し、それを AndroidManifest.xml で適用することでバリデーターの表示を切り替えることができます。

app/build.gradle に設定を追加

android {
    flavorDimensions "environment"
    productFlavors {
        development {
            dimension "environment"
            manifestPlaceholders = [
                NATIVE_AD_DEBUGGER_ENABLED: "true"
            ]
        }
        staging {
            dimension "environment"
            manifestPlaceholders = [
                NATIVE_AD_DEBUGGER_ENABLED: "false"
            ]
        }
    }
}

AndroidManifest.xmlに設定を追加

次に、AndroidManifest.xmlで manifestPlaceholders の値を適用します。

<manifest>
    <application>
        <meta-data
            android:name="com.google.android.gms.ads.flag.NATIVE_AD_DEBUGGER_ENABLED"
            android:value="${NATIVE_AD_DEBUGGER_ENABLED}" />
    </application>
</manifest>

こうすることで、development環境ではバリデーターを表示し、staging環境では非表示にすることができます。

iOSで表示・非表示を切り替える

iOSでは、xcconfigファイルを使用して環境変数を設定し、それをInfo.plistで使用します。

xcconfig ファイルに環境ごとの設定を追加


ios/config/development.xcconfig

GAD_NATIVE_AD_VALIDATOR_ENABLED=false

ios/config/staging.xcconfig

GAD_NATIVE_AD_VALIDATOR_ENABLED=true

Info.plist に設定を追加

Info.plist に以下の設定を追加します。

<key>GADNativeAdValidatorEnabled</key>
<string>$(GAD_NATIVE_AD_VALIDATOR_ENABLED)</string>

まとめ

今回は、開発環境ごとにバリデーターのポップアップを切り替える方法を共有しました。manifestPlaceholders や .xcconfig を活用することで、簡単に設定を切り替えることができました。

参考文献

https://developers.google.com/admob/ios/native/validator?hl=ja

https://developers.google.com/admob/android/native/validator?hl=ja

Tailwind CSSでHTML属性に対する疑似クラスのスタイリングをする方法

こんにちは。エキサイトでデザイナーをしている齋藤です。

今回は、Tailwind CSSでHTML属性に対する疑似要素のスタイリングをする方法をご紹介します。

実現したいこと

冒頭、実現したいことを整理します。

HTMLの各タグには属性が用意されていることがあり、要素の動作を制御することができます。

inputタグのdisabled属性などがこれにあたります。

CSSではHTML属性に対して疑似クラスが存在し、{タグ}:{属性名}の様式でスタイリングすることができます。

input:disabled {
    background: red;
}

お示ししたCSSの例では、「disabled属性がtrueinput要素は背景が赤になる」というスタイリングを表現しています。

一方で今回実現したいのは、Tailwind CSSでTailwind CSSでHTML属性に対する疑似クラスのスタイリングをすることです。

Tailwind CSSで擬似クラスを指定する方法

結論から申し上げると、Tailwind CSSでは {属性名}:{ユーティリティクラス} の様式を用いることで、擬似クラスを用いたスタイリングが可能です。

先の例をTailwind CSSで表現すると以下のようになります。

<input class="disabled:bg-red-500" ...>

disabled属性以外にも用意されています。対応一覧は公式ドキュメントをご参照ください。

tailwindcss.com

注意点

擬似クラスに対して複数のユーティリティクラスを指定したい場合は、都度接頭辞として{属性名}:を付与する必要があります。

CSS

純粋なCSSでは宣言ブロック内に複数のプロパティを記述することが可能です。

input:disabled {
    background: red;
    border-color: red;
}

Tailwind CSS

Tailwind CSSの場合は、ユーティリティクラスに対して都度接頭辞として{属性名}:を付与します。

<input class="disabled:bg-red-500 disabled:border-red-500" ...>

まとめ

今回は、Tailwind CSSでHTML属性に対する疑似要素のスタイリングをする方法をご紹介しました。

Tailwind CSSは純粋なCSSの機能と互換するように設計されているため、書き味は異なりますがスタイリングの幅はほぼ変わらずに十分な表現ができます。

一方で、擬似クラスなどを多用していくとどうしても可読性は低下します。

可読性を担保するためにPrettierのプラグインを使用して、class属性内を規則的に並び替えるようにすることをおすすめします。

tech.excite.co.jp

tech.excite.co.jp

Tailwind CSSを使用される方の一助となれば幸いです。ご精読ありがとうございました。

参考文献

アコーディオンで使用するアイコンはどれがいい?

こんにちは!エキサイトお悩み相談室でデザイナーをしているサヅカです。

サイトをもっと使いやすくするべく日々UI改修を行っているのですが、今回アイコンの選定に非常に悩みましたのでその時のお話をしようと思います。

アイコンの意味を他のデザイナーと考えてみた

アコーディオン=クリックするとビヨーンとさらに内容が表示されるアレなのですが、ページ改修をしていてアコーディオンのデザインガイドラインはまだ設定していないことに気付きました。

こちらは「エキサイトお悩み相談室」のコイン購入ページの一部になります。

今回アコーディオン化することになったので、シェブロン下、シェブロン右、プラスマイナスの3種類を提案しました。

シェブロン下(開くとシェブロンは上になる)

👩‍🦰:一番よく見かける気がする

👨:Web標準化団体w3cでも用いられているようです

シェブロン右(開くとシェブロンは下になる)

👩‍🦰:他のページに遷移しそう

👨:右と下というのが動きに統一感がない

プラスマイナス(開くとプラスはマイナスになる)

👩‍🦰:「コイン購入」ページということもあり「+」は混乱しそう

👨:コンテンツの内容が多い時に使うイメージ

👱‍♀️:プラスマイナスアイコンは追加/削除の意味を持っていてアコーディオン以外でも頻用されると思う

色々な人と意見交換して検討した結果、弊サービスでは「シェブロン下」を採用することにしました。

個人的にはプラスマイナスが分かりやすくて好きなのですが、デザイナーの好みで安易に決めてしまうとユーザーが混乱するので気を付けたいと思います。

おまけ「”く”みたいなやつ」ってもう言わない

上の記事でシェブロンという言葉を使いました。矢印みたいな、ひらがなの”く”みたいなやつのことです。

正式名称を知りたくて色々と調べました。

シェブロン

服の生地などで「シェブロン柄」というのがあるのですが、どうやらそこからきているようです。

キャレット

キーボードの「キャレット」という記号のことで、「キャレット右」というデザイナーも多いようです。

結局のところ正式名称は?

特にこれといって正式なものはないようですが、マテリアルアイコンでは”Chevron Right”という名前でしたので「シェブロン」と呼ぶのが良いかもしれません。浸透するまではチーム内では「矢印」等で会話しそうですが汗

[Java]ローカルキャッシュを導入した話

はじめに

こんにちは、メディアプラットフォーム事業部エンジニアの岡崎です。最近、ローカルキャッシュの導入を行いました。今回はその時のことを備忘録としてブログに残します。

前提

今回の要件として一番大切なことは、できるだけユーザーがページを見ることができる状態にすることでした。

これを前提として、今までの実装を見ていきましょう。

最初、キャッシュはRedisに保存するような構成になっていました。

この場合、もしDBに障害が起きたとしても、Redisに保存しているデータがあった場合は、そこから取得することができます。

しかし、Redisに障害が起きてしまったら、データが取得できなくなり、クライアントではページを表示できなくなってしまいます。

今回は要件で、できるだけユーザーがページを見れる状態にしたかったので、他にもっといい方法がないか模索することになりました。

そこで、キャッシュしたデータを保存する場所をRedisからローカルに変更しました。

こうすることで、DBに障害が起きたとしても、ローカルサーバーにデータがある限りは、データを取得できます。

環境

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.2)
  • build.gradleに以下の依存関係を追加する
implementation "com.github.ben-manes.caffeine:caffeine:3.1.8"

実装

まずは、ローカルキャッシュを使うための準備を行いました。

必要なものは以下の通りです。

  • ローカル用のキャッシュマネージャー
  • Redis用のキャッシュマネージャー
  • ローカルのキャッシュキー

これらを、それぞれのファイルに実装していきました。

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

CacheConfig

ここでは、ローカルキャッシュのキャッシュマネージャーの設定を行っています。

@Configuration
public class CacheConfig {
    /**
     * ローカルキャッシュ用のキャッシュマネージャーを設定する
     *
     * @return キャッシュマネージャー
     */
    @Bean(CacheLocalKeyType.LOCAL_CACHE_MANAGER_NAME)
    public CacheManager localCacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();

        Arrays.stream(CacheLocalKeyType.values()).forEach(e -> {
            Cache<Object, Object> cache = Caffeine
                    .newBuilder()
                    .expireAfterWrite(Duration.ofSeconds(e.getTtl()))
                    .build();

            cacheManager.registerCustomCache(e.getKey(), cache);
        });
        return cacheManager;
    }
}

CacheLocalKeyType

ローカルのキャッシュキーの実装例です。

public enum CacheLocalKeyType {
    WEB_GET_TOP(CacheLocalKeyType.WEB_TOP_KEY, TimeUnit.HOURS.toSeconds(1));

    @Getter
    private final String key;
    @Getter
    private final Long ttl;

    /**
     * ローカルキャッシュのキータイプ
     *
     * @param cacheName キャッシュ名
     * @param ttl キャッシュ時間
     */
    CacheLocalKeyType(String cacheName, Long ttl) {
        this.key = cacheName;
        this.ttl = ttl;
    }

    public static final String SAMPLE_KEY = "sampleKey";
    public static final String LOCAL_CACHE_MANAGER_NAME = "localCacheManager";
}

Redis.config

Redis.configにキャッシュマネージャーがない場合、実装する必要があります。

今回は、Redisのキャッシュをデフォルトの設定にしたいため、@Primaryをつけます。

    /**
     * Redisのキャッシュマネージャーを設定する
     *
     * @param redisConnectionFactory redisConnectionFactory
     * @return キャッシュマネージャー
     */
    @Bean
    @Primary
    public CacheManager cacheManager(LettuceConnectionFactory redisConnectionFactory) {
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        Arrays.stream(CacheKeyType.values()).forEach(e ->
                cacheConfigurations.put(
                        e.getKey(),
                        RedisCacheConfiguration
                                .defaultCacheConfig()
                                .entryTtl(Duration.ofSeconds(e.getTtl()))
                                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()))
                                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(this.serializer()))
                )
        );

        return RedisCacheManager
                .builder(redisConnectionFactory)
                .withInitialCacheConfigurations(cacheConfigurations)
                .build();
    }

以上で、ローカルキャッシュを使う準備は整いました。

それでは、実際にローカルキャッシュを使ってみましょう。実装例は以下の通りです。

@Override
// キャッシュマネージャーを指定しない場合、デフォルトの設定が使われます
@Cacheable(cacheNames = CacheKeyType.SAMPLE_KEY, cacheManager = CacheLocalKeyType.LOCAL_CACHE_MANAGER_NAME)
public String getIdList() {
    // 以下略
}

これで完成です!

最後に

今回は、ローカルキャッシュを導入したので、簡単に紹介しました。簡単に実装ができるので、ぜひローカルキャッシュの実装の導入を検討してみてください。

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

募集職種一覧はこちらになります!

www.wantedly.com

DockerでPostgreSQLを起動する方法

はじめに

こんにちは、新卒2年目の岡崎です。今回は、DockerでPostgreSQLを起動する方法を紹介します。

環境

$  docker version

Client:
 Cloud integration: v1.0.31
 Version:           23.0.5
 API version:       1.42
 Built:             Wed Apr 26 16:12:52 2023
 OS/Arch:           darwin/arm64
 Context:           default

Server: Docker Desktop 4.19.0 (106363)
 Engine:
  Version:          23.0.5
  API version:      1.42 (minimum version 1.12)
  Built:            Wed Apr 26 16:17:14 2023
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.6.20
  GitCommit:      × × × × × × × × × × ×
 runc:
  Version:          1.1.5
  GitCommit:        v1.1.5-0-gf19387a
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

ディレクトリ構成

.
|-- docker-compose-postgres.yml
`-- postgres
``-- postgres-init
``|-- init.sql

ファイルごとの実装例を示します。

docker-compose-postgres.yml

name: "demo"
services:
  postgres:
    image: postgres:15.4
    container_name: demo-postgres
    environment:
      POSTGRES_PASSWORD: local_root_password
      POSTGRES_HOST_AUTH_METHOD: scram-sha-256
      POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
      TZ: Asia/Tokyo
    ports:
      - "5433:5432"
    volumes:
      - "./postgres/postgres-init:/docker-entrypoint-initdb.d"
      - "./postgres/data:/var/lib/postgresql/data"

name

コンテナの名前を定義します。

service

起動するサービス群を定義します。今回の場合は、PostgreSQLを起動するための設定を定義しています。

image

イメージ名を定義します。

container_name

コンテナ名を定義します。

environment

環境変数名を定義します。

今回はPOSTGRES_PASSWORD(PostgreSQLのパスワード)、POSTGRES_HOST_AUTH_METHOD(PostgreSQLの認証方法)、POSTGRES_INITDB_ARGS(PostgreSQLロケール)、TZ(タイムゾーン)を設定しています。

ports

ポートを定義します。

volumes

名前付きボリュームをコンテナ上のフォルダに割り当てます。フォーマットは以下の通りです。

ボリューム名:コンテナの絶対パス

今回の場合、/docker-entrypoint-initdb.dを、コンテナ上の./postgres/postgres-initに割り当てています。(./postgres/data:/var/lib/postgresql/dataも同様です。)

init.sql

CREATE ROLE demo_user WITH LOGIN PASSWORD 'demo_password';
CREATE DATABASE demo OWNER demo_user ENCODING 'UTF8' LOCALE 'C' TEMPLATE template0;

初期設定として、ロール・データベースの作成の作成を行っています。

コマンド

$ docker-compose -f docker-compose-postgres.yml up

実際にこのような起動画面になったら準備完了です。

今回は、TablePlusというGUIツールを使って接続できるか確認しました。

tableplus.com

Port、User、Password、Databaseを指定します。

無事に接続することができました。

最後に

今回は、DockerでPostgreSQLを起動する方法を紹介しました。

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

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

www.wantedly.com