JavaのSpringBootでMyBatisを利用して複数のデータソースに接続する方法

こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス Advent Calendar 2023の5日目を担当いたします。

今回はJavaのSpringBootでMyBatisを利用して、複数のデータソースに接続する方法をご紹介します。

はじめに

複数のデータベースに利用したい情報が分散しており、一つの「アプリケーション」からそれらにアクセスする必要があるという場合を考えます。

このとき、考えられる対処法は主に2つあります。

  • それぞれのデータソースからデータを取得するためのAPIを別プロジェクトとして実装する
  • 複数のデータソースに一つのアプリケーションから直接アクセスできるような設定をする

1つ目の方法では、一つの「アプリケーション」から直接アクセスするデータソースを最大1つにすることができるので、複雑さの観点からは望ましいと言えます。 しかし、データベースに複数回アクセスする必要があるような状況では、呼び出しのオーバーヘッドが大きくなってしまいます。 また、単純にAPIを実装する手間もかかります。

そのような場合には、2つ目の方法も視野に入れることが考えられます。

本ブログでは、この2つ目の方法をご紹介いたします。

前提条件

本ブログでは以下の項目を前提条件としています。

  • Java、SpringBoot、MyBatisについてはある程度知っている
  • アプリケーションコードを記述する環境が整っている
  • 単一のデータソースにアクセスしてデータを取得することが可能なコードは実装している

動作確認環境

  • Java 17
  • SpringBoot v3.1.5
  • MyBatis ・・・ org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2

問題設定

本ブログの例では、以下のような設定を用います。

データソース1 データソース2
DBの種類 MySQL 8.2 MySQL 5.7
ポート 3307 3308
接続先スキーマ schema1 schema2

今回はどちらもMySQLですが、当然ながらPostgreSQLなどの他のデータベースでも問題ありません。

application.ymlファイルへの記述

まずは、各データソースへの接続情報をapplication.ymlファイルに記述していきます。

spring:
  datasource:
    schema1:
      connection-properties:
        jdbc-url: jdbc:mysql://localhost:3307/schema1
        username: sample1-user
        password: sample1-password
      hikari-data-source-properties:
        max-lifetime: 600000
        maximum-pool-size: 5
    schema2:
      connection-properties:
        jdbc-url: jdbc:mysql://localhost:3308/schema2
        username: sample2-user
        password: sample2-password
      hikari-data-source-properties:
        max-lifetime: 600000
        maximum-pool-size: 5

もちろん、コード内に設定情報を記述しても動きます。 しかし、これらのパラメータはプロパティファイルにまとめておいた方が良いでしょう。

hikari-data-source-properties内のmax-lifetimemaximum-pool-sizeは今回の要件では必須ではないですが、カスタマイズできるようにしておくと後々便利です。

なお、キーはこの通りでなくても問題ありませんので、それぞれのデータソースに合わせて適切なキー名にしてください。 また、usernamepasswordも適宜変更してください。

接続設定項目をYAMLファイルから読み込む

次に、前の節で記述したYAMLファイルの内容を読み込むためのコードを記述します。

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.datasource")
public record CustomDataSourceProperties(Properties schema1, Properties schema2) {
    public record Properties(
            ConnectionProperties connectionProperties, HikariDataSourceProperties hikariDataSourceProperties
    ) {
        public record ConnectionProperties(String jdbcUrl, String username, String password) {}

        public record HikariDataSourceProperties(
                Integer maxLifetime,
                Integer minimumIdle,
                Integer maximumPoolSize
        ) {
        }
    }
}

イメージとしては、@ConfigurationPropertiesprefixで指定したspring.datasourceの配下に、connectionPropertieshikariDataSourcePropertiesを配下にもつPropertiesレコードであるschema1schema2があるという設定になっています。

YAMLファイルのキーと見比べてみると、なんとなく記述していることは見えてくるのではないでしょうか。

実際に接続設定を行う

それでは、実際に接続のための設定を記述していきましょう。

といっても、SpringBootでMyBatisをデータベースに接続する際に、接続設定をカスタマイズするのとほとんど変わりません。

import javax.sql.DataSource;
import java.util.Objects;

import com.example.sample_project.project1.property.CustomDataSourceProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema1",
        sqlSessionFactoryRef = "schema1SqlSessionFactory"
)
@RequiredArgsConstructor
public class Schema1DataSourceConfig {
    private static final String SCHEMA1_DATA_SOURCE_NAME = "schema1DataSource";
    private static final String SCHEMA1_DATA_SOURCE_PROPERTIES_NAME = "schema1DataSourceProperties";
    private static final String SCHEMA1_SQL_SESSION_FACTORY_NAME = "schema1SqlSessionFactory";

    private static final String DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";

    private final CustomDataSourceProperties customDataSourceProperties;

    /**
     * schema1スキーマ用のDataSourcePropertiesを取得
     *
     * @return schema1スキーマ用のDataSourceProperties
     */
    public DataSourceProperties getSchema1DataSourceProperties() {
        return mapToDataSourceProperties(
                customDataSourceProperties.schema1().connectionProperties().username(),
                customDataSourceProperties.schema1().connectionProperties().password(),
                customDataSourceProperties.schema1().connectionProperties().jdbcUrl()
        );
    }

    /**
     * DataSourcePropertiesを取得  ①
     *
     * @param url DatabaseのエンドポイントURL
     * @return DataSourceProperties
     */
    private DataSourceProperties mapToDataSourceProperties(String username, String password, String url) {
        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName(DRIVER_CLASS_NAME);
        dataSourceProperties.setUsername(username);
        dataSourceProperties.setPassword(password);
        dataSourceProperties.setUrl(url);

        return dataSourceProperties;
    }

    /**
     * schema1スキーマ用のDataSourceのプロパティを作成
     *
     * @return DataSourceのプロパティ
     */
    @Bean(name = SCHEMA1_DATA_SOURCE_PROPERTIES_NAME)
    @Primary
    public DataSourceProperties schema1DataSourceProperties() {
        return getSchema1DataSourceProperties();
    }

    /**
     * schema1スキーマ用のDataSourceを作成
     *
     * @return DataSource
     */
    @Bean(name = SCHEMA1_DATA_SOURCE_NAME)
    @Primary
    public DataSource schema1DataSource(@Qualifier(SCHEMA1_DATA_SOURCE_PROPERTIES_NAME) DataSourceProperties dataSourceProperties) {
        final HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        return this.setPropertiesToHikariDataSource(hikariDataSource, customDataSourceProperties.schema1().hikariDataSourceProperties());
    }

    /**
     * HikariDataSourceにプロパティをセットする  ②
     *
     * @param hikariDataSource セット先HikariDataSource
     * @param properties セットしたいプロパティ
     * @return プロパティをセットしたHikariDataSource
     */
    private HikariDataSource setPropertiesToHikariDataSource(
            HikariDataSource hikariDataSource,
            CustomDataSourceProperties.Properties.HikariDataSourceProperties properties
    ) {
        hikariDataSource.setMaxLifetime(properties.maxLifetime());
        hikariDataSource.setMaximumPoolSize(properties.maximumPoolSize());

        if (Objects.nonNull(properties.minimumIdle())) {
            hikariDataSource.setMinimumIdle(properties.minimumIdle());
        }

        return hikariDataSource;
    }

