Spring BootのRedisキャッシュで、Master/Replicaを呼び分ける方法

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

アドベントカレンダーも14日目となり、中盤を過ぎてきました。 今回はRedisとSpring Bootの話になります。

Redisの可用性を高めるために有用な手段ですので、参考にしていただければ幸いです。

はじめに

Redisでは、同じデータに対して、書き込み・読み込みが両方可能なMasterと、読み込み専用のReplicaを別々に設定することができます。 Masterを基本的に書き込み専用に、Replicaを読み込み専用に使用することで、可用性を高くすることができます。

今回は、そのような構成になっているRedisをキャッシュとして使う際に、Spring Bootでどのように呼び分けるかを解説していきます。

RedisのMaster/Replica

Redisは、オンメモリのkey-valueなデータベースです。 MySQLなどに比べて非常に高速で、一方でリレーショナルではないため、複雑なデータ結合が不要なキャッシュなどで使用されることが多いアプリケーションとなっています。

非常に高速とはいえ、もちろん負荷が高まればアクセスを捌ききれなくなることもあります。 その際、いくつか対応方法があります。

スペックを上げる

Redisに使用するサーバ等のCPUスペックやメモリサイズを上げることで、Redisが捌けるアクセス数を増やすことができます。

数を増やす

Redisを動かしているサーバ等の数を増やすことで、1台あたりのアクセス数を減らし、結果としてRedis全体が捌けるアクセス数を増やすことができます。

どちらにするかはその時々ですが、「数を増やす」対応だと、例えば1台落ちたときに他で対応できたりなど可用性が高まりやすい利点があります。 一方でデータに不整合が出ないよう、「1台のみの書き込み・読み込み両用のMaster」と「1台以上の読み込み専用のReplica」を用意し、書き込みはMasterで、読み込みは基本的にReplicaで行うようにするため、呼び分けをする必要が出てきます。

Spring Bootでは、次の方法で呼び分けることができます。

Spring Bootでの呼び分け方法

基本的なRedisキャッシュの設定に以下の設定を加えれば、Master/Replicaで呼び分けてくれるようになります。

package sample;

import io.lettuce.core.ReadFrom;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
@EnableCaching
public class RedisConfig {
    /**
     * Redis接続用設定のFactory
     *
     * @return Lettuce接続用設定のFactory
     */
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 環境ごとに値が違う場合は、application.yml等を使って分けてください
        String masterHost = "masterHost";
        Integer masterPort = 6379;
        String replicaHost = "replicaHost";
        Integer replicaPort = 6379;
        Integer database = 0;

        // ReadFromには他にもいくつかタイプがあるので、適したものを指定してください
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .build();

        // Amazon ElasticacheではRedisStaticMasterReplicaConfigurationが適していますが、構成によってはRedisStandaloneConfigurationが適している場合もあります
        // LINK: https://spring.pleiades.io/spring-data/redis/docs/current/reference/html/#redis:write-to-master-read-from-replica
        RedisStaticMasterReplicaConfiguration serverConfig = new RedisStaticMasterReplicaConfiguration(masterHost, masterPort);
        serverConfig.addNode(replicaHost, replicaPort);
        serverConfig.setDatabase(database);

        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}

基本的にはこれだけで、キャッシュに書き込む時はMaster経由で、キャッシュから読み込む時はReplica経由で、それぞれアクセスできるようになります。

最後に

Redisを複数台で動かすことは、可用性を高めるために非常に有効な手の1つです。 Spring Bootの場合は上記の方法で簡単に呼び分けができるようになるので、ぜひ試してみてください。


アドベントカレンダーはまだまだ続きます! 明日以降も見ていっていただけると幸いです!

qiita.com

DBeaver でいい感じに date 型に bind parameter する方法(日付型のみ)

エキサイトホールディングス Advent Calendar 2021 の14日目は、エキサイト株式会社の大澤 が担当させていただきます。

やりたいこと

SELECT :date  FROM dual;

このとき :date に対して 2021-12-07 15:01:00 の日付型をbindして実行したい

解決方法

SELECT :date  FROM dual;

:datetimestamp '2021-12-07 15:01:00' を bind して実行する

f:id:hibikiosawa4388:20211207152726p:plain
date に timestamp xxxx を bind

詳細

弊社では 一部で Oracle の DB を利用しています。 また、 Named parameters な ORM を採用しているため、 生 SQL を取得する場合は上記のようなクエリが取れます。

このときに DatetimeImmutable 型を バインドしている日付型については、 DBeaver (GUI な DB viewer) で うまくバインドできないと思っていました。

なので下記のように to_date でクエリをいちいち書き換えていましたが面倒でした。

SELECT to_date(:date, 'yyyy-mm-dd hh24:mi:ss')  FROM dual;

操作していてたまたま timestamp '2021-12-07 15:01:00' と書くことで用が済むことを発見したためここに記します。

応用使用例 (フラッシュバックを複数同時検索)

with timestamp_all as (
  select :timestamp1 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp1)
  UNION ALL
  select :timestamp2 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp2)
  UNION ALL
  select :timestamp3 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp3)
)
select * from timestamp_all;

Github ActionsでのECSへの手動デプロイ

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

アプリケーションのデプロイ方法は、エンジニアなら誰もが頭を悩ませるものでしょう。 コマンドラインAWS CodePipeline、Jenkinsなど、様々な方法が考えられます。

今回は、ECSへのデプロイ方法としてGithub Actionsを選択したときの方法と注意点について説明していきます。

Github Actions

Github Actionsは、Github上で任意の処理を実行できる機能です。 公式には以下のように説明されています。

Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized workflow.

手動実行はもちろん、指定時間に実行したり、ブランチやプルリクエストに何かしらのアクションがあったタイミングで自動実行させることもできます。 例えば

  • デフォルトブランチにブランチがマージされたら、自動的にタグを切る
  • プルリクエストが新しく作られたら、ユニットテストを実行する

などが可能です。

今回やりたいのは、「手動実行」でECSにデプロイする方法となります。