    /**
     * schema1スキーマ用のSQLセッションファクトリを作成
     *
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean(name = SCHEMA1_SQL_SESSION_FACTORY_NAME)
    @Primary
    public SqlSessionFactory schema1SqlSessionFactory(@Qualifier(SCHEMA1_DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        final SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
        if (Objects.isNull(sqlSessionFactory)) {
            throw new NullPointerException();
        }

        // この辺りの設定は適宜変更してください。
        sqlSessionFactory.getConfiguration().setCacheEnabled(false);
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setDefaultScriptingLanguage(RawLanguageDriver.class);

        return sqlSessionFactory;
    }
}

①で、データベースの接続設定をDataSourcePropertiesにセットしています。
また、②で接続の最大ライフタイムや、最大プールサイズのような設定をHikariDataSourceへセットしています。

この辺りは、接続先が1つの場合にも設定する項目です。

大きな違いはこの設定を接続先のデータベースの数だけ記述する必要があることです。 先ほどの設定は「データベース1」の「schema1」用の接続設定です。
よって、今度は「データベース2」の「schema2」用の接続設定を記述していきましょう。

import javax.sql.DataSource;
import java.util.Objects;

import com.example.sample_project.project1.property.CustomDataSourceProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema2",
        sqlSessionFactoryRef = "schema2SqlSessionFactory"
)
@RequiredArgsConstructor
public class Schema2DataSourceConfig {
    private static final String SCHEMA2_DATA_SOURCE_NAME = "schema2DataSource";
    private static final String SCHEMA2_DATA_SOURCE_PROPERTIES_NAME = "schema2DataSourceProperties";
    private static final String SCHEMA2_SQL_SESSION_FACTORY_NAME = "schema2SqlSessionFactory";

    private static final String DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";

    private final CustomDataSourceProperties customDataSourceProperties;

    /**
     * schema2スキーマ用のDataSourcePropertiesを取得
     *
     * @return schema2スキーマ用のDataSourceProperties
     */
    public DataSourceProperties getSchema2DataSourceProperties() {
        return mapToDataSourceProperties(
                customDataSourceProperties.schema2().connectionProperties().username(),
                customDataSourceProperties.schema2().connectionProperties().password(),
                customDataSourceProperties.schema2().connectionProperties().jdbcUrl()
        );
    }

    /**
     * DataSourcePropertiesを取得
     *
     * @param url DatabaseのエンドポイントURL
     * @return DataSourceProperties
     */
    private DataSourceProperties mapToDataSourceProperties(String username, String password, String url) {
        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName(DRIVER_CLASS_NAME);
        dataSourceProperties.setUsername(username);
        dataSourceProperties.setPassword(password);
        dataSourceProperties.setUrl(url);

        return dataSourceProperties;
    }

    /**
     * schema2スキーマ用のDataSourceのプロパティを作成
     *
     * @return DataSourceのプロパティ
     */
    @Bean(name = SCHEMA2_DATA_SOURCE_PROPERTIES_NAME)
    @Primary
    public DataSourceProperties schema2DataSourceProperties() {
        return getSchema2DataSourceProperties();
    }

    /**
     * schema2スキーマ用のDataSourceを作成
     *
     * @return DataSource
     */
    @Bean(name = SCHEMA2_DATA_SOURCE_NAME)
    @Primary
    public DataSource schema2DataSource(@Qualifier(SCHEMA2_DATA_SOURCE_PROPERTIES_NAME) DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    /**
     * schema2スキーマ用のSQLセッションファクトリを作成
     *
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean(name = SCHEMA2_SQL_SESSION_FACTORY_NAME)
    @Primary
    public SqlSessionFactory schema2SqlSessionFactory(@Qualifier(SCHEMA2_DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        final SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
        if (Objects.isNull(sqlSessionFactory)) {
            throw new NullPointerException();
        }

        sqlSessionFactory.getConfiguration().setCacheEnabled(false);
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setDefaultScriptingLanguage(RawLanguageDriver.class);

        return sqlSessionFactory;
    }
}

コードの構成自体は「データベース1」の「schema1」への設定と全く同じですが、

customDataSourceProperties.schema2().connectionProperties().username()

のように所々、「schema2」用の設定を読み込んでいます。

注意していただきたいのは、@Beanアノテーションを付けて各データソースについて登録したBeanは型が重複してしまうことです。
そこで、nameパラメータを指定し、DIする際にはQualifierを付けてBeanを指定しています。

このコードで注目してほしい点は、 各データソースへの接続設定クラスにつけている以下のアノテーションです。

@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema1",
        sqlSessionFactoryRef = "schema1SqlSessionFactory"
)
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema2",
        sqlSessionFactoryRef = "schema2SqlSessionFactory"
)

これを、設定することで、com.example.sample_project.persistence.schema1パッケージに記述したクエリはschema1SqlSessionFactoryを使って、「データベース1」の「schema1」へ接続して実行され、

com.example.sample_project.persistence.schema2パッケージに記述したクエリはschema2SqlSessionFactoryを使って、「データベース2」の「schema2」へ接続して実行されるという設定を行うことができるのです。

つまり、それぞれのデータソースで実行されるクエリを記述するためのパッケージを分けることができ、まとまりが良くなります。

なお、指定するパッケージはご自身の使用したいものを設定してください。

これで、複数のデータソースに接続するための設定はおわりです。

2度目ですが、この設定の場合には、「データベース1」の「schema1」からデータを取得する場合には、com.example.sample_project.persistence.schema1パッケージに、「データベース2」の「schema2」からデータを取得する場合には、com.example.sample_project.persistence.schema2パッケージにクエリを記述したコード or ファイルを置くようにしてください。

おわりに

今回は、JavaのSpringBootでMyBatisを利用して複数のデータソースに接続する方法を紹介しました。

では、また次回。

参考文献

Javaで、文字列内に文字列変数を挿入する方法

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

本記事は、エキサイトホールディングス Advent Calendar 2023の4日目のものになります。

qiita.com

良ければ他の記事もどうぞ!

さて、コーディングをしていると、文字列の中に変数で定義してある文字列を挿入したい場合があります。

今回は、Javaで変数挿入を行う方法を3つ紹介します。

1. + を使った連結

最も直感的なのは、 + を使った連結でしょう。

String variable = "world";

String text = "Hello " + variable + "!";

System.out.println(text);

結果

Hello world!

方法自体はシンプルで非常にわかりやすいですが、挿入したい変数が多くなればなるほど式が複雑になってしまいます。

2. String.format を使う方法

String.format を使うこともできます。

String variable = "world";

String text = String.format("Hello %s!", variable);

System.out.println(text);

結果

Hello world!

%s などを文字列の中に入れておき、第2引数以降に渡した変数の内容に置換する」というこの方法は他の言語でもよく見られる方法であり、見慣れている方も多いのではないでしょうか。

一方で、 複数の変数を挿入したい場合は、たくさんの %s 等が文字列の中に並ぶことになるため、どこにどの変数が置換されることになるのか分かりづらくなってしまうこともあります。

なおJava15以降であれば、文字列に直接 .formatted を使うことで同じことができます。

String variable = "world";

String text = "Hello %s!".formatted(variable);

System.out.println(text);

結果

Hello world!

docs.oracle.com

3. 独自の文字列で置換する方法

上記とは別に、独自の文字列で置換する方法もあります。

String variable = "world";

String text = "Hello {world}!".replace("{world}", variable);

System.out.println(text);

結果

Hello world!

{world} など独自のわかりやすい単語を文字列の中に入れておき、あとから replace を使って置換する方法です。

置換するための単語を適切に設定しておけば、どこにどの変数が置換されるかがわかりやすく、その意味では2番目の String.format を使った方法の改良版とも言えます。

ただし、もちろん置換用の単語を適当にしてしまっては逆に分かりにくくなってしまうでしょう。

また、万が一元の文章に置換用の単語と全く同じ単語が含まれていた場合、想定外の部分に変数が入ってしまうおそれもあります。

おまけ:文字列内で変数を展開することはできない

これまで3つ紹介してきましたが、もっとも理想は、以下のように文字列内で変数を展開することではないでしょうか。

String variable = "world";

String text = "Hello ${variable}!";

System.out.println(text);

ですが、残念ながら以下のように出力されます。

結果

Hello ${variable}!

Javaではこの方法は使えません。

ただし、まだプレビューではあるものの、Java21から「String Templates」として文字列内での変数展開の方法が示されています。

openjdk.org

将来この機能が正式採用されれば、非常に有力な選択肢となるでしょう。

最後に

Javaで文字列に変数を挿入する方法を3つ紹介してきました。

それぞれメリット・デメリットがあるので、適切に使い分けると良いでしょう。

また、例えばURLを組み立てるときに UriComponentsBuilder を使えば、文字列への変数挿入を行わなくてもクエリパラメータの組み立てなどを行うことが可能です。

文字列への変数挿入を行いたい場合は、そもそもまず文字列への変数挿入が必要なのかというところから考え始めても良いかもしれません。

GitHub Actionsでデプロイした時に、自動的にCSSファイルをS3にアップロードする

はじめに

こんにちは、新卒1年目の岡崎です。エキサイトホールディングス Advent Calendar 2023の4日目を担当します。

今回はGitHub Actionsを使って、Amazon ECSにアプリケーションをデプロイした時に、自動的にCSSファイルをS3にアップロードする実装を行いました。その備忘録として記事に残します。

環境

S3にCSSファイルをアップロードする

S3の機能の1つに、CSSファイルなどの静的なファイルの管理があります。S3について詳しく知りたい人は、公式ドキュメントをご覧下さい。

上記の通り、静的なファイルの管理はS3が行うので、CSSファイルに変更があった場合、S3にアップロードする必要があります。

よって今回は、任意のCSSファイルが存在するディレクトリを、アプリーションをデプロイした時に、S3に自動でアップロードする方法を紹介します。

実装

GitHub Actionsのワークフローは.github/workflows/配下のYAMLファイルに記載しています。そのファイルに、以下のような実装を行います。

- name: upload to Amazon s3 for CDN
  env:
      AWS_ACCESS_KEY_ID: アクセスキー
      AWS_SECRET_ACCESS_KEY: シークレットキー
  run: |
     aws s3 cp --recursive --region ap-northeast-1 アップロードしたいCSSファイルがあるディレクトリのpath s3: s3のpath

クライアントがCSSファイルにアクセスする方法

クライアントは、静的ファイルを取得するために、以下の手順でCSSファイルにアクセスしています。

  1. クライアントがCSSファイルにアクセスします。
  2. CloudFrontは、リクエストされたCSSファイルのキャッシュの有無を確認します。
    • キャッシュがあれば、それをクライアント側に返します。
    • キャッシュがなければ、次に進みます。
  3. CloudFrontはS3からCSSファイルを取得し、その時に取得したCSSファイルをクライアントに返します。

詳しくは公式ドキュメントに記載があります。

CloudFrontのキャッシュを削除する

CloudFrontにキャッシュがあると、新しいCSSファイルがS3にアップロードしても、クライアントは古いCSSファイルを取得してしまいます。

この問題を解決するために、CloudFrontのキャッシュを削除する必要があります。

実装

これも.github/workflows/配下のYAMLファイルに記載を行います。実装例は以下です。

- name: Clear cache in CloudFront
  env:
     AWS_ACCESS_KEY_ID: アクセスキー
     AWS_SECRET_ACCESS_KEY: シークレットキー
  run: |
     aws cloudfront create-invalidation --distribution-id ディストリビューションID --paths "キャッシュを消したいファイルが存在するディレクトリのpath"  

キャッシュの削除は、1ヶ月で1000pathまで無料で行うことができます。その後は有料になり、1pathごとに0.005USDかかります。

詳しくは公式ドキュメントに記載があるので、興味がある人は見てください。

まとめ

GitHub Actionsを使って、Amazon ECSにアプリケーションをデプロイした時に、CSSファイルをS3に自動でアップロードしたり、CloudFrontのキャッシュを削除したりする方法を紹介しました。

ここまで読んでいただきありがとうございました。

RiverpodのStreamProviderを使ってPushNotificationを実装してみた

はじめに

エキサイト株式会社の高野です。エキサイトホールディングス Advent Calendar 2023の3日目を担当させていただきます。

今回はFlutterにおけるRiverpodのStreamProviderを使ってPushNotificationを実装した話です。

動作環境

Flutter: 3.13.4

hooks_riverpod: 2.3.4

riverpod_annotation: 2.0.4

rxdart: 0.27.1

実装方法

まず始めに実際のコードを載せておきます。

@Riverpod(keepAlive: true)
Stream<PushNotificationData?> pushNotificationStream(
  PushNotificationStreamRef ref,
) async* {
  final event = await ref.watch(pushNotificationOnMessageProvider.future);
  yield ref.watch(pushNotificationProvider).onOpenMessage(event.data);
}

@Riverpod(keepAlive: true)
Stream<RemoteMessage> pushNotificationOnMessage(
  PushNotificationOnMessageRef ref,
) {
  return Rx.merge([
    Stream.fromFuture(FirebaseMessaging.instance.getInitialMessage())
        .whereNotNull(),
    FirebaseMessaging.onMessage,
    FirebaseMessaging.onMessageOpenedApp,
  ]);
}

まず一つ目のポイントとしているのは、Pushの監視状態は途中で切れてほしくないのでkeepAliveをtrueにしています。 次に以下の部分です。

  return Rx.merge([
    Stream.fromFuture(FirebaseMessaging.instance.getInitialMessage())
        .whereNotNull(),
    FirebaseMessaging.onMessage,
    FirebaseMessaging.onMessageOpenedApp,
  ]);

初回の通知確認、アプリを開いている時に通知を開いた場合、アプリを閉じている時に通知を開いた場合という3つを区別なく処理をします。

こちらの各Streamを同一StreamにするProviderを作成しました。

  final event = await ref.watch(pushNotificationOnMessageProvider.future);
  yield ref.watch(pushNotificationProvider).onOpenMessage(event.data);

そして ref.watch を使用して流れてくる値を監視し、整形してまたStreamとして流します。

ref.listen(pushNotificationStreamProvider, (_, asyncValue) {
      final data = asyncValue.valueOrNull;
      if (data == null) {
        return;
      }
      // ここに遷移処理等を書く
}

最後にこのProviderをlistenしてあげ、Push通知を開いた際の処理を書きます。

意外と行数も増えることなく書くことができました。

最後に

エキサイトではエンジニアを随時募集しています。 興味がありましたらお気軽にご連絡いただければ幸いです。

www.wantedly.com

MySQL5.7から8.0へAmazon RDS ブルー/グリーンデプロイを使って移行する

こんにちは。エキサイトでエンジニアをしている吉川です。

エキサイトホールディングス Advent Calendar 2日目の記事になります。

私が担当しているサービスで使用しているMySQL DBについて、先日5.7系から8.0系にバージョンアップを行いましたので、ご紹介させていただきます。

環境

・Amazon RDS Aurora 
・旧エンジンバージョン:5.7.mysql_aurora.2.11.2
・新エンジンバージョン:8.0.mysql_aurora.3.04.0
・クラスターの状態:1DBクラスターに、1リーダーインスタンス、1ライターインスタンスが紐付く

Amazon RDS ブルー/グリーンデプロイ手順

Amazon RDS ブルー/グリーンデプロイとは、RDSを移行する際のダウンタイムを1分かからない(=移行中に抜け漏れるデータをほぼなくせる)ようにできるサービスです。

公式リファレンス

以下のようなステップをマネジメントコンソールからポチポチで作業できます

  1. 新しいステージング環境を作成し、旧環境とステージング環境を同期
  2. ステージング環境のDBエンドポイントを旧環境のものに、旧環境のDBエンドポイントは別のものに各々変更する

早速やっていきます。

新しいステージング環境を作成し、旧環境とステージング環境を同期

  1. DBクラスターを選択→アクションから「ブルー/グリーンデプロイの作成」
  2. デプロイの識別子、新DBクラスターのバージョン、DBクラスターのパラメータ、DBインスタンスのパラメータを選択して開始
  3. 完了するとステージング環境ができ、旧環境の変更は自動で同期されるようになります

以下のような状態になればOKです。

なおステージング環境に旧環境のデータがコピーされるので、この切り替え前の時点ではそこそこ時間がかかります。 手元の環境で空のDBで試したところ1時間弱かかっていました。全部まとめて数分で終わる話かなと最初思っていましたが勘違いでした。。。(流石にそんなオイシイ話はないですね)

ハマった点

  • 現在使用しているDBクラスター用のパラメータグループについて、binlog_format=ROW(またはMIXED)にしておく必要があります
  • 現在使用しているパラメータグループが同期できていないとステージング環境を作成できません
    • もしbinlog_format=ROW(またはMIX)になっていない、またはパラメータグループを変更しても同期できていない場合は、DBインスタンスを再起動する必要があります(この再起動にはダウンタイムが発生します)
  • ステージング環境を作る時点で、そこで使用する新しいパラメータグループが必要になります。デフォルトでもOKですが、旧環境でパラメータをデフォルトから変更していたので、新規でaurora-mysql8.0用のパラメータグループを作成しました。
  • 旧環境のインスタンスクラスが新環境で使えない場合、ステージング環境を作る前にクラスを上げる必要があります
    • 5.7系ではdb.t3.smallを使っていましたが、8.0系ではdb.t3.mediumから使用可能なので、先に上げることになりました

ステージング環境のDBエンドポイントを旧環境のものに、旧環境のDBエンドポイントは別のものに各々変更する

  1. 上記の写真で「ロール = ブルー/グリーンデプロイ」となっている識別子を選択→アクションから「切り替え」
  2. 変更内容を確認したら実行
  3. 数分で完了する

以下のような状態になればOKです。

  • 旧DBインスタンス名、およびエンドポイント名には「old」が入ります
  • 新DBインスタンス名、およびエンドポイント名は旧環境と同一です
  • リージョン、AZ、クラスターの内どれがリーダーでどれがライターか、などもよしなに引き継いでくれます

移行タスク進行

私のチームでは「ライブラリ改善タイム」というのを設けていて、業務時間内にMySQLや開発言語のバージョンアップの調査・実施を行えるようにしています。頻繁にやるものではないので、必要になった際に週1回3、4時間間隔で実施する形をとっています。

今回の移行はその時間を使って行いました。またMySQL5.7系→8.0系はそこそこ破壊的な変更があり、プロダクトにどこまで影響があるかチェックするのもこの時間に行なっていました(Blue/Greenデプロイとは主旨がずれるので今回は割愛します)。

個人的に、このような機能開発とは別だけど必要なタスク、というのは専用の時間をとって一気にやった方が進むなと思いました。 チーム全体でライブラリ改善を行うので、コード書いてなくても後ろめたさがないとも感じました。

また「ライブラリ改善タイムで行ったことはドキュメントにまとめる」というルールがあるので、本番環境で実施する際や、このように社外に公開するときに役立つとも思います。

旧環境の削除

旧環境を残しておくのは料金的にはあまりよろしくないです。 インスタンスを停止させることはできますが、1週間で再起動されてしまいます。 継続して停止させたい場合は、EventBridgeなどを使って明示的に停止し続けさせることになります。

停止させ続ける必要も特にないので、今回は移行完了後1週間をメドにクラスター、インスタンスを削除するようにしました。 (念のため削除前に最終スナップショットはとっています)

さいごに

いかがだったでしょうか?これから5.7系->8.0系の移行作業を行う方、また今後のバージョンアップの際の助けとなれば幸いです。 Blue/Greenデプロイ自体は本当に簡単に済んだので、個人的には今後の破壊的変更はあまりないといいなと思っています(笑)

今年のアドベントカレンダーも見どころ満載でお届けしますので、明日以降の記事もぜひお楽しみください!

またエキサイトではデザイナー、エンジニアを絶賛募集しております! ご興味があればこちらからご連絡ください!

www.wantedly.com

SpringBootとInfinispanの組み込みモードでローカルなキャッシュ機構を作る

エキサイト株式会社エンジニアの佐々木です。エキサイトホールディングス 2023 advent calendar 1日目を担当させていただきます。

qiita.com

今回は、SpringBoot/Javaで、サーバローカルなキャッシュをRedHatが開発しているInfinispanの組み込みモードを使用し実装していきます。

前提

## Java

openjdk version "21.0.1" 2023-10-17 LTS
OpenJDK Runtime Environment Corretto-21.0.1.12.1 (build 21.0.1+12-LTS)
OpenJDK 64-Bit Server VM Corretto-21.0.1.12.1 (build 21.0.1+12-LTS, mixed mode, sharing)
## Gradle

------------------------------------------------------------
Gradle 8.4
------------------------------------------------------------

Build time:   2023-10-04 20:52:13 UTC
Revision:     e9251e572c9bd1d01e503a0dfdf43aedaeecdc3f
## SpringBoot

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

設定

build.gradleはこのように設定する。

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-cache'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    developmentOnly 'org.springframework.boot:spring-boot-devtools'
    implementation 'org.infinispan:infinispan-spring-boot3-starter-embedded:14.0.13.Final'  // SpringBoot3に対応したinfinispanの組み込みモードのライブラリ
    implementation 'org.infinispan:infinispan-component-annotations:10.1.8.Final'   // infinispan共通で使用するライブラリ
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

SpringBootの設定用のコードは下記のように実装する。

@Configuration
public class CacheConfig {

    /**
     * キャッシュの設定を行う.
    */
    @Bean
    public EmbeddedCacheManager embeddedCacheManager(EmbeddedCacheManager embeddedCacheManager) {
        Arrays.stream(CacheType.values()).forEach(e -> {
                    ConfigurationBuilder configurationBuilder = new ConfigurationBuilder();
                    org.infinispan.configuration.cache.Configuration configuration = configurationBuilder
                            .expiration()
                            .lifespan(e.getTtl(), TimeUnit.SECONDS) // キャッシュのTTLを設定する
                            .build();

                    embeddedCacheManager.defineConfiguration(e.getName(), configuration);  // 上記で設定した内容を CacheNameと共に、設定を行う
                }
        );
        return embeddedCacheManager;
    }
    
}

enum CacheType {  // キーごとにTTLを設定する用の enumを作成する