ECSへのデプロイ

Github Actionsは、 .github/workflows ディレクトリ配下に実行処理を書いたYAMLファイルを置くことで設定することができます。 Github Actionsの実行単位は workflow と呼ばれ、このディレクトリ配下に置かれる1ファイルごとに1workflowが設定されることになります。

ですが、ファイル作成前にまずは下準備をします。

AWS環境

今回はECSへのデプロイということで、関連する以下のようなAWSの環境を用意しておく必要があります。

  • ECR
  • ECS
  • コード上からAWSへアクセスするためのIAMユーザ

Secretsの設定

AWSへのアクセス用のキーやSlack通知用のWebhookなどは、セキュリティ性の高いものであるため、Githubのバージョン管理に含めるべきではありません。 Github Actionsでは、Githubの「Setting -> Secrets」にて、そういった文字列を外部に見られないように保存することができます。

保存した文字列は、Github Actions実行時に ${{ secrets.保存したキー }} の形式で参照することができます。

今回は、

を作成します。

なおSlack通知はデプロイに必須ではありませんが、これがあるとデプロイの運用が容易になるため今回は含めています。

Task Definition

ECSに使用するTask Definition用のJSONファイルも用意しておく必要があります。 イメージ名は後から上書きするので適当で大丈夫ですが、それ以外については適切なファイルを作っておきます。

設定ファイル

ここまで準備できたら、ようやくGithub Actionsの設定ファイルを作成します。

name: Deploy

# 同じ文字列がconcurrencyに指定されているworkflowは、二重で実行できなくなります
concurrency: deploy

# 手動実行をさせるため、workflow_dispatchを指定してください
on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      ECR_REPOSITORY: sample-ecr
      CLUSTER_NAME: sample-cluster
      SERVICE_NAME: sample-service
      TASK_DEFINITION: task-definition.json
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # AWS環境へデプロイするため、認証を行います
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # Dockerイメージをビルドし、ECRにpushします。また、outputでイメージ名を出力します
      # イメージのタグ名にはコミットSHAを使用します
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          IMAGE_NAME: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:latest .
          docker push $IMAGE_NAME:$IMAGE_TAG
          docker push $IMAGE_NAME:latest
          echo "::set-output name=image::$IMAGE_NAME:latest"

      # 出力されたイメージ名で、 あらかじめ作成しておいたTask DefinitionのJSONファイルのイメージ名を上書きします
      - name: Fill the Amazon ECS task definition with image ID
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.TASK_DEFINITION }}
          container-name: sample
          image: ${{ steps.build-image.outputs.image }}

      # 作成したTaskDefinitionを元にECSへのデプロイを行います
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          cluster: ${{ env.CLUSTER_NAME }}
          service: ${{ env.SERVICE_NAME }}
          wait-for-service-stability: true

      # Slackへ通知します
      - name: Slack Notification
        if: always()
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_CHANNEL: '#sample'
          SLACK_COLOR: ${{ (job.status == 'success' && 'good') || 'danger' }}
          SLACK_TITLE: "[${{ github.repository }}] にデプロイしました"
          SLACK_MESSAGE: "デプロイ結果:${{ job.status }}"
          SLACK_USERNAME: github actions
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          MSG_MINIMAL: ref,actions url

これで、準備は完了です。

あとは、Githubの「Actions」から手動で実行すればデプロイが可能です。

注意点

Github Actionsで手動実行する際、いくつか注意点があります。

ブランチしか選択できない(解決済み!)

かつては手動実行する際に選択できるのはブランチのみでしたが、最近になってタグを選択できるようになりました! 特にデプロイではタグによって制御している場合も多いと思うので、これは非常に嬉しいアップデートです!

デフォルトブランチにマージされないと実行できない

手動実行する場合は、デフォルトブランチにYAMLファイルがマージされていないと項目が出てきません。 最初は詰まりやすいところだと思いますので、注意しましょう。

デプロイをしたブランチ / タグがどれだか分かりづらい

ブランチやプルリクエストのアクションで自動実行されるものはGithub Actionsの一覧画面で実行元のブランチやタグが表示されていますが、手動実行のものは表示されないようです。 少し手間ですが、実行ログの「Checkout -> Checking out the ref」から確認できます。

uses で指定しているライブラリではできないことがある

例えば aws-actions/amazon-ecs-deploy-task-definition@v1 では、2021/12/13現在、ECSでのデプロイ時に platform-version の指定ができないようです。 指定したい場合は、自分でAWS CLIを使うようにするなど、方法を変えると良いでしょう。

最後に

Github Actionsは、

  • Githubをバージョン管理に使用している場合、リポジトリと密接につながっているため、エンジニアが見る場所がバラけづらい
  • 完全に独立した環境のため、共用デプロイサーバのように同時実行時のCPUやメモリの心配をする必要がない
  • Github Actions用の様々なライブラリが存在するため、やりたいことを簡単に設定できる

などの利点があります。 みなさんもぜひ使ってみてはいかがでしょうか?

SESのバンス対策

エキサイトでファクタリングの開発をしている、森脇です。

今回はファクタリングで使用しているSESのバンス対策について書こうと思います。

ファクタリング事業とは

BtoB向け後払い決済・請求代行サービスです 法人向けサービスの事業拡大に取り組む企業様の①請求・回収業務の効率化、②未回収リスクの解消、③資金繰りの改善を実現します。

法人向けサービスの事業拡大に取り組む際、顧客候補に対する与信審査や、成約した顧客に対する請求・回収業務、事業運営に必要な運転資金の確保などの付随業務が発生し、100%のリソースを営業先開拓や商品開発に注力することへの阻害要因となってきました。

与信審査/請求書発行/請求書送付/入金消込/督促等の請求・回収業務など付随業務をすべて代行します。加えて、登録された請求については全額を保証し、早期の資金ニーズがある場合には取引内容の登録後、最短翌日入金も可能とし、積極的な事業拡大に取り組む企業が抱える請求・回収業務の負荷や資金繰りに関する課題を解決します。