    MIN_1(CacheType.MIN_1_KEY, 60)  // 1分のキャッシュ設定
    , SECOND_30(CacheType.SECOND_30_KEY, 30); // 30秒のキャッシュ設定

    public static final String MIN_1_KEY = "MIN_1";
    public static final String SECOND_30_KEY = "SECOND_30";

    @Getter
    private final String name;
    @Getter
    private final long ttl;
    CacheType(String name, long ttl) {
        this.name = name;
        this.ttl = ttl;
    }
}

SpringBootのController/Serviceの実装は下記のようになる。

interface DemoService {

    Object get5SecondsExpirationData() throws InterruptedException;

    Object get10SecondsExpirationData() throws InterruptedException;
}

@RestController
@RequestMapping("")
@RequiredArgsConstructor
public class DemoController {

    private final DemoService demoService;

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @GetMapping("cacheExpiration5Seconds")
    public Object cacheExpiration5Seconds() throws InterruptedException {
        return LocalTime.now().format(formatter) + ":" + demoService.get5SecondsExpirationData();
    }

    @GetMapping("cacheExpiration10Seconds")
    public Object cacheExpiration10Seconds() throws InterruptedException {
        return LocalTime.now().format(formatter) + ":" + demoService.get10SecondsExpirationData();
    }
}

@Service
class DemoServiceImpl implements DemoService {

    DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    @Cacheable(CacheType.SECOND_10_KEY)  // 10秒キャッシュ指定
    public Object get10SecondsExpirationData() throws InterruptedException {
        Thread.sleep(10000);   // スリープ10秒
        return "expiration 10 seconds \n";
    }

    @Override
    @Cacheable(CacheType.SECOND_5_KEY)  // 5秒キャッシュ指定
    public Object get5SecondsExpirationData() throws InterruptedException {
        Thread.sleep(5000);  // スリープ5秒
        return "expiration 5 seconds \n";
    }
}

Service層のメソッドにアノテーションでそれぞれのキャッシュ時間を指定します。タイプセーフにキャッシュ指定できるようにしています。 検証用にService層内のメソッドが実行される場合は、それぞれ5秒、10秒待たされる実装になっています。

検証

5秒キャッシュの方のAPIを実行してみます。

5秒キャッシュ

キャッシュの有効期限が過ぎたタイミングで、5秒待たされるような挙動になっています。

まとめ

今回はInfinispanの組み込みモードを使用して、サーバーローカルなキャッシュ機構を作成してみました。SpringBootではこのようなライブラリを使用しなければ、HashMapを使用したキャッシュが作られますが、有効期限などの設定は自前で実装することになるので、これだけの設定で作れるは便利かと思います。サーバーローカルなキャッシュを設定とリモートサーバーキャッシュの両方を使用する設定は次回ブログに書きます。

最後に

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

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

BEAR.Saturdayで画面を作るために必要な三つのコンポーネントの概要

はじめに

こんにちは。新卒1年目の岡崎です。エキサイトホールディングス 2023 advent calendar 1日目を担当させていただきます。

qiita.com

BEAR.Saturdayでの開発で苦戦しました。
今回の備忘録では、BEAR.Saturdayで画面を作るために必要な三つのコンポーネントの概要をまとめます。

BEAR.Saturdayとは

BEAR.Saturdayとは、PHPのWebアプリケーションフレームワークです。さらに詳しく知りたい人は、公式ドキュメントをご覧ください。

注意事項

  • BEAR.SaturdayはサポートされているPHPのバージョンが7.4までなので、今後の新規利用は非推奨です。
  • BEAR.Saturdayは既にサポートが終了しています。BEAR.Saturdayの後続として、BEAR.Sundayがリリースされています。

BEAR.Saturdayの概要

クライアントがURLでアクセスし、画面に任意のデータが表示されるまでの流れは、以下のようになります。

  1. クライアント側がURLを使用して、ページにアクセスします。
  2. リソースがデータを取得し、ページにそのデータを返します。
  3. ページがビューにデータを渡し、ビューの内容が画面に表示されます。

BEAR.Saturdayでは、ページ・リソース・ビューの3つのコンポーネントが使われます。
これらの役割について、詳しく紹介していきます。

ページ

ページは、Page.phpが親クラスです。onInjectonInitonOutputの三つの要素で構成されています。

class Page_Sample_Index extends BEAR_PAGE
{
    public function onInject()
    {
        // 処理が記述される
    }

    public function onInit(array $args)
    {
        // 処理が記述される
    }
        
    public function onOutput()
    {
         $this->display();
     }
}

App_Main::run('Page_Sample_Index');

実行順番は、onInjectonInitonOutputの順で実行されていきます。

onInject

依存関係の注入や、クエリパラメーターの受け取りを行います。

onInit

リソースからデータを取得したり、取得したデータを加工したりします。

onOutput

ビューにデータを渡します。

リソース

リソースは、Ro.phpが親クラスです。外部のAPIやDBと繋いで、データの取得・作成・削除・更新を行います。
リソースは二つの要素で構成しますが、取得・作成・削除・更新でそれぞれ要素が異なります。

取得の場合

取得の場合は、onInjectonReadで構成します。onReadは、取得処理を実装します。

class Sample_List extends BEAR_Ro
{
    public function onInject()
    {
        // 処理が記述される
    }

    public function onRead($values)
    {
        // 処理が記述される         
    }
}

作成の場合

作成の場合は、onInjectonCreateで構成します。onCreateは、作成処理を実装します。

class Sample_Create extends BEAR_Ro
{
    public function onInject()
    {
        // 処理が記述される
    }

    public function onCreate($values)
    {
        // 処理が記述される             
    }
}

削除の場合

削除の場合は、onInjectonDeleteで構成します。onDeleteは、削除処理を実装します。

class Sample_Delete extends BEAR_Ro
{
    public function onInject()
    {
        // 処理が記述される
    }

    public function onDelete($values)
    {
        // 処理が記述される             
    }
}

更新の場合

更新の場合は、onInjectonUpdateで構成します。onUpdateは、更新処理を実装します。

class Sample_Update extends BEAR_Ro
{
    public function onInject()
    {
        // 処理が記述される
    }

    public function onUpdate($values)
    {
        // 処理が記述される             
    }
}

ビュー

ビューでは、SmartyというPHPのテンプレートエンジンを使います。Smartyについて詳しく知りたい人は、公式ドキュメントをご覧ください。
ここではページから渡された値の表示を使って、画面上に表示を行います。

最後に

今回は、BEAR.Saturdayで画面を作るために必要な三つのコンポーネントの概要をざっくりと紹介しました。
ここまで読んでいただきありがとうございました。

Next.jsのLayoutにpropsを渡したい

はじめに

BB.excite事業部でエンジニアをしている小川です。

はじめに注意書きですが、本記事ではNext.js 12.x の Layouts機能を取り扱います。

Next.jsのPages Routerには同じ要素を再利用するための機能としてLayoutパターンが用意されています。 また、Layoutはページ遷移時に再レンダリングが行われないようになっているため、状態を維持できるようになっていることが特徴的です。

nextjs.org

全てのページに共通して同一のレイアウトということであれば_app.jsなどにLayoutを適用すれば済む話ですが、大抵はページごとに多少の違いを持たせたくなるものです。

今回はレイアウトを変えるほどではないけれど、propsで多少の変更を加えたい!というときにpropsを渡す方法があるのでご紹介です。

Per-Page Layoutsを使ってみる

まずは公式ドキュメントで紹介されているベーシックなLayoutを定義します。

components/BasicLayout.jsx

import Navbar from './navbar'
import Footer from './footer'

export const BasicLayout = (page) => {
  return (
    <>
      <Navbar />
      <main>{page}</main>
      <Footer />
    </>
  )
}

定義したレイアウトを使うときは以下のような記述になります。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}
 