事業自体はクレディセゾン様と協業で行っております

prtimes.jp

システム概要

システムでは、請求データを登録するだけで、後は全て自動で行うようになっています。 期日になれば、請求書が作られ、メールか郵送で請求書が送付され、入金があれば消し込みを行う、DX化がされています

請求書の送付はSESを使ってメールで送信しております、システム構成はこんな感じです。

システム構成

f:id:moriwaki111:20211213093709p:plain

SESの送信ログをSNSを通して、SQSに溜めAWS Batchを使ってキューを取得して、解析を行い結果をDBに書き込みます。

オンプレ時代の課題

・ログをメールサーバーからどうやって取得するか ・メールのログをどうやってパースするのか ・解析処理をどういうタイミングで行うのか ・ログファイルの何処から処理を開始するか ・などなど

考えることは多岐にわたり、実装も大変です、AWSのサービスを使うとそう言った煩わしい問題がほぼ解決されます。

設定

すごく簡単です、SESのNotificationsの設定にSNSのトピックを設定します。 f:id:moriwaki111:20211213093954p:plain

SNSではサブスクリプション先として、SQSを指定してあげます。 あとはSQSからデータを取り出すバッチを書いて終了です。

テスト

面倒なテストですが、そこもAWSが用意してくれています。 以下の公式ドキュメントに詳しく書かれています。

docs.aws.amazon.com

バンスの確認をしたければ、bounce@simulator.amazonses.com 宛にメールを送ればバンスの処理を行うことができるようになっています。バンス以外にも苦情系のメアドも用意されているので、細かく状況に応じて処理を行うことができるようになっています。

最後に

AWSであれば簡単に実装でき、運用もほぼ手をかけることなく安定した運用が可能になるので、アプリケーションの開発に注力することができます。 今後もAWSのいろんなサービスに触れていきたいと思います。

【社内バリュー浸透施策】バリューポスターを製作した話

f:id:excite_ny:20211206190106p:plain

はじめに

こんにちは、エキサイト21卒デザイナーの山﨑です。 エキサイトホールディングス Advent Calendar 2021 2の12日目の記事を担当させていただいています!

qiita.com

今回はエキサイトのミッション・バリューが決定したので、その周知を図る社内ポスターを製作した話をしようと思います。 私が担当したのはエキサイトのバリュー5つを5枚のポスター連作でデザインしました。

エキサイトバリュー

1. 好奇心を起点にする。

好奇心の芽を大切に探究し続けることで、今を、そして、未来を変えていこう。

2. 当事者意識でやりぬく。 

大きな視野で自分ごと化してやり抜くことで、生まれる機会を成果と成長につなげよう。

3. 世の中に寄り添う。

世の中で、そして、自分や身の回りで、何が起こり、どのような流れがあるのか。世の中に寄り添うことで、小さな変化も大きな流れも感じとろう。

4. 素直さとリスペクトで学ぶ。

物事をありのままに見て感じる心を大切に、身近なこと、遠くのこと、歴史から学ぶ。身近な人、遠くの人をリスペクトし、そして、巨人の肩に乗ろう。

5. 得意なことで繋がり合うチームワークで。

肩書き、部門、社内外。あらゆるボーダーを超えて、一人ひとりが得意なことで繋がりあるチームワークで大きな仕事を形にしていこう。

イメージボード制作+すり合わせ

まず、イメージボードでポスターのイメージを固めていきます。

なるべく路線の違う画像を3〜6パターンほど集めてジャンル分けしていき、自分が参考にしたいと思ったものなどをメモしていきます。

エキサイトのコーポレートカラーは赤・黒・白なので、赤いポスターが多めですね🤔

赤はエネルギッシュでスポーティーなイメージなので、社内を盛り上げるためのバリューポスターとは相性がよかったです。

f:id:excite_ny:20211206184424p:plain

ラフ案制作

ラフ案とも言えない何かを生成しながら進めて行きます。 「バリューを体現した社員をピックアップして作るのが良いんじゃないか?」と思いつつこねくり回した残骸です。

f:id:excite_ny:20211211014554p:plain

ラフ案と言ってもほぼ完成に近いのでこの作業が一番時間がかかります。

社員選定+アポ取り+撮影+レタッチ

デザインが決まったらモデルとなる社員12人の撮影を行います。自前の一眼レフを使って写真撮影しました。

写真が下手くそなせいで何度も撮り直したりと割とバタバタしてしまい、写真の勉強もしないとなと反省しました…😢

デザイン完成+パネル貼り

f:id:excite_ny:20211211015252p:plain

色々あってデザインが完成しました!あとは試し印刷をして色味や誤字などの最終チェックを行います!

社内には大判プリンターがないので、A3の紙を9枚につなぎ合わせたもので印刷しました!(合計紙数は90枚ほどに…)

f:id:excite_ny:20211211015503j:plain

最終調整を終えたらついに完成!

デザインから被写体の撮影、試し印刷までやるのは大学生ぶりだったので、とても懐かしい気持ちになりました。

今回のポスターで少しでもバリュー浸透に貢献できれば良いなと思います!それではまた!

AWS Secres Managerでパスワード等をセキュアに管理する

はじめに

エキサイトホールディングス Advent Calendar 2021の12日目は、エキサイト株式会社の吉川が担当させていただきます! 今回は、AWSのシークレット情報管理サービスの1つである AWS Secrets Manager について触れていきます!

AWS Secrets Manager

AWS Secrets Managerでできること

アプリケーションを構築する上で、DBにアクセスするためのユーザー名やパスワードは特に慎重に取り扱う必要があります。またアプリケーション内でAWS外部のサービスにアクセスする場合も同様で、その外部サービスにアクセスするためのパスワード等は管理に気を使う必要があります。

AWS Secrets Manager(以下AWSは省略します)はこれらのシークレット情報をセキュアに管理・共有するためのサービスです。例えば以下のような流れで利用することができます。

  1. Secrets Managerにパスワードを保存
  2. アプリケーション側はまずSecrets Managerにアクセスしてパスワードを取得
  3. 取得したパスワードを用いてアプリケーションからDBにアクセスする

アプリケーションのソースコード等とパスワードが切り離されるので、よりセキュアに運用することができるのです。

それパラメータストアでよくね?

はい。上記の説明でそう感じた方もいるのではないかと思います(というか自分がそうでした)。AWSには パラメータストア というサービスがあり、Secrets Managerと同じくセキュアにパスワードなどを管理できます。ということで両者の違いは何なのか、使ってみて比べてみました。

違い1: キーと値のペアで保存する

パラメータストアでは1つのパラメータにつき1つ以上の「値」が設定でき、Secrets Managerでは1つのシークレットにつき1つ以上の「キーと値のペア」が設定できます。図にすると以下のような違いになります。

# パラメータストア
パラメータA
 └値a
 └値b
# Secrets Manager
シークレットA
 └キーa - 値α
 └キーb - 値β

例えばDBの「ユーザー名」と「パスワード」を1つのパラメータ/シークレット内で保存したい場合、パラメータストアでは、この2つをどの順番で保存するか意識する必要があります。Secrets Managerではキー名にnamepasswordなどを設定しておけば順番問わず区別できるので、ちょっと楽ができそうです。

違い2: 認証情報の自動更新ができる

公式ドキュメントに詳しく書いていますが、標準でAmazon RDS、Amazon DocumentDB、Amazon Redshiftの認証情報の更新をサポートしています。こまめにパスワードを変更することはセキュリティ上有効な手段ですが、手動で更新することは結構面倒なので(大きなアプリケーションになると実質不可能だと思います)、これを自動で行ってくれるのはありがたいですね。パラメータストアにはこの機能はなく、LambdaなどからAPIを使って更新する必要があります。

違い3: 料金

パラメータストアは基本的に無料(パラメータ数の上限あり)ですがSecrets Managerは保存とAPIコールに料金がかかります。料金表はこちら。

実際に作ってみた

マネジメントコンソールからポチポチで作ってみました。今回は簡単のため「その他のシークレットのタイプ」で作成します。AWS外部のサービスのシークレット情報はこちらの方法で保存できます。

マネジメントコンソールから「その他のシークレットのタイプ」を選択

シークレットの中身になる、キーと値のペアを作成します。プレーンテキストではJSON形式で複数のペアを一気に作成できるので、コピペで作る場合はこちらを選ぶと良いです。

キーと値のペアを作成

シークレットの名前を設定します。どういった名前でも良いのですが、パラメータストアに倣いスラッシュ区切りでつけておくのがわかりやすいかもしれません(個人的な感想です)。

シークレット名を設定

「その他のシークレットのタイプ」でもLambdaを使って自動更新の設定をすることができます。今回は単に作るだけなので、割愛させていただきます。

シークレットの自動更新を設定する

あとはタグなどの任意の情報を設定して完了です! 作成したシークレットは以下のように確認できます。

シークレットを確認する

おわりに

いかがだったでしょうか?個人的にはRDSの認証情報の自動更新が魅力的でした。パラメータストアと状況に応じて使い分けたいところですね。


それでは明日以降のアドベントカレンダーも引き続きお楽しみください!

https://qiita.com/advent-calendar/2021/excite-hd

弊社採用情報はこちらからどうぞ!

https://www.wantedly.com/companies/excite/projects

DOM操作をするjQueryコードをJestでテストする方法を勉強した話

f:id:e125731:20211206173432p:plain

エキサイトホールディングス Advent Calendar 2021の10日目は、 エキサイト株式会社 エンジニアのあはれんがお送りします。

DOM操作をするjQueryコードをJestでテストするために、勉強としてJestのサンプルコードを動かしてみました。

その際に学んだ点、サンプルコードの実行方法を共有したいと思います。

Jestについて

Jestは、JavaScriptのコードの正しさを保証するために設計されたJavaScriptテスティングフレームワークです。 JavaScript単体で動作するのはもちろん、html要素を操作するものもテストすることができます。

jestjs.io

Jestの導入

Jestを導入するにはnpmまたはyarnが必要ですので、準備をお願いします。

npmの場合は以下のように実行してください。

npm install --save-dev jest

これだけで導入は完了です!簡単ですよね。

詳しい導入方法は、公式ドキュメントにありますのでそちらを参考ください。

jestjs.io

サンプルコードを動かしてみる

jestjs.io

上記のページで、JestでDOM操作する方法を説明しています。

また、サンプルコードを公開していますので、 サンプルコードをダウンロードして、実際に動作を確認することができます。

// 1. プロジェクトをダウンロードする
$ git clone git@github.com:facebook/jest.git
// 2. jQueryのサンプルコードがあるディレクトリに移動する
$ cd examples/jquery/
// 3. Jestを導入
$ npm install --save-dev jest
// 4. テストを実施
$ npm test

> example-jquery@0.0.0 test
> jest

 PASS  __tests__/display_user.test.js
 PASS  __tests__/fetch_current_user.test.js

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.579 s
Ran all test suites.

では、サンプルコードを読んでみましょう。