Page.getLayout = (page) => BasicLayout(page) // ← ココ

レイアウトの定義をいくつかしておいて、呼び出し側でどのレイアウトを呼び出すか決めれば使い分けられて便利ですね。

propsを渡す

ここからが本題で、Layoutでpropsを受け取れるようにします。

components/basic-layout.jsx

import Navbar from './navbar'
import Footer from './footer'

export const BasicLayout = (page, props) => {
  return (
    <>
      <Navbar />
      <p>{props}</p>
      <main>{page}</main>
      <Footer />
    </>
  )
}

Layoutを呼び出す側では以下のように記述して渡します。 単純に第2引数で渡すだけですね。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}

const props = "sample text"
 
Page.getLayout = (page) => BasicLayout(page, props)  // ← ココ

最近のドキュメントやNext.jsのexampleを見ていると以下のような記述もできるようです。

この辺りはお好みでしょうか。

pages/index.jsx

import BasicLayout from '../components/basic-layout'
 
export default const Page = () => {
  return (
    /** Your content */
  )
}
 
Page.getLayout = (page) => {
  <BasicLayout>
    <p>propsを渡さなくてもちょっとした内容であればここに書ける</p>
    {page}
  </BasicLayout>
}

初めてのオフライン展示会からデザイナーが学んだこと

こんにちは! SaaS・DX事業部デザイナーの鍜治本です!

技術ブログとは毛色が少し異なりますが、エキサイトのSaaS事業部としてオフラインの展示会に出展し、デザイナーとして体験したことをさっくり書き起こします!

出展したイベント概要

2日目(11/22) ブースの様子
今回はパシフィコ横浜で開催された「BOXIL EXPO IT・DX展 in TOKYO 2023」に2日間出展していました。 過去には福岡、今回初めて横浜で開催され、出展した企業数はおよそ70社。各社ブースが思い思いの装飾を施し開催されました。

イベントサイトはこちら event01.expo.boxil.jp

展示したもの

今回の出展ではIT・DX分野を取り扱う展示会であるため、SaaS・DX事業部の経営管理クラウドツール「KUROTEN」と、エキサイトで現在注力している「生成系AI開発支援」を展示し、幅広い業界・職種の来場者にアプローチしました!

KUROTENについてはこちら kuroten.biz

エキサイトのAI事業についてはこちら www.excite-holdings.co.jp

ブース運営と来場者の反応

出展するまでの間、デザイナーとしてブース装飾アイテムの準備や、配布するパンフレットの作成しました。が、話し出すとキリがなくなるので、この記事では実際の会場について触れます。

経営管理ツールとAI開発支援への反応の違い

ブース内に設置したパネル(KUROTEN事業とAIソリューション事業)
KUROTENのインサイドセールスやフィールドセールスが苦戦していると聞いていた通り、KUROTENは来場者への声かけの反応が得られにくい印象でした。元々「経営企画」といった少ないターゲットを指していることもあり、改めて「予実管理SaaS」の間口の狭さを体感しました。
逆に「経営企画」を主業務としている方が、自らブース訪問をしてくださるなど、課題や悩み自体は顕在化していてたどり着くまでの道のり開拓が課題であると再認識しました。

一方で、生成系AIの開発支援は業界・職種を問わず幅広に興味を持っていただけていました。お話を聞く中には「昨今のAI技術活用の波で自社も何かしたい」「どんな方法があるか情報収集に…」など、立ち話程度で聞いていただいた方から温度感高めの方までいらっしゃり、話題性が高いゆえ広範囲の興味が寄せられていました。

デザイナーの視点から見た展示会の雰囲気

本来であれば「ザ・営業」をする展示会。今回は出展準備に関わっていた関係で、当日のフロント営業にも挑戦しました。会場で営業したり来場者と話してみた感想を、デザイナーなりに綴ってみます。

とにかく声をかけまくるフロント営業

通りかかる来場者に、とにかく声をかけまくります。来場者は我々に興味を持ってきているわけではないので、『いかにキーワードを耳に入れてもらえるか』『目に入った情報から興味を持ってもらえるか』が鍵となります。
元営業経験者からのアドバイスによると「とにかく目を見て声をかける」がコツ。そのコツに合わせてサービスのキーワードになりそうな「予実管理」「生成系AI」「エキサイトのtoB事業」を通りかかる人に向けて連発し続けます。

ちょっと見切れて写ってました
興味を持ってもらえたら、パンフレットを見せながら「どういったサービスなのか」「何が解決されるのか」を簡単に伝えます。来場者の所属や担当業務など伺いながらトークに要素を混ぜてゆき、「あるある、わかる」「言われてみればそうかも」と共感を引き出します。もう少し聞いてみようかな?と思ってもらえるように、相手が引かない距離感での会話を意識していました。
さらに興味を持ってもらえたら、デモ実演担当者まで繋ぎ、より具体的なペインの聞き出し、最終的にアポ獲得を目指すフローを回します。
大多数の来場者はブース前を通り過ぎるので、聞いてもらえない悔しさを感じましたが、いざ聞いてもらえた時には半端ない嬉しさがありました。

プレゼンしながら興味を引き出す

事前に配布用のパンフレットをカツカツスケジュールで大急ぎ作成しており、これを使って来場者にサービスの説明をします。
資料には自分が話す時のヒントが散りばめられていますし、興味を持った人が聞くときの視覚情報を補完してプレゼンができます。

実際に配布していた折パンフレット
自社サービスの説明やセールスポイントが頭で分かってはいるものの、いざ話そうとすると緊張で頭もこんがらがるもの。今回作成したパンフレットには、サービスの特徴・使用後のイメージ・導入事例…など記載していたため、話の糸口を引き出しながら初めての営業ができました。

達成度と反省点

今回2日間で来場した総数のうち、名刺交換や情報交換できた数(リード数)がおよそ15%、さらにアポに繋がったのがリードのうち2%という結果に。
一般的に展示会開催後のリード数は5~10%で、商談化につながるのはそのうちの1〜5%と言われています。今回の結果と比較しても、ブースでの営業やデモ実演によってより多くのリード獲得ができたこと、そしてリードからアポに繋げられました。普段の業務ではなかなか経験できる事柄では無いので、とても新鮮な経験を積めたと感じています。

一方で、エキサイトの認知度は「toC事業」によるものが多く、話しているほとんどの方々が「ポータルサイトやメディアじゃないの?」とおっしゃっていました。まだまだtoBSaaSをやっている認知は届いておらず、広める活動をする必要があると痛感しました。

さらに、展示会自体も初めて出展したことから、準備が行き届いていないこと、計画もままならずスケジュールがカツカツしていたこと、他社ブースと比較すると垢抜けなさがありました。例えば、通路側にディスプレイを設置しプロダクト紹介動画を流していたのですが、実際のツール画面をベースとした説明で初見者には分かりにくいものでした。