f:id:e125731:20211204231902p:plain
(引用:https://jestjs.io/ja/docs/tutorial-jquery )

今回読むサンプルコードは、displayUser.jsをテスト対象としたコードです。

テストには、準備、実行、評価の3段階があります。

準備の段階では、 テスト対象のDOMとJavaScriptファイル(displayUser.js)を用意しています。 また、今回はテスト対象が別のJavaScriptファイル(fetchCurrentUser.js)を利用していたのでモックしています。 fetchCurrentUser.jsをモックすることでdisplayUser.jsの振る舞いのみをテストすることができます。

実行の段階では、jQueryを利用してユーザーが行うであろうクリック処理を実現させ、テストを実行しています。

テスト結果の評価では、モックでセットしたデータのJohnny Cash - Logged Inが表示されることを確認しています。

このようにして、DOM操作をするコードのテストを行うことができます。

サンプルコードから一歩進んでみる

サンプルコードの説明だけでは味気ないので、テストケースを1つ追加してみたいと思います。

fetchCurrentUser.jsから返ってくるデータのloggedInがfalseならば、Johnny Cash - Logged Outと表示されるか確認するテストケースを追加します。

f:id:e125731:20211204231950p:plain
ログアウト時の画面表示を確認するテストを追加

$ npm test

> example-jquery@0.0.0 test
> jest

 FAIL  __tests__/display_user.test.js
  ● displays a logout user after a click

    expect(received).toEqual(expected) // deep equality

    Expected: "Johnny Cash - Logged Out"
    Received: "  "

      62 |
      63 |   expect(fetchCurrentUser).toBeCalled();
    > 64 |   expect($('#username').text()).toEqual('Johnny Cash - Logged Out');
         |                                 ^
      65 | });
      66 |

      at Object.<anonymous> (__tests__/display_user.test.js:64:33)

 PASS  __tests__/fetch_current_user.test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 3 passed, 4 total
Snapshots:   0 total
Time:        1.589 s
Ran all test suites.
zsh: exit 1     npm test

テスト実行結果は失敗してしまい、文字が表示されていないという結果になりました。

これは、1つ目のテストケースでfetchCurrentUser.jsjQuery等のモジュールがすでにrequireされているので、 2つ目のテストケースでrequireされないためです。

なので、毎回テストケースを実行する際に、requireするように、jest.resetModules()を呼び出す必要があります。

f:id:e125731:20211204231808p:plain
jest.restModulesを追記

上記を追記することでテストが成功するようになりました。

$ npm test

> example-jquery@0.0.0 test
> jest

 PASS  __tests__/display_user.test.js
 PASS  __tests__/fetch_current_user.test.js

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.452 s
Ran all test suites.

最後に

Jestは、ReactやVue.js等のJavaScriptフレームワークのテストを書くためのフレームワークのイメージだったので、 DOMを操作するjQueryコードのテストもこんなに簡単に書けるとは思っていませんでした。

今後、jQueryコードを書く際は、Jestを使ってテストを書いていきたいと思います。

エキサイトホールディングスのアドベントカレンダーはまだまだ続きます。

明日の執筆担当は@Suzuki-Shuheiさんです。 引き続きお楽しみください。

採用情報はこちら↓

https://www.wantedly.com/companies/excite