(ちょうど正面ブース「リーガルフォース」さんは、こんな感じの動画を流していました。)
youtu.be
他社ブースを見ると、アニメーションで課題解決までを簡潔にまとめているものや、ストーリーベースで課題を提示したり、理想のゴールイメージまで見せているものなど、サービス自体にまだ興味がない人が見るアプローチで作られていました。
デザイナーとしてこだわれるならもっとこだわりたい思いも強く、日頃から色々なアウトプット方法を模索し、作成し続けなければならないと思えた刺激的な1日であったと感じています。

まとめ

今回は初めての展示会出展で、まだまだ改善でる箇所や反省点がいくつかあります。
ただ、デザイナーとして普段体験できないことを一気に吸収できたので、これらを踏まえてもっとSaaS・DX事業部全体を盛り上げられるようにデザインで貢献したいです。

そんなエキサイトの、デザイナーやエンジニア(もちろんビジネスも!)を盛り上げてくれるメンバーを、新卒・中途問わず募集しています!
カジュアル面談も随時受付可能ですので、お気軽にお声かけください!🙆 recruit.jobcan.jp

新しく環境を作るために必要なざっくりとしたAWSの知識

はじめに

こんにちは。新卒1年目の岡崎です。

今回、初めて内部用のAPIのための環境を作成しました。その時に必要だった知識も分からずに始めたので、その備忘録として記事にまとめます。初めてAWSで環境を作るよ!という人や、まず何から初めて見ればいいのか分からない……といったような人の手助けになれば幸いです。

環境

大まかな流れ

ユーザーがURLでアクセスしてから、レスポンスを返すまでの大まかな流れを解説します。

  1. ユーザーがURLでアクセスする
  2. ALBがターゲットグループで振り分け
  3. コンテナ化されたアプリケーションが処理を行い、ユーザーにレスポンスを返す

それでは、これを実現するためには何の設定が必要なのでしょうか。

必要なAWSの設定

内部用のAPIの環境が必要だった設定を記しておきます。

今回は下記のサービスを使いましたが、作りたいものによっては他のサービスも検討してください。

Route 53

Route 53では、ドメインの作成を行うことができます。ドメインを作成することで、ユーザーはアクセスすることができるようになります。

ALB

ALBでは、設定したターゲットグループごとに振り分けを行います。サーバーにかかる負荷を均等に分散する役割を担います。

EC2

EC2は、AWS上に構築できる仮想サーバーの一つで、クラウド上で簡単に立ち上げることができます。

ECR

ECRでは、Dockerイメージの保存・管理を行います。リポジトリの作成、Dockerイメージのプッシュ/プル、Dockerイメージの詳細確認をすることができます。

ECS

ECSは、コンテナを管理するためのフルマネージドなAWSのサービスです。ここではECRで作成したDockerイメージからコンテナを起動することができます。

IAMロール

IAM ロールでは、AWSのサービスへのアクセス権限などを設定することができます。

最後に

数日前の私が必要だったAWSのざっくりとした知識をまとめました。正直、もっと早く勉強していれば良かったと思っていますが、学べる機会があったので良かったと思っています。もっと精進します……。

ここまで読んでいただきありがとうございました。

デザインのクオリティを上げる一手間!デザインTips集第二弾

はじめに

こんにちは、エキサイト株式会社3年目デザイナーの山﨑です。

今回は、デザインのクオリティを上げる一手間!デザインTips集第二弾を紹介したいと思います!

▼前回の記事はこちら▼

tech.excite.co.jp

意外と簡単にできるものばかりなので、ぜひ実践してみてください🙌

人物の背景に図形を組み合わせる時は、人物の頭を少しだけ出すと遠近感が生まれる

ウェビナーなどでよく使われるのですが、登壇者紹介時のデザインは人物の頭を少しだけ出してあげたり何かポーズをとっている場合はポーズをとっている手を出してあげたりすると遠近感が出て画面が少し賑やかになります。

人物と写真の背景を組み合わせる時は、人物により目がいくようにするため背景を少しだけぼかす

少しわかりづらいですが、画像のように切り出した人物に背景を組み合わせる場合は背景のみぼかしてあげると人物がくっきりと浮かび上がります。人物と背景の間に影を入れてあげるとより効果的です。

影の色は黒ではなく、背景写真の一番濃い色(この場合はネイビー)にすると馴染みやすいです。

Canvaの「ぼかし」機能を使って簡単に立体感を出せたり、人物だけ切り出さなくても「オートフォーカス」機能で背景のみぼかすことができるので非常におすすめです!

エフェクトを配置する時は対角線上に配置する

画面をより豪華に盛り上げたいときに使うキラキラは、対角線上に配置するとバランスよくまとまることが多いです。

その際にキラキラの大きさをほんの少しだけ変えてあげると単調にならない画面作りができると思います!

最後に

いかがだったでしょうか?

必ずこれが正解とも言えないのですが、最後のデザインの仕上げとして使えるので、ぜひ活用してみてください🙏

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

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

www.wantedly.com

Spring Securityで、同一ドメインにて別セッションでログイン管理をする方法

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

Spring Securityでセッションを使ってログイン管理をする場合、基本的にはドメイン単位でセッションが分かれます。

今回は、同一ドメインにて別セッションでログイン管理する方法を説明します。

1. プロジェクトを分ける

まずは、セッションを分けたいサービス同士を別プロジェクトとして作成します。

Nginx等のプロキシを使って、同一ドメインにアクセスが来てもパスなどをもとにリクエストが振り分けられるようにしておきましょう。

2. セッション用Cookie名を別々にする

次に、セッション用のCookie名を別のものにします。

この設定が肝で、これによって同一ドメインでも別セッションとして管理できるようになります。

Spring SecurityをMVCで扱っていれば、 application.yml に以下の設定を書くだけでCookie名を変更することができます。

server:
  servlet:
    session:
      cookie:
        name: CUSTOM_COOKIE_NAME

spring.pleiades.io

セッションを分けたいプロジェクト間で、別々の名前を設定しましょう。

これで、同一ドメインでも別のログインセッションとして扱われるようになりました!

終わりに

同一ドメインで別セッションのログイン管理をしたい、という需要はそこまで多くないと思いますが、いざそうなった時に使える情報ではないでしょうか。

またそれ以外にも、何かしらの事情でセッション用のCookie名を変えたい場合にも使えるでしょう。

そういった際に、何かしらの役に立てば幸いです!

タスクランナーをgo-taskにする

エキサイト株式会社メディア事業部エンジニア佐々木です。開発で使用するタスクランナーは、一般的にMakefileを使うものが多いかと思います。より簡単なgo-taskを紹介いたします。

インストール

Macの場合は、Homebrewを使用しインストールします。

brew install go-task

他のOSは、公式ドキュメント を参照してください。

タスクファイル

Taskfile.ymlを作成します。Makefileのようなものです。

YAML形式ですので、視認性が良いです。

version: '3'
tasks:
  helloworld:
    desc: hello world
    cmds:
        - echo 'hello world'
        - echo 'hello world' > output.txt

  helloshell:
    desc: hello shell
    cmds:
        - echo 'helloshell'
    silent: true

上記のように視認性が良いです。

実行

設定したタスクを実行してみます。

$ task helloworld helloshell 

task: [helloworld] echo 'hello world'
hello world
task: [helloworld] echo 'hello world' > output.txt
helloshell

正常に動作しています。設定で silent: true を設定することで、実行時の標準出力から task: xxx がなくなって結果のみになります。

タスク一覧

Makefileでは、呪文をかかないとタスク一覧がでてきませんでしたが、go-taskでは、タスク一覧を表示してくれるものが標準で備わっています。

$ task -a


 task -a
task: Available tasks for this project:
* helloshell:                 hello shell
* helloworld:                 hello world

desc も記載しておくことで、説明も出力してくれます。

まとめ

Makefileより視認性も使いやすさもあると感じました。標準導入に向けて活動していこうと思います。

最後に

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

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

FlutterKaigi 2023に参加してきました。

はじめに

エキサイト株式会社の高野です。

今回はFlutterKaigi 2023に参加してきましたのでその感想及びレポートになります。

聴講したセッション一覧

  • Flutterアプリにおけるテスト戦略の見直しと自動テストの導入
  • 出前館におけるFlutterの現在とこれから
  • Master of Flutter lifecycle
  • Dartのコード自動生成のパッケージを自作する方法について
  • Flutterで構築する漫画ビューア
  • 魅せろ!Flutterで目を惹くUIデザインを実装する

Flutterで構築する漫画ビューアに関しまして弊社の克さんが発表してくださいました。

補足としてのブログがありますのでぜひご一読ください。

tech.excite.co.jp

感想

詳しくはアーカイブを見ていただけたらと思います。

個人的には出前館様のFlutterでリプレイスを行った話が興味深かったです。

出前館様はあまり他に例を見ない韓国チームを含んだ上でのプロダクトということでした。 日本語をネイティブとしないメンバーがいる中で、英語での進捗確認だったり、チームビルディングに力を入れているそうです。

また、その後のテスト戦略としての話はあまりテストを書いてきていない自分にはとてもためになる内容でまたアーカイブをもとに参考にしたいと思います。

そしてもう一つ、AAkira様のFlutterのライフサイクルの話です。

下記にスライドリンクを貼りましたのでぜひご一読いただいて欲しい良い資料でした。

あまり自分で実装する際に意識していなかった

  • 画面のライフサイクル
  • アプリのライフサイクル
  • 遷移のライフサイクル

この部分について詳しく知ることができました。

ライフサイクルは、プロダクト開発でログを計測する際によく使います。このスライドをもとにもっと自身のプロダクトにおいて柔軟にライフサイクルの検知及びログの送信を実装できるように改善していきたいと思います。

speakerdeck.com

最後に

全部のセッションがすぐにためになるわけではないですがしっかりとアーカイブを見た上でさらにFlutterへの深みへと進んでいきたいと思いました。

このようなイベントを企画・運営してくださった運営の皆様、ありがとうございました。

採用情報

エキサイトではエンジニアを随時募集しています。 興味がありましたらお気軽にご連絡いただければ幸いです。

www.wantedly.com

FlutterKaigi 2023 参加レポート

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

FlutterKaigi 2023に参加してきましたので、その内容をレポートします。

FlutterKaigi 2023の概要

FlutterKaigi とは、FlutterやDartの知見を共有する技術カンファレンスです。 flutterkaigi.jp

会場は株式会社ナビタイムジャパン様にて行われました。

スタート前の会場の様子

聴講したセッション

セッション数は基調講演を含めて21セッションありました。

聴講したセッションについて紹介します。

基調講演「Flutter's 8 years journey」

Flutter GDEの方から、Flutterの8年間の歩みについてお話しいただきました。

Flutterは2015年にDart Developer Summitにて発表されました。 その後、Dartが開発言語として進化するとともに、FlutterもUI フレームワークとして進化してきました。

Flutterは、モバイルアプリの開発においては既に習熟してきています。今後の展望としては、Flutter WebやDesktopアプリといったモバイル以外の領域も充実していくであろうとのことでした。 また、Casual Games Toolkit のゲーム開発や、3Dレンダリングサポートの話もあり、Flutterが更に幅広く使われていく進化が期待できると思いました。

私がFlutterを触り始めたのは2022年からだったので、Flutter登場初期の話は興味深かったです。

「Flutter アプリにおけるテスト戦略の見直しと自動テストの導入」

WINTICKET のテスト戦略の見直しと改善事例の紹介でした。

speakerdeck.com

過去の障害事例から改善点を考えたり、テストピラミッド、テストマトリクスを用いて、体系的に現状を分析し、注力する部分を導いていく内容でした。

まとめとして、どのようなアプローチを取るべきか、まずは現状のプロダクトやチーム体制を分析することが重要であることを挙げていました。 また、導入コストが高いことや効果の実感までに時間を要することを挙げており、テスト設計に取り組むべきと思いつつも、ハードルが高い点は検討すべきと感じました。

「詳解!Flutterにおける課金実装」

TOKYO MIX CURRYのアプリ決済についての事例でした。 TOKYO MIX CURRYとは、アプリでしか買えないカレーを提供するサービスであり、実店舗におけるアプリ決済の具体的なシステム構成や陥った課題についてお話し頂きました。

システム構成としては、アプリ、サーバーサイドが登場する一般的な構成に加えて、決済端末であるSquareリーダーやレシートをプリントするためのEpsonといったハードウェアとも連携する仕組みになっています。

実際にあったユーザーの意図しない操作や外部SDK連携で困ったことがあり、それらを解決した方法について紹介頂きました。

DartによるBFF構築・運用 〜Dart Frog×melos〜」

株式会社ゆめみさんのDartによるBFFの事例について紹介頂きました。

speakerdeck.com

BFFは、一般的にクライアントサイドが開発することが多く、今回はクライアントサイドがFlutterアプリだったので、DartでBFFを構築することに至ったとの事でした。 サーバーサイドのDartフレームワークには、いくつか選択肢がありますが、今回は軽量かつ比較的新しいDart Frogを採用したそうです。

その他、Melosというパッケージを使ったマルチパッケージ構成(モノレポ)の話や、BFFとアプリのAPIレスポンスの型を共通化の話もあって興味深かったです。

Dartのコード自動生成の仕組みと、コード自動生成のパッケージを自作する方法について」

freezedやretrofitといったコードの自動生成をするパッケージが行っている自動生成の仕組みについての内容でした。

www.slideshare.net

サンプルプロジェクトが公開されているので、スライドと合わせて理解を深めていきたいです。 github.com

「Flutterで構築する漫画ビューア」

弊社所属のアプリエンジニアである katsuさんが、漫画ビューアの実装について登壇しました。

speakerdeck.com

内容の補足についてこちらの記事で紹介しております。 tech.excite.co.jp

「我々にはなぜRiverpodが必要なのか - InheritedWidgetから始まるappstate管理手法の課題」

Flutterにおける状態管理の仕組みと課題、解決策についての紹介でした。

docs.google.com

Riverpodでは、状態管理が持つ多くの課題に対して解決策を用意しており、利用する側はその解決策を理解しておく必要があるとのことでした。 会場でも多くの方がRiverpodを使っていると手を挙げており、興味関心が高いセッションだったと思います。

ノベルティー

ノベルティーグッズやスポンサー企業様のステッカーを頂いてきました。 ステンレスマグカップがかっこよかったので、使っていきたいと思います。

ノベルティーのステンレスマグカップ

おわりに

久々にオフライン開催のカンファレンスに参加してきました。

オフラインならではの会場の空気感や懇親会が楽しく、有意義なカンファレンスを体験できました。 運営して下さったFlutterKaigiスタッフの皆様、各スポンサー企業様、ありがとうございました。

採用アナウンス

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

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