quarkusを使う(mybatis編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 mybatisを入れていきます。 mybatisにはserviceと同様@Singletonをつけて、@Injectで呼び出します。 その際、repositoryを間に挟みます。

まず、必要なextentionをgradleに追加します。

implementation 'io.quarkiverse.mybatis:quarkus-mybatis:0.0.10'
implementation 'io.quarkus:quarkus-jdbc-mysql:0.26.1'

続いてデータベースの設定です。

CREATE schema test;

CREATE TABLE book (
    id integer not null primary key,
    title varchar(80) not null,
    author varchar(80) not null,
    created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO `test`.`book` (`id`, `title`, `author`) VALUES (1, 'hobby', 's-nakao');

続いてアプリケーションの設定です、

  • application.properties
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=YYYYYYY
quarkus.datasource.password=XXXXXXX
quarkus.datasource.jdbc.url=jdbc:mysql://localhost/test

※サンプルです。

  • repository interface
package org.my.hobby.repository;

import org.my.hobby.core.Book;

public interface BookRepository {
    Book find(String title);
}
  • repository impl
package org.my.hobby.repository;

import javax.inject.Inject;
import javax.inject.Singleton;

import java.util.Optional;

import org.my.hobby.core.Book;
import org.my.hobby.persistence.BookDto;
import org.my.hobby.persistence.BookMapper;

@Singleton
public class BookRepositoryImpl implements BookRepository {

    @Inject
    BookMapper bookMapper;

    @Override
    public Book find(String title) {
        final Optional<BookDto> book = bookMapper.getBook(title);
        return book
                .map(bookDto -> new Book(bookDto.getTitle(), bookDto.getAuthor()))
                .orElse(new Book("", ""));
    }
}
  • persistence interface

テキストブロック使います。 簡単なselectでも私はsql書きたい人です。 persistenceに全てのSQLをできるだけ書きたいです。

package org.my.hobby.persistence;

import java.util.Optional;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface BookMapper {
    @Select("""
        SELECT
            id,
            title,
            author,
            created
        FROM
            book
        WHERE
            title = #{title}
    """)
    Optional<BookDto> getBook(String title);
}
  • persistence dto
package org.my.hobby.persistence;

import java.time.LocalDateTime;

public class BookDto{
    private Integer id;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public LocalDateTime getCreated() {
        return created;
    }

    public void setCreated(LocalDateTime created) {
        this.created = created;
    }

    private String title;
    private String author;
    private LocalDateTime created;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
  • service impl

シンプルになります。

package org.my.hobby.service;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.my.hobby.core.Book;
import org.my.hobby.repository.BookRepository;

@Singleton
public class BookServiceImpl implements BookService {

    @Inject
    BookRepository bookRepository;

    @Override
    public Book find(String title) {
        return bookRepository.find(title);
    }
}
  • コントローラー

前回のコントローラーは変わりません。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

import org.my.hobby.core.Book;
import org.my.hobby.service.BookService;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    @Inject
    BookService bookService;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        final Book book = bookService.find(bookSearchRequest.title());
        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book(book.title(), book.author());
        return new BookSearchResponse(book.isFind(), book.message(), hobbyBook);
    }
}
  • curlでアクセス
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'

{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'

{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%

取得できました!よかったですね!

一通り、controller,servie,repositoryができました。簡単でしたね。

quarkusでmybatisの設定は以下を参考にしてください。

Quarkus - Using MyBatis :: Quarkiverse Documentation

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

Spring BootでCacheableが効かなかった

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣です。

エキサイトホールディングス Advent Calendar 2021の9日目の記事です。 qiita.com

タイトルのとおりですが、Spring BootでCacheableが効かなかったことがありました。 これについて調査したことと、原因について紹介します。

調査

下記に簡単なメソッドを用意しました。 getItem1メソッドはItemServiceにあるgetItemから呼ばれています。 このときに、下記サービスを呼び出してItemをキャッシュすることを試みます。

// インタフェース
public interface ItemService {
    Item getItem(String name);
}

// 実装
@Service
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
    @Override
    public Item getItem(String name) {
        return this.getItem1(name);
    }

    @Cacheable(cacheNames = "item", key = "#name")
    public Item getItem1(String name) {
        return new Item("1", "item1");
    }
}

結果

getItem1メソッドに@Cacheableを付与したときに、Itemがキャッシュされないことを確認しました。 getItem1メソッドから、getItemメソッドにアノテーションを移動するとキャッシュされます。

原因

これは、Cacheableそのものの問題ではないことがわかりました。 CacheableはSpring AOPを採用した実装であるため、ItemServiceImplの中で自身のメソッド呼びだしていることがキャッシュが効かないことの原因でした。 インタフェースを実装したメソッドに対して素直に@Cacheableを付与したほうがよさそうです。

おわりに

本記事では、Cacheableがうまく動かなかったときの原因について紹介しました。 実行時にエラーが出ずになかなか解決できなかったので、かなり詰まってしまいました。 最後まで読んでいただき、ありがとうございました!

quarkusを使う(inject編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 serviceを入れていきます。 serviceには@Singletonをつけて、@Injectで呼び出します。 ※サンプルです。

  • interface
package org.my.hobby.service;

import org.my.hobby.core.Book;

public interface BookService {
    Book find(String title);
}
  • impl
package org.my.hobby.service;

import javax.inject.Singleton;

import org.my.hobby.core.Book;

@Singleton
public class BookServiceImpl implements BookService {
    @Override
    public Book find(String title) {
        if (!"hobby".equals(title)) {
            return new Book("", "");
        }

        return new Book("hobby", "s-nakao");
    }
}
  • model
package org.my.hobby.core;

import io.netty.util.internal.StringUtil;

public record Book(String title, String author) {
    public boolean isFind(){
        return !StringUtil.isNullOrEmpty(title);
    }

    public String message(){
        if(isFind()){
            return "";
        };
        return "not found book";
    }
}
  • コントローラー

前回のコントローラーをちょっと改良します。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

import org.my.hobby.core.Book;
import org.my.hobby.service.BookService;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    @Inject
    BookService bookService;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        final Book book = bookService.find(bookSearchRequest.title());
        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book(book.title(), book.author());
        return new BookSearchResponse(book.isFind(), book.message(), hobbyBook);
    }
}
  • curlでアクセス
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'

{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'

{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%

取得できました!よかったですね!

次回、repositoryを作成します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

SpringBoot x Spring Shell で簡易バッチ処理を作る

エキサイト株式会社エンジニアの佐々木です。2021年エキサイトホーディングスアドベントカレンダー7日目を担当させていただきます。

以前、SpringBootで対話的インターフェースをSpring Shellで実装するで対話的なアプリケーションについては記載しており、今回は簡易的なバッチ処理の方を記載します。

はじめに

前回の対話的なアプリケーションのコードを再掲します。(一部修正していますが、メソッド名部分のみになります。)

@ShellComponent(value = "demo")
public class DemoShellController {

    @ShellMethod(value = "足し算をする", key = "addCalc" , group = "calc")
    public Integer add(int a, @Max(10) int b, @ShellOption(value = "--optional_c" , defaultValue = "0") int c){
        return a + b + c;
    }
}

こちらになりますが、引数を渡されたものを足し算する簡単なものになります。前回のままだと対話的なアプリケーションになってしまい、Cron等で動作させられないので、これを改造していきます。

SpringShell内の実装方法

SpringShellではどうやって対話的アプリケーションにしているかの実装をみていきます。

InteractiveShellApplicationRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE)
public class InteractiveShellApplicationRunner implements ApplicationRunner {

        // 省略

    @Override
    public void run(ApplicationArguments args) throws Exception {
        boolean interactive = isEnabled();
        if (interactive) {
            InputProvider inputProvider = new JLineInputProvider(lineReader, promptProvider);
            shell.run(inputProvider);
        }
    }

        // 省略
}

ApplicationRunnerのインターフェースを用いて実装されているのがわかります。@Orderを用いて、実行順序を制御しています。簡易バッチの方の処理は、ApplicationRunnerインタフェースを用いて、且つ@Orderで対話型アプリケーションよりも早く動かないといけません。

簡易バッチの実装

下記のような実装コードになります。

NonInteractiveRunner.java

@Component
@Order(InteractiveShellApplicationRunner.PRECEDENCE - 1)    //  ① InteractiveShellApplicationRunnerより1つ優先順位を上げる
public class NonInteractiveRunner implements ApplicationRunner {

    private Shell shell;
    private ConfigurableEnvironment configurableEnvironment;

    public NonInteractiveRunner(Shell shell, ConfigurableEnvironment configurableEnvironment) {
        this.shell = shell;
        this.configurableEnvironment = configurableEnvironment;
    }

    @Override
    public void run(ApplicationArguments args) throws Exception {
        if (args.getNonOptionArgs().isEmpty() || args.getNonOptionArgs().stream().anyMatch(s -> s.startsWith("@"))) {
            // ②ここに入った場合は、通常の対話型アプリケーションを実行する
            return;
        }

        InteractiveShellApplicationRunner.disable((configurableEnvironment));  // ③ ここで対話型アプリケーションを無効にする
        final Object evaluate = shell.evaluate(() -> String.join(" ", args.getSourceArgs()));  // ④ここで簡易バッチアプリケーションを実行する
        if (evaluate != null) {
            System.out.println(evaluate);    // ⑤出力するものがあればここで出力する
        }

    }
}

対話型アプリケーションの機能も残したい為、引数がなかったら or 引数に@があれば対話型アプリケーションの動きをするようにしています。(※対話型アプリケーションでは@があると、ファイルに記載されているコマンドを対話型アプリケーションの中で展開する仕様になっています。)

それ以外の場合は、対話型アプリケーションを無効にし、渡された引数でメソッド等を呼び出しバッチとして処理するようなものにします。

 $ java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

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

2021-12-05 17:46:45.958  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 76623 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-05 17:46:45.963  INFO 76623 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-05 17:46:46.911  INFO 76623 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.416 seconds (JVM running for 6.881)

7

答えは7になっています。 これで、SpringShellで簡易的なバッチ処理を作ることができました。対話型アプリケーションもコマンドアプリケーションもできるので、お得な実装になっているかと思います。

おまけ

上記のログにでている、起動時のSpringBootのバナーが邪魔ですね。これはメインメソッド内で消せるのでその実装を行います。

BatchApplication.java

@SpringBootApplication
public class BatchApplication {

    public static void main(String[] args) {
        new SpringApplicationBuilder(BatchApplication.class)
                .bannerMode(Banner.Mode.OFF)  // ここでバナーをオフにする
                .run(args);
    }
}

上記のように、bannerMode()OFFにすれば表示されません。これで邪魔ではなくなりました。 下記のように、起動してもバナーがでないようになります。下記が実行結果です。

$ ./gradlew assemble && java -jar build/libs/batch-0.0.1-SNAPSHOT.jar addCalc 1 2 4

2021-12-06 09:33:25.406  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Starting BatchApplication using Java 11.0.13 on 61-17616.local with PID 85570 (/Users/kohei.sasaki/git/sample/batch/build/libs/batch-0.0.1-SNAPSHOT.jar started by kohei.sasaki in /Users/kohei.sasaki/git/sample/batch)
2021-12-06 09:33:25.411  INFO 85570 --- [           main] com.example.batch.BatchApplication       : No active profile set, falling back to default profiles: default
2021-12-06 09:33:26.228  INFO 85570 --- [           main] com.example.batch.BatchApplication       : Started BatchApplication in 6.197 seconds (JVM running for 6.6)
7

まとめ

重要データに関しては、SpringBatch等をつかった方が、動作履歴やレジューム(途中から再開)機能があるのでそちらを使ったほうがいいですが、ちょっとしたバッチ処理であれば使ってみるのもいいと思います。対話型のアプリケーションと簡易型のコマンドラインアプリケーションがついてくるので、お得かもしれません。

最後に

2021年エキサイトホーディングスアドベントカレンダー7日目でした。引き続きエキサイトホーディングスのアドベントカレンダーをお楽しみいただけると幸いです。 qiita.com

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

カジュアル面談はこちらになります! meety.net

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

【タイポグラフィ・ポスター製作に】簡単計算!Illustratorでグリッドを作る方法

f:id:excite_ny:20211201131504p:plain

はじめに

エキサイト21卒デザイナーの山﨑です😊

こちらはエキサイトホールディングス Advent Calendar 2021の6日目の記事になります!

qiita.com

今回はIllustratorでのポスターデザインに必須なグリッドシステム(グリッドレイアウト)を解説していきたいと思います。

グリッドシステム・制作方法について

はじめに、グリッドシステムとはどんな物なのか説明します。

グリッドシステムとは?

色々意味はありますが、画面やページを格子状に分割して、規則的な直線を基準にデザイン要素を配置していくデザイン手法です。

ポスターをデザインする前にグリッドレイアウトを作っておくと、デザインが圧倒的に作りやすくなります。

今回エキサイト社内に掲示するバリューポスターをグリッドレイアウトを活用してデザイン制作したので、例として出してみます。

f:id:excite_ny:20211204234145p:plain

右がグリッドレイアウトです。このグリッドがある程度のデザインの配置の目安になっています。

一見適当な正方形を並べて配置しただけに見えますが、実は計算して配置しています。

では、早速解説して行きましょう。

①フォントサイズ・行送り・行間・グリッドに入れる行を決める

まずはこの四つの数字をきめましょう。

製作する文字のフォントサイズを元に決めると作りやすいです。

正直数字はなんでも良いのですが、あまりに数字が大きすぎるとグリッドの正方形が大きくなって組みづらいグリッドレイアウトになりがちなので、注意が必要です。

ひとまず今回は【フォントサイズ21pt、行送り25pt・行間4pt・グリッドに入れる行数は3行】に設定しました。 (グリッドに入れる行は5行・4行・3行から選ぶのが基本的です)

②その数字をもとに計算してグリッドサイズ・グリッド間を割り出す

そうすると、ざっくりこんな感じの図になります。

f:id:excite_ny:20211205005126j:plain

グリッドの正方形の中に3行文が入るとなると、

21pt+4pt+21pt+4pt+21pt=71pt (行送り25pt+行送り25pt+フォント21pt=71ptでも可)

f:id:excite_ny:20211206102305p:plain

3行目の行間4ptがはみ出る形になるので、グリッド間のサイズは

4pt+21pt+4pt=29pt

f:id:excite_ny:20211206103205p:plain

というわけで、正方形のサイズは71pt・正方形と正方形の間は29ptになりました。

③グリッドを作ってみる

f:id:excite_ny:20211205010752p:plain

71ptの正方形を一つ作ります。

f:id:excite_ny:20211205010812p:plain

環境設定>一般>キー入力で「71pt(グリッドサイズ)+29pt(グリッド間)」を入力してください。

このときに数字だけではなくpt (ポイント)という単位も入れましょう。

f:id:excite_ny:20211205010933p:plain

最初の正方形をクリックしてaltを押しながら→を押すと間隔が29ptに保たれた正方形が量産されます。

f:id:excite_ny:20211205011044p:plain

→を押し続けるとすぐに横一列のグリッドが完成しました。次はこの横一列のグリッドを全て選択して、今度はaltを押しながら↓を押します。

f:id:excite_ny:20211205011220p:plain

グリッドが完成しました。

このグリッドをアートボードの中心に合わせます。

この正方形を全て選択し、Command+5を推してグリッド化させれば完成です。

f:id:excite_ny:20211205011311p:plain

終わりに

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

慣れるまで少し複雑なのですが、仕組みを覚えれば意外と簡単に組めると思います😊

グリッドに合わせすぎるとデザインが窮屈な印象になってしまうので、ご注意ください。

グリッドレイアウトはあくまで目安なので、グリッドから外れてしまっても気にせずデザインするのが良いと思いました😊

それでは、ありがとうございました!

quarkusを使う(バリデーション編)

こんばんは、エキサイト株式会社の中尾です。

前回の続きです。 必要そうなツールを入れていきます。 まず、バリデーションを追加します。

  • gradleに以下を追加
implementation 'io.quarkus:quarkus-hibernate-validator'

java17にしているのでもちろんRecordを使います。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    /**
     * 書籍をタイトルで検索
     *
     * @param bookSearchRequest
     * @return
     */
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        if (!"hobby".equals(bookSearchRequest.title())) {
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, "not found book", hobbyBook);
        }

        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("hobby", "s-nakao");
        return new BookSearchResponse(true, "", hobbyBook);
    }
}
  • postmanでアクセス
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'
{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'
{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost: $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'
{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%
shogo.nakao@localhost: $

ちゃんとバリデーションできていますね!嬉しい嬉しい!

springbootと違ってbindingResultは使えないですが、、、(もしかしたら使える方法があるかも?今後調べます)

次回、さらに必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

JavaとLombokの@Withでイミュータブルなデータ型を生成する

エキサイト株式会社エンジニアの佐々木です。2021年アドベントカレンダー5日目を担当します。 サービスを開発している上で、データをなるべくイミュータブルにするというのは、バグを作り込まない上で重要だと思います。Java15でRecord型が導入されていますが、まだ未対応なライブラリがあると思います。今回はLombokでは@Withを使って簡単に実装できます。

ミュータブルなデータ型

ミュータブルなデータ型は、Lombokでは@Dataを使って簡単にできます。

@Data
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form();
form.setId(1L);
form.setName("sample");

form.setId(2L);   // form.id が 2にかわる
form.setName("sample2"); // form.name が 2に変わってしまう

コード見れば明らかですが、途中でオブジェクトの状態が変わっています。メソッドの引数で渡して、再代入等を特にしていない場合でも変わってしまうので、バグの1つの原因になります。

イミュータブルなデータ型(@Value)

@Value
class Form {
    private final Long id;
    private final String name;
}

使用時:
Form form = new Form(1L, "sample");

form.setId(2L);  // メソッドが存在しないので、エラーになる
form.setName("sample2"); // メソッドが存在しないので、エラーになる

Form form2 = new Form(2L, "sample2"); // 代入 or 再代入をする

コンストラクタを使用したときだけ値を設定できるようにすると、1度定義したオブジェクトの状態は途中で変えることができません。途中で変えるには、再度コンストラクタ呼び出しを行い、再代入等が必要です。しかし、引数が少ないときはいいですが、多くなってくると辛くなってきます。そこで@Withが登場します。

イミュータブルなデータ型(@With)

@With
@Value
class Form {
    private final Long id;
    private final String name;
}

使用時
Form form = new Form(1L, "sample");
form.withId(2L);
form.withName("sample");

log.info("{}", form);  // Form(id=1, name=sample);

Form form2 = form.withId(2L).withName("sample2");
log.info("{}", form);  // Form(id=2, name=sample2);   

withId(), withName() を使用して値変更しても、formオブジェクトの値は変更されていません。setterとは違い、新規オブジェクトが返ってきます。では、@Withがどういう処理をしているのかをdelombokをして見ていきます。

public Form withId(Long id) {
    return this.id == id ? this : new Form(id, this.name);
}

public Form withName(String name) {
    return this.name == name ? this : new Form(this.id, name);
}

このようなコードが裏側で実行されています。値が同じ時は自分の参照を返し、値が変更されている場合は、新規のオブジェクトを生成して返しています。これでイミュータブルなオブジェクトになって安全です。

まとめ

Java15でRecord型が導入され、イミュータブルなオブジェクト専用の型が追加されています。状態が変更される副作用を抑えるべく導入されているかと思います。周辺ライブラリが対応するまではLombokを駆使して、ミュータブル、イミュータブルなオブジェクト生成の使い分けをする感じになるかと思います。

最後に

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 Calendar for エキサイトホールディングス Advent Calendar 2021 | Advent Calendar 2021 - Qiita

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

カジュアル面談はmeetyを公開していますので、よろしくお願いいたします! meety.net

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

quarkusを使う(環境構築編)

初めまして、Red Hat大好きエキサイト株式会社の中尾です。

趣味でquarkusを使っていこうと思うので、その記録を残そうと思います。

quarkus cliは使いません。

まずは環境作りです。

quarkusでbuildツールgradleを使おうと思います。 mvnよりもgradleのほうが柔軟なので、、、(宗教戦争ありそうですが。)

  • mvnインストール

インストールするんかい!!って疑問あるかもしれないですが、quarkus自体を入れる最初の儀式です。

brewで入れてください。

brew install maven
  • 初期構築

mvn使って構築します。 buildToolにgradleを指定すると、gradle使えます。

mvn io.quarkus.platform:quarkus-maven-plugin:2.5.1.Final:create \
    -DprojectGroupId=quarkus \
    -DprojectArtifactId=hobby \
    -DprojectVersion=v1.0.0 \
    -DclassName="org.my.hobby.Resource" \
    -Dextensions="resteasy,resteasy-jackson" \
    -DbuildTool=gradle
  • 起動
./gradlew --console=plain quarkusDev

開きました!嬉しい嬉しい!

http://localhost:8080/

http://localhost:8080/hello

次回、必要そうなツールを入れるに移動します。

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。
www.wantedly.com

Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com