LocalDateTime型をrequestから受け取るカスタムアノテーションを作る

エキサイトのしばたにえんです。 早速ですが カスタムアノテーションの作成をしていきます。 リクエストからLocalDateTimeを受け取る時に@JsonFormatを使って受け取ると思いますが、この時にpatternを書く必要があります。

@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
private LocalDateTime date;

こんな感じです

pattern = "yyyy-MM-dd'T'HH:mm:ss" 毎回これを書いていくのはめんどうなのと patternが間違ったりでミスをする可能性が出てきます。

そんな時にはカスタムアノテーションを作ると便利かもしれません。 簡単です。

@JacksonAnnotationsInside
@Retention(RetentionPolicy.RUNTIME)
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
public @interface LocalDateTimeRequest {
}

としてアノテーションを登録して、

@LocalDateTimeRequest
private LocaldateTime date;

とすれば、毎回patternを書く必要がなくなります。 便利なので是非使ってみてください

SpringBootで設定ファイル(application.yaml)を一括で読み込む

エキサイト株式会社エンジニアの佐々木です。SpringBootでの設定ファイル(application.yaml)を一括で読む方法のメモになります。

前提

SpringBoot2.4以上で検証しています。

起動クラスの設定

ソースコードは下記になります。

起動クラスに@ConfigurationPropertiesScanを付与します。これで、@ConfigurationPropertiesアノテーションを読みにいきます。

@SpringBootApplication
@ConfigurationPropertiesScan  // ConfigurationPropertiesを読みにいくおまじない
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

単一項目な設定ファイル

設定ファイルの項目名と変数名が一致していればバインドされます。

設定ファイル(appliation.yaml)

設定ファイルは下記になります。

env:
  host: local
  port: 8080

読み取りクラス

読み取りクラスは下記のように定義します。

@Getter
@RequiredArgsConstructor
@ConstructorBinding          // この設定がないと Setterが必要になるのでつけておく
@ConfigurationProperties(prefix = "env")   // prefixで指定された部分のyamlを読みいく
public class Config {
    private final String host;
    private final Integer port;
}

@ConfigurationPropertiesアノテーションapplication.yamlのどこを読むかを設定します。@ConstructorBindingアノテーションで、setterがなくても値がセットされるようにします(つけておかないとSetterメソッドが必要になります)。@Getter@RequiredArgsConstructorLombokアノテーションです。Getterの生成とコンストラクタの定義を省略できます。yamlの中のプロパティ名とクラスのプロパティ名を合わせると、バインドしてくれます。型は気にしなくても、StringやIntegerくらいなら対応してくれます。設定されたものは、DIをすれば、使えます。

出力

{
"host": "local",
"port": 8080
}

Key-Value型の読み取り

Key-Value形式の設定は、Map型を使いKey-Valueをオブジェクトにバインドします。

設定ファイル

regionの中にjausのようなkey-valueのファイルがあるとします。

env:
  host: local
  port: 8080
  region:
    ja: 日本
    us: アメリカ

読み取りクラス

読み取りでは、Map型を使いJavaオブジェクトに変換していきます。

@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "env")
public class Config {
    private final String host;
    private final Integer port;
    private final Map<String,String> region; // Map型で定義(ジェネリクスは相応の型を指定)
}

上記で、region変数にKey-Value型のデータがバインドされます。

出力

{
"host": "local",
"port": 8080,
"region": {
    "ja": "日本",
    "us": "アメリカ"
    }
}

プロパティがネストされたapplication.yaml

ネストされた設定ファイルもバインド可能です。項目名と変数名が一致していれば自動的にバインドしてくれます。

設定ファイル

ネストされた設定ファイルです。

env:
  host: local
  port: 8080
  people:
    - name: hogehoge
      age: 18
    - name: fugafuga
      age: 30

読み取りクラス

階層がある場合は、staticな内部クラスを書くか、外部クラスに定義して型を指定するかになります。変数名が一致すればクラスに合わせて読み込みまれます。

@Getter
@RequiredArgsConstructor
@ConstructorBinding
@ConfigurationProperties(prefix = "env")
public static class Config {
    private final String host;
    private final Integer port;
    private final List<People> people;

    @Getter
    @RequiredArgsConstructor
    static class People {
        private String name;
        private Integer age;
    }
}

staticな内部クラスでも@Getter、@RequiredArgsConstructorアノテーションは必要になります。

出力

{
  "host": "local",
  "port": 8080,
  "region": {
    "ja": "日本",
    "us": "アメリカ"
  },
  "people": [
    {
      "name": "hogehoge",
      "age": 18
    },
    {
      "name": "fugafuga",
      "age": 30
    }
  ]
}

まとめ

@Valueで1つずつ読み込む方法もありますが、記述が多くなるので、クラス定義だけでできる方法を記載します。Yamlとの脳内変換が多くなってくるので、ネストの深さがでてくると、冗長でも@Valueの方が楽な場合もあるかもしれません。適材適所でお使いください。

最後に

エキサイト株式会社では、新卒・中途(大卒・高専卒・高卒問わず)バックエンドエンジニア・アプリエンジニア・フロントエンジニア・クリエイティブを募集しております。興味がありましたらご連絡をお待ちしております。

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

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

Javaで1日の始まりと終わりの時刻を簡単に取得する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 既存サービスのリビルドをするにあたり、日付まわりの処理を記述することが多くなってきました。 日付まわりの処理を誤ると、未公開のデータを取得できてしまうこともあるため慎重にコーディングしたいです。 そこで得た知見として、Javaで1日の始まりと終わりの時刻を取得する簡単な方法について紹介します。

環境

LocalTime.MIN / LocalTime.MAX を使う

LocalTimeのクラス変数であるMIN / MAXを使用することで、1日の始まりと終わりの時刻を取得することができます。 これを使用することで、自分で0時0分0秒を作成したり、23時59分59秒のLocalTimeを作成する必要がなくなるため、積極的に使っていきたいです。

public LocalDateTime getStartDate(LocalDateTime date) {
    return LocalDateTime.of(
            date.toLocalDate(),
            LocalTime.MIN
    );
}

public LocalDateTime getEndDate(LocalDateTime date) {
    return LocalDateTime.of(
            date.toLocalDate(),
            LocalTime.MAX
    );
}

参考

docs.oracle.com

SpringBootでスネークケースのリクエストパラメータを受け取る方法

エキサイト株式会社エンジニアの佐々木です。古いシステムをSpringBootリビルドしており、リクエストパラメータの命名が統一されていないというのがあったので、対応方法の一例を紹介します。

@RequestParamで解決する

@RequestParamname属性で指定できます。

@GetMapping
public String index(@RequestParam(name = "last_name") String lastName){
    return lastName;
}

curl "http://localhost:8080?last_name=buzz"
"buzz"

この方法は、パラメータが少ないときはいいのですが、多くなってくるとこのメソッドが辛くなっていきます。次はオブジェクトでの受け取り方法です。

変数名を変更する

オブジェクトで受け渡すときは、変数名を変更すると対応できます。last_nameの変数名を

@RestController
@RequestMapping
public class DemoController {

    @RequestMapping
    public String index(Form form){
        return "";
    }

    @Data
    static class Form {

        private String last_name;

    }
}

curl "http://localhost:8080?last_name=buzz"
{
  "last_name": "buzz"
}

これだと、レスポンスされるデータや内部で使うデータもスネークケースになってしまいます。Javaは、キャメルケースが通常なので、変換が面倒になります。

@ConstructorPropertiesで整える

スネークケースが必要な箇所だけ、@ConstructorPropertiesを付与します。

@RestController
@RequestMapping
public class DemoController {

    @RequestMapping
    public Form index(@Valid Form form){
        return "";
    }

    @Data
    static class Form {

        private String firstName;
        @NotEmpty
        private String lastName;
        private String phoneNumber;

        @ConstructorProperties({"last_name","phone_number"})
        public Form(String lastName, String phoneNumber){
            this.lastName = lastName;
            this.phoneNumber = phoneNumber;
        }
    }
}


// バリデーションも効きます
curl "http://localhost:8080?firstName=fizz&last_name=buzz&phone_number=123"
{
  "lastName": "buzz",
  "phoneNumber": "123",
  "firstName": "fizz"
}

curl "http://localhost:8080?firstName&phone_number=123"
// Validation Error.

検索とかは受け取るパラメータが多かったりするんで、中にはスネークケースのものがあったりします。1つだけ例外があった場合に、すべてのパラメータをコンストラクターで定義しないといけないのは辛いので、必要な箇所だけでいいようにします。ここらへんは賢いと思います。

最後に

パラメータの命名等は、コンパイル等では弾けず、linterでも見落とす部分があるので、最終APIでカバーすることになると思いますが、この程度でよければ解決できそうなので、ぜひ利用してみてください。

エキサイトではバックエンドエンジニア、アプリエンジニア、フロントエンジニア、UI/UXデザイナーを積極採用中です。ぜひご連絡ください。

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

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

AWS上でElasticSearch7.10ドメインをOpenSearch1.0ドメインに移行した話

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

今回ElasticSearchドメインの移行を行い、さらにElasticSearch7.10からOpenSearch1.0にバージョンを上げました。

Amzon OpenSearch Serviceとは

Amzon OpenSearchとは、Amazon Elasticsearch Service の後継サービスであり、Elasticsearch 7.10.2から派生したサービスです。

ElasticSearchとは互換性があり、基本的にバージョンアップ自体は容易に行えます。

※ElasticSearchのRestHighLevelClientからのアクセスができなくなり、OpenSearchのClientを使うなどの変更が必要なケースもあります。

ドメイン移行

今回、移行元のドメインのスナップショット(バックアップ)を新しいドメインにリストアして移行を行います。

OpenSearch(ElasticSearch)には、自動でバックアップを作成してくれる機能が標準で備わっていますが、ElasitcSearchの自動スナップショットは他のドメインにリストアをすることはできない為、移行元ドメインの手動スナップショットを作成する必要があります。

参考 docs.aws.amazon.com

スナップショット保存用S3バケット作成

S3コンソールで手動スナップショット保存用のS3バケットを作成 f:id:excite-at-ma:20211001180352p:plain

作成したS3バケットを開きプロパティから以下のようなAmazon リソースネーム (ARN)をメモしておきます。

arn:aws:s3:::s3-dev-opensearch-repo

ポリシー作成

IAM>ポリシー>ポリシーの作成 から先ほど作成したS3のポリシーを作成します。

ポリシー名例 s3-dev-opensearch-repo-policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "s3:ListBucket"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::s3-dev-opensearch-repo"
            ]
        },
        {
            "Action": [
                "s3:*"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::s3-dev-opensearch-repo/*"
            ]
        }
    ]
}

Resourceには先ほどコピーしたS3のARNを記述してください。

ロール作成

ロール名例:dev-opensearch-repo-role

ロールの作成 から、先程作成したポリシー(今回の場合はs3-dev-opensearch-repo-policy)をアタッチします。 f:id:excite-at-ma:20211001181642p:plain

その後、作成したロールのARNをメモしておきます。

OpenSearch(ElasticSearch)用ポリシーを作成

ポリシー名例:dev-opensearch-policy

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "上記で作成したロールのARN"
    },
    {
      "Effect": "Allow",
      "Action": "es:ESHttpPut",
      "Resource": "移行前ElasticSearchドメインのARN/*"
    },
    {
      "Effect": "Allow",
      "Action": "es:ESHttpPut",
      "Resource": "移行後ElasticSearchドメインのARN/*"
    }
  ]
}

その後、再度 dev-opensearch-repo-role に上記のポリシーをアタッチします。

ロールに信頼関係を追加

dev-opensearch-repo-roleの[信頼関係]タブの[信頼関係の編集]ボタンから、下記の信頼関係設定をアタッチします。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "",
    "Effect": "Allow",
    "Principal": {
      "Service": "es.amazonaws.com"
    },
    "Action": "sts:AssumeRole"
  }]
  
}

リポジトリ登録

移行元、移行先ドメインそれぞれに対して下記のpythonコードでリポジトリ登録を行います。

import boto3
import requests
from requests_aws4auth import AWS4Auth

host = '[移行元・移行先ElasticSearchドメイン]/' # 必ず末尾に/をつける!
region = 'ap-northeast-1'
service = 'es'
credentials = boto3.Session().get_credentials()
awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)

path = '_snapshot/[スナップショットを置くリポジトリの名前]' # the Elasticsearch API endpoint
url = host + path

payload = {
  "type": "s3",
  "settings": {
    "bucket": "s3-dev-opensearch-repo",
    "region": "ap-northeast-1", # for all other regions
    "role_arn": "[作成したIAMロールのARN]"
  }
}

headers = {"Content-Type": "application/json"}

r = requests.put(url, auth=awsauth, json=payload, headers=headers)

print(r.status_code)
print(r.text)

移行元のkibana,移行先のOpenSearchDashboardsでそれぞれリポジトリを確認

GET /_snapshot?pretty

結果

{
  "[スナップショットを置くリポジトリの名前]" : {
    "type" : "s3",
    "settings" : {
      "bucket" : "s3-dev-opensearch-repo",
      "region" : "ap-northeast-1",
      "role_arn" : "IAMロールのARN"
    }
  }
}

移行先ドメインからkibanaを削除

作ったばかりのドメインだと、移行元ドメインと.kibana_1が被るのでOpenSearchダッシュボード上で下記のコマンドを実行します

DELETE /.kibana_1?pretty

スナップショット登録

import boto3
import requests
from requests_aws4auth import AWS4Auth

region = 'ap-northeast-1' # e.g. us-west-1
service = 'es'
credentials = boto3.Session().get_credentials()

awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)
host = '[移行元ElasticSearchドメイン]/'

path = '_snapshot/[スナップショットを置くリポジトリの名前]/[スナップショット名]'
url = host + path

r = requests.post(url, auth=awsauth)

print(r.text)

スナップショットの確認

移行元kibanaと移行先OpenSearchDashboardsでそれぞれ下記のコマンドを実行し、スナップショットが存在しているか確認する

GET /_snapshot/[スナップショットを置くリポジトリの名前]/_all?pretty

移行先ドメインにスナップショットをリストア

import boto3
import requests
from requests_aws4auth import AWS4Auth

host = '/' # include https:// and trailing /
region = 'ap-northeast-1' # e.g. us-west-1
service = 'es'
credentials = boto3.Session().get_credentials()

awsauth = AWS4Auth(credentials.access_key, credentials.secret_key, region, service, session_token=credentials.token)
host = '[移行先OpenSearchドメイン]/'

path = '_snapshot/[スナップショットを置くリポジトリの名前]/[スナップショット名]/_restore'
url = host + path

r = requests.post(url, auth=awsauth)

print(r.text)

※ スナップショットをリストアした場合、indexのデータは全て入りますがindexテンプレートの設定まではコピーしてくれず、手動で設定し直す必要があるので注意です。

移行後ドメインに対してINDEXテンプレートを確認

GET _template/

結果

{ }

Javaで、JSONのタイムゾーン込みの日付文字列をDateTime型に変換する際の注意点

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

JSON上の日付文字列をDateTime型に変換するのはJavaではよくあると思いますが、その際の注意点について書いていきます。

JSONの日付文字列の変換方法

まずは、通常の変換方法について見ていきます。

例えば、

{
    "sampleDate": "2021-01-01T10:10:00"
}

のような形で日付が渡された場合は、

@Data
public class SampleModel {
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private LocalDateTime sampleDate;
}

とすれば変換してくれます。

タイムゾーン込みの場合

以下のようにタイムゾーン込みの場合、

{
    "sampleDate": "2021-01-01T10:10:00+09:00"
}

こうしてしまうと実は変換できません。

@Data
public class SampleModel {
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZ")
    private LocalDateTime sampleDate;
}

というのも、 Z だけだと +0900 のようにコロンを付けないタイムゾーンのフォーマットとして処理してしまうためです。

+09:00 のようにコロンをつけたい場合は、

@Data
public class SampleModel {
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ssZZZZZ")
    private LocalDateTime sampleDate;
}

このように、 ZZZZZ としましょう。

なお、今回は LocalTimeZone で受け取っていますが、これだと変換自体はできてもタイムゾーンは考慮されません。 タイムゾーン情報も使用したい場合は、 ZonedDateTime 型を使いましょう。

最後に

日付は様々なフォーマットがあるので、適切なものを選んでいく必要があります。 渡されるフォーマットに合わせることも大事ですが、渡す側でもどんなフォーマットで渡すかをきちんと考えていきましょう。

Nim言語でダイクストラ法を書いてみる

はじめに

ダイクストラ法というものをご存知でしょうか?
ざっくりいうと、目的地までの最短コースを探すときに使うアルゴリズムの一つです。

それを性懲りもなくNimで実装してみましょう。

ダイクストラ

以下のサイト様がわかりやすいかと思います。
https://products.sint.co.jp/topsic/blog/dijkstras-algorithm

ざっくりいうと、次の目的地までのコストを計算するアルゴリズムの一つです。
あれ?さっきも書いた?
コストというのは、現実世界で言う「信号」だとか「登道」だとかそういった時間がかかるものになります。

ダイクストラ法のコード

では早速コードです。

proc dijkstra*(edges: seq[seq[seq[float64]]], vertexNum: int): seq[float64] =
  # 道のりとして使う変数を初期値として大きな値をセット
  var destination: seq[float64]
  for i in 0..vertexNum - 1:
    destination.add(99999999.0)
  
  # 配列の最初はスタート地点なのでコストを0にする
  destination[0] = float64(0)

  var queue: seq[int]
  for i in 0..vertexNum - 1:
    queue.add(i)
  
  while len(queue) > 0:
    # コストが最小限の頂点(頂点)を見つける
    var r = queue[0]
    for i in queue:
      if destination[i] < destination[r]:
        r = i

    let u = queue[queue.find(r)]
    queue.delete(queue.find(r))

    for edge in edges[u]:
      if destination[int(edge[0])] > (destination[u] + edge[1]):
        destination[int(edge[0])] = destination[u] + edge[1]

    
  return destination

与えられた目的地とそのコストから、コストが最小になるように道のりを出していきます。
実際に動作させてみましょう。

動作付きのコード

proc dijkstra*(edges: seq[seq[seq[float64]]], vertexNum: int): seq[float64] =
  ##
  ## edges: 辺の終点と個数のリストを渡します
  ## vertexNum: 頂点の数を渡します
  ## 

  # 初期値として大きな値をセット
  var destination: seq[float64]
  for i in 0..vertexNum - 1:
    destination.add(99999999.0)
  
  # 配列の最初はスタート地点なのでコストを0にする
  destination[0] = float64(0)

  var queue: seq[int]
  for i in 0..vertexNum - 1:
    queue.add(i)
  
  while len(queue) > 0:
    # コストが最小限の頂点(頂点)を見つける
    var r = queue[0]
    for i in queue:
      if destination[i] < destination[r]:
        r = i       # コストが小さい頂点が見つかると更新

    let u = queue[queue.find(r)]
    queue.delete(queue.find(r))

    for edge in edges[u]:
      if destination[int(edge[0])] > (destination[u] + edge[1]):
        destination[int(edge[0])] = destination[u] + edge[1]

    
  return destination


const dijkstraTestData = @[
    @[@[1.0, 4.0], @[2.0, 3.0]],                    # 頂点A(0.0)からの辺のリスト[[頂点Bへ行ける, コストは4.0], [頂点Cへ行ける, コストは3.0]]
    @[@[2.0, 1.0], @[3.0, 1.0], @[4.0, 5.0]],       # 頂点B(1.0)からの辺のリスト[[頂点Cへ行ける, コストは1.0], [頂点Dへ行ける, コストは1.0], [頂点Eへ行ける, コストは5.0]]
    @[@[5.0, 2.0]],                                 # 頂点C(2.0)からの辺のリスト[[頂点Fへ行ける, コストは2.0]]
    @[@[4.0, 3.0]],                                 # 頂点D(3.0)からの辺のリスト[[頂点Eへ行ける, コストは3.0]]
    @[@[6.0, 2.0]],                                 # 頂点E(4.0)からの辺のリスト[[頂点Gへ行ける, コストは2.0]]
    @[@[4.0, 1.0], @[6.0, 4.0]],                    # 頂点F(5.0)からの辺のリスト[[頂点Eへ行ける, コストは1.0], [頂点Gへ行ける, コストは4.0]]
    @[]                                             # 頂点G(6.0)からの辺のリスト(ゴール)
  ]

let dijkstraResult = dijkstra(dijkstraTestData, 7)
assert dijkstraResult == @[0.0, 4.0, 3.0, 5.0, 6.0, 5.0, 8.0]

このような感じです。
assetの部分は、頂点Aから各頂点に向かったときの最小のコストとなっております。
図にすると以下のようになります。

f:id:taanatsu:20210930182002p:plain

今回だとA→C→F→E→Gと進むのが一番コストが少なそうですね!
このように利用します。

あとがき

結構昔に勉強したので思い出すのに時間がかかりました……
ヒープを使った計算式の最適化とか、ダイクストラを応用したA*(エースター)探索なんてのもありますね。
まだまだ奥が深い領域です。

DMSを使ってDBのAWS移行をする

エキサイト株式会社の武藤です。

オンプレのDBをAWSのRDSに移行する際に、AWSのDatabase Migration Service (DMS) を利用しました。 サービスの特性上読み込みが多く、書き込みが少なかったので、無停止での切り替えができました。 その手順について説明します。

前提情報として、オンプレDB はMySQL 5.5で構築されており、RDSはAurora MySQL 5.7を採用しました。

Database Migration Service とは

DBのAWS移行のために、移行元と移行先のDB間でデータ同期を行うサービスです。 データの一括移行と継続的なレプリケートが設定できるため、ダウンタイムを少なく移行が可能です。

aws.amazon.com

オンプレMySQLからAurora MySQLに移行する方針であったので、下記の記事を参考にしました。

aws.amazon.com

大まかな流れとしては、最初にオンプレDBからRDSにスキーマの変換を行います。 その後、オンプレからRDSに継続的なレプリケートを設定するため、オンプレDBのバイナリログ周りの設定を確認し、必要であれば変更します。 そして、レプリケーションを処理するインスタンスの作成や移行タスクを設定し、移行を実施します。

Schema Conversions Tool でスキーマを変換する

AWSで提供されているSchema Conversions Tool (SCT) を利用して、スキーマの変換を行いました。

docs.aws.amazon.com

SCTはWebの管理コンソールではなく、ローカルにツールをダウンロードします。

SCT上でソース、ターゲットのDBに接続をし、スキーマの変換を行います。

そのままRDSにスキーマを反映させることも可能ですが、変換内容を確認したかったので、一旦SQLファイルに保存して、差分比較を行いました。 今回のケースでは、下記のような差分がありました。

  • カラム、テーブルのCOMMENT の削除
  • MyISAMからInnoDBに変更
  • KEY に USING BTREE が付与
  • RDSの設定に合わせてCHARSETの変更(utf8 -> utfmb4)
  • text型がmediumtext型に変更

おそらくですが、DBエンジンや設計によってどのような差分が出るか異なると思われます。 動作に影響がありそうな変更がありますので、念の為確認しておくのが良いでしょう。

オンプレDBの設定確認

DMSの参考記事には下記が示されています。

AWS DMS は1回限りのデータ移行を行うことも、データを継続的にレプリケーションすることもできます。MySQLから Amazon Aurora への移行では、まずはバルクロードを行い、それからレプリケーションまたは Change Data Capture (CDC) を実行します。変更をレプリケーションするために、DMSはソースデータベースのバイナリログファイルを読む必要があるため、ソースのMySQLデータベースのバイナリログが有効になっているという前提条件を満たしているかを確認する必要があります。

今回は、サービスの特性的に無停止で切り替えが可能だったので、継続的なレプリケーションも設定しました。 そのため、下記のMySQLの設定を確認し、適宜修正しました。

log-bin=mysql-bin

binlog_format=row

binlog_checksum=none

DMSの設定

DMSの設定にもいくつか手順があります。

レプリケーションインスタンスの作成

オンプレDBからRDSへのレプリケーションの処理を担うインスタンスを作成します。 サブネットグループと紐付ける必要があるので、こちらも適宜作成します。

レプリケーションインスタンスの作成

エンドポイントの作成

移行元となるソース、移行先となるターゲットのエンドポイントを設定します。

エンドポイントの作成

それぞれに識別するための名前や、DBホスト名、ユーザ、パスワード等の接続情報を設定します。

移行タスクの作成

作成したレプリケーションインスタンス、エンドポイントを設定します。 移行タイプに「既存のデータを移行して、継続的な変更をレプリケートする」を指定します。

移行タスクの設定

移行タスクの詳細を設定します。

タスクの詳細設定

移行タスクを開始して、オンプレDBからRDSに変更がレプリケートされる状態となりました。

移行スケジュール

DMSの設定が完了して、実際にアプリケーションの向き先をオンプレからRDSへと変更します。

まずは、読み込みが多いアプリケーションから向き先をRDSに変更しました。1日ほど経過を見て問題が起きていないか等を確認します。

次に、書き込みが発生するアプリケーションの向き先をRDSに変更します。適宜、社内運用ツールやバッチを停止して行います。

その後、書き込み動作等を確認して問題なければ完了です。

最後に

DMSの使ったDBのAWS移行について、実際に行った流れを説明しました。 AWSで提供されているSCT、DMSのおかげで独自に移行方法を確立する必要がなく、便利なツールだと思います。 参考になれば幸いです。

参考

docs.aws.amazon.com

エキサイトは「PHP Conference2021」に協賛・登壇します

エキサイトは「PHP Conference2021」にシルバースポンサーとして協賛します。 今回は、弊社社員がRegular session、スポンサーツアーセッションに登壇いたします。 登壇情報については、以下をご覧ください。

Regular sessionについて

登壇者: おおしげ

日時:2021/10/02 16:15〜

場所:Track4

タイトル:新規プロジェクトの開発スタート前にやっておくべき環境整備たち

プロポーザルURL: fortee.jp

スポンサーツアーについて

日時:2021/10/02 12:00-12:30

登壇者: のぎわ あはれん

弊社は「メディアプラットフォーム」「ヘルスケア」「インフラテック」の3つの領域を主軸とし、自社企画開発で様々なサービスを展開しています。設立24年と歴史は長いですが、新規事業の立ち上げなどにも注力をしております。

スポンサーツアーでは、弊社について詳しく説明いたします。 また、今年度から行われている"技術組織活性"の取り組みについても説明いたします。 遊びに来てくださった方とざっくばらんにお話ができたらと思っております。ぜひ遊びに来てください♪

PHP Conference2021

開催日時:2021/10/02(土)~2021/10/03(日)

開催場所:オンライン開催

公式サイト:PHP Conference Japan 2021

公式TwitterPHPカンファレンス2021 (@phpcon) | Twitter

「Booost」ハッカソン型インターンを開催しました。

エキサイト株式会社では、9月9日(木)〜13日(月)に「Booost」ハッカソンインターンを開催しました。

学生向けの長期インターンは以前から実施していますが、ハッカソン型のインターンは初開催でした。 「学生生活をエキサイトさせるサービス」というテーマで、5日間かけてサービスの企画から開発までを行いました。

スケジュールについて

2日間でサービスを企画し、3日間で開発するというスケジュールでした。 サービスの企画は学生のみで行い、 開発からエキサイト株式会社とiXIT株式会社のエンジニアがメンターとしてサポートに入りました。

9月 9日(木) サービス企画1日目

9月10日(金) サービス企画2日目、中間発表

9月11日(土) 開発1日目

9月12日(日) 開発2日目

9月13日(月) 開発3日目、最終発表、オンライン懇親会

ハッカソンでの使用技術について

開発に集中していただきたかったので開発環境用意の負担を減らすため、 Docker・Laravel・Next.js・MySQL・Redisのプロジェクトを用意していました。 また、サービスを本番環境にリリースできるように、学生分のAWSアカウントを用意し、EC2等を利用できる状態にしていました。

ベースは用意していましたが、実際に使用する技術に関しては学生の皆様にお任せしていたので、チームによって違う技術構成になりました。

各チームが、サービスのコンセプトやメンバーの技術スタック、挑戦したい技術等で判断していて、チームの色が出ていました。各チームのサービス企画内容のリンク先に、使用技術について書いていますので、ぜひご覧ください。

コミュニケーションについて

コミュニケーションツールとしては、Discordを利用し、チームごとのボイスチャンネルとテキストチャンネルを用意しました。 ハッカソン中は、ボイスチャンネルに入って開発しており、話し合いがすぐできる環境になっていました。

毎日、朝会と夕会を行い、チームの進捗状況を人事やメンターが都度確認できるようにしていました。

メンターについて

エキサイト株式会社とiXIT株式会社のエンジニアがメンターとして参加しました。

チームごとにメンターが2〜3名付きまして、チームのボイスチャンネルに入って開発の状況を聞きながら、 タスクの優先順位付けやスケジューリング等のプロジェクトマネージメントから、 データベース設計やコーディング等の技術的なことまで、プロダクトの完成を目指して技術問わずサポートしていました。

各チームのサービス企画内容

各チーム、最終発表までにデモができる段階まで完成させてくれました。

以下のリンクに、サービスの詳細が記載されています。

topaz.dev topaz.dev topaz.dev topaz.dev

ハッカソン 結果

優勝:

本音ターン

受賞理由:

サービスの完成度が高く、実機と管理画面のデモが完璧動いていた点が素晴らしかったです。 学生に関する社会的な課題の解決を目指しているところがより実用的で良くアイデアも高い評価がされました。

特別賞:

Lian

受賞理由:

サービスデザインの完成度の高い点や、 短期間の間で、想定していた機能を作り上げていた点が評価されました。

参加者のブログ

参加してくださった学生さんがブログ記事を書いてくださりましたので、こちらもご覧ください。とても有り難いです。

exciteのハッカソン型インターン「Booost」で特別賞もらえた話 - Qiita

最後に

ハッカソン初心者の学生もいる中で、学生やメンター含め全メンバーが切磋琢磨し、最後発表まで開発をやり遂げてくれました。 また学生の皆様に、このハッカソンを通してチーム開発や技術的な知識について学びを得ることができたと言っていただけました。 運営としても、学生の皆様のチャレンジ力に刺激され、様々な学びを得ることができました。 どちらとも学びのある良い5日間となりました。

弊社では、今回のようなハッカソンインターンや、長期インターン等の用意しています。 また、一緒に働いてくれる仲間を絶賛募集しております! ご興味のある方は、以下のリンクからぜひご応募ください!

www.wantedly.com

JavaとPythonとGoのHTTPステータスコードの実装を調査した

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 Spring Boot / Javaで既存システムのリビルドを進めてHTTPステータスコードの実装を調べている途中に、ふと他の言語の実装が気になり、普段趣味で使用しているPythonとGoのソースコードを見てみました。 本記事では、JavaPython、GoのHTTPステータスコードの実装について紹介します。

調査環境

今回調査した環境は以下のとおりです。

  • JavaSpring Framework 5.3.7)
    • org/springframework/http/HttpStatus.java
  • Python 3.9.7
    • http/__init__.py
  • Go 1.17
    • src/net/http/status.go

Javaステータスコード

JavaSpring Framework)のステータスコードenumを使って実装されていました。 ステータスコードごとに要素が存在し、とてもわかりやすい実装になっています。 また、HttpStatusには2xx系のステータスコードかどうかを判定するis2xxSuccessful()メソッドや、エラーかどうかの判定するisError()メソッドが実装されています。ここに挙げたメソッド以外にもHTTPステータスコードに関係する便利なメソッドがいくつか実装されているので、必要に応じて活用していきたいです。

public enum HttpStatus {
    OK(200, Series.SUCCESSFUL, "OK"),
    CREATED(201, Series.SUCCESSFUL, "Created"),
    ACCEPTED(202, Series.SUCCESSFUL, "Accepted"),

   public boolean is2xxSuccessful() {
        return (series == Series.SUCCESSFUL);
    }


    public boolean isError() {
        return (is4xxClientError() || is5xxServerError());
    }

    private final int value;
    private final Series series;
    private final String reasonPhrase;

    HttpStatus(int value, Series series, String reasonPhrase) {
        this.value = value;
        this.series = series;
        this.reasonPhrase = reasonPhrase;
    }

    public int value() {
        return this.value;
    }
}

使い方

HttpStatusはvalue()メソッドを呼び出すことでステータスコードを取得することでき、getReasonPhrase()メソッドを呼び出すことでメッセージを取得することができます。

// 引数にHttpStatus.CREATEDを渡して実行
public void check(HttpStatus status) {
    System.out.println(status.value()); 
    // → 201
    
    System.out.println(status.getReasonPhrase());
    // → Created
}

Pythonステータスコード

Pythonの標準ライブラリのステータスコードは、IntEnumを継承して実装されていました。 Javaステータスコードの実装と比較的似ていますが、こちらはステータスコードのみ実装されており、ちょっとしたことで使える便利なメソッドの実装はありませんでした。

class HTTPStatus(IntEnum):
    OK = 200, 'OK', 'Request fulfilled, document follows'
    CREATED = 201, 'Created', 'Document created, URL follows'
    ACCEPTED = (202, 'Accepted', 'Request accepted, processing continues off-line')

使い方

HTTPStatusは、valueを呼び出すことでステータスコードを取得することができ、phraseを呼び出すことでメッセージを取得することができます。 また、IntEnumを継承しているためint型と比較することができます。

# 引数にHTTPStatus.CREATEDを渡して実行
def check(status: http.HTTPStatus):
    print(status.value)
    # → 201

    print(status.phrase)
    # → Created

    print(status == http.HTTPStatus.CREATED)
    # → True

    print(status == 201)
    # → True

Goのステータスコード

Goには上記に挙げたenumのような機能はありません。 Goでは、定数とmapを組み合わせてHTTPステータスコードを実装しています。 そのため、上記2つとは異なり、HttpStatus型のようなものはないため、ステータスコードはint型として扱わなければなりません。

package http

const (
    StatusOK       = 200 // RFC 7231, 6.3.1
    StatusCreated  = 201 // RFC 7231, 6.3.2
    StatusAccepted = 202 // RFC 7231, 6.3.3
)

var statusText = map[int]string{
    StatusOK:       "OK",
    StatusCreated:  "Created",
    StatusAccepted: "Accepted",
}

func StatusText(code int) string {
    return statusText[code]
}

使い方

とてもシンプルです!

// 引数にhttp.StatusCreatedを渡して実行
func check(code int) {
    fmt.Println(code)
    //→ 201

    fmt.Println(http.StatusText(code))
    // → Created
}

おわりに

本記事ではJavaPython、GoのHTTPステータスコードの実装についてまとめました。 実際にソースコードを比較してみると、各言語の特色が出てきて面白かったです。 最後まで読んでいただき、ありがとうございました!

Javaでクエリパラメータの日時データを受け取る方法

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

APIで、日付情報をクエリパラメータとして受け取るという場面はそう珍しいものではありません。 ただし日付情報の問題点として、様々なフォーマットが存在しうるというものがあります。

今回は、渡されるフォーマットに合わせて日付情報を受け取る方法を説明します。

日付情報とフォーマット

日付情報というのは、例えば「2020年1月1日 0時0分0秒」みたいなものです。 ただ、これを示すフォーマットはいくつかあります。 例えば、

  • 2020-01-01 00:00:00
  • 2020/1/1 00:00:00
  • 20200101000000

などなど…。 先程の日本語を織り交ぜた「2020年1月1日 0時0分0秒」もそうですし、日本語以外でもそのようなフォーマットはあるでしょう。

では、これらをJavaで考慮してAPIのクエリパラメータとして受け取れるようにするにはどうすれば良いでしょうか?

@DateTimeFormat

実は、Spring Bootであれば @DateTimeFormat というものを使えば簡単にできます。

実装方法は、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

こんな感じで、引数にアノテーションとしてつけるだけでOKです。 アノテーション内の pattern で、受け取りたいフォーマットを指定します。

また、もし複数パターンで来ることがある場合でも、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "[yyyy-MM-dd HH:mm:ss][yyyyMMddHHmmss]") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

このように指定することで複数パターンを許容することができます。

ただし注意点として、 LocalDateTime 型(年月日・時間の両方を持つことが前提の型)では、

class SampleRequest {
    LocalDateTime sampleDateTime;

    public SampleRequest(
        @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDateTime sampleDateTime
    ) {
        this.sampleDateTime = sampleDateTime;
    }
}

のように日付だけで受け取ろうとするとエラーになるのでご注意下さい。

最後に

日付のフォーマットは、言語によって取り扱い方の難易度が変わってきます。 Javaであれば上記のようにフォーマットの指定が必要ですが、言語によってはあまり気にせず受け取れたりする場合もあるでしょう。

ただ、最初はゆるい制限の言語を使っていたために日付のフォーマットはそこまで気にしていなかったが、仕様や言語が変わって硬いフォーマット指定が必要になって大変…ということも起こり得るので、普段から日付のフォーマットの統一は心がけると良いでしょう。

もうキャッシュの実装は怖くない!

はじめに

こんにちは。エキサイト株式会社で長期インターンをさせていただいている岡崎です。

今回は私が学んだキャッシュについての記事を書かせていただきます。

「もう分かっているんだが?」というエンジニアの皆さん向けではなく、Spring Bootでキャッシュを初めて実装する人向けの記事となっております。

そもそもキャッシュとは?

キャッシュとは、アクセスしたwebページの情報を一時的に保存していく技術です。

キャッシュを用いることで1からページを読み込むことがないので、動作の速さが期待できるぞってことですね。

キャッシュの流れは以下の通りになっています。

  1. コントローラまたはサービスは@Cacheableがついたメソッドを呼び出す
  2. Cache AOPが提供する@Cacheableにキーが渡される。CacheManegarを利用して、Hash Mapからデータを取得する。キャッシュデータが取得できた場合はコントローラまたはサービスへキャッシュデータを返却し、キャッシュデータが取得できない場合は次を実行する。
  3. Cache AOPは引数を渡し、定義されたサービスメソッドを実行し、戻り値を取得する。Cache AOPは2で特定されたキャッシュキーで取得した戻り値をCache Manegerを利用してHash Mapへデータとして格納する。
  4. Cache AOPはコントローラまたはサービスへ戻り値を返却する。

Cache AOP

キャッシュ機能の入り口になるインターフェース

Cache Maneger

キャッシュ機能をコントロールしてくれる

キャッシュのデータはredisが保持してくれます。

このredisはデータをメモリに保存してくれる高速なデータストアのことです。

キャッシュの実装は?

はい、ようやく本題ですね。

じゃあ、それってどうやって実装するの?という話です。

Spring Bootではキャッシュ機能のためのライブラリがすでに用意されています。

 キャッシュする必要があるメソッドに@Cacheableをつける

ますはここです。

この@Cacheableとは、キャッシュを有効化にするためのアノテーションです。

キャッシュを行いたいメソッドに対し、

@Cacheable(

cachename = “名前を任意でつける”

key = “キーがあればキーをつける。なければなくてよい”

condition = “キャッシュをする条件があればここでつける。なくてもよい”

)

public void sample(Integer sample, String sample) {

とつけます。

これだけでいい場合もあるのですが、現在だとそれだけではないときも多いと思います。

散々見たエラーはCould not read JSON: Cannot construct instance ofというものでした。

データのやり取りのイメージは以下です。

f:id:ooo-ka999:20210907105750p:plain

このアノテーションをつけることだけでうまくいく場合もありますが、そうでないこともあります。

例えばLocalDateTimeです。 うまくデフォルトのものだと変換ができなくて落ちてしまいます。 なので、

@JsonSerialize(using = LocalDateTimeSerializer.class)
@JsonDeserialize(using = LocalDateTimeDeserializer.class)
private LocalDateTime sampleTime;

のようにアノテーションをつけてあげる必要があります。

そのほかにも、こんな事例がありました。 Jacksonでは、publicのフィールド、publicのゲッターを対象としてしまうので、対象にしたくないものは@JsonIgnoreをつける必要がありました。これで対象から外すためです。

使用例は以下の通りです。

@JsonIgnore
public Boolean isHogeHoge(String hogehoge) {
 return this.hogehoge == hogehoge;
}

また、デシリアライズ時にデフォルトコンストラクタが使用されてしまうので、デフォルトコンストラクタでデシリアライズしてほしくない場合は、@JsonCreatorを使ってデシリアライズ時のコンストラクタを使用してあげる必要があります。また、このときにprivateなインスタンス変数を使う場合は@JsonPropertyを用いてマッピングしたいキーの名前を指定してあげる必要がありました。

使用例は以下の通りです。

@JsonCreator
public SampleModel(@JsonProperty("sample") String sample) {
 this.sample=sample;
}

まとめ

キャッシュはキャッシュの対象にきちんと必要なアノテーションをつけてあげることが大切でした。キャッシュって奥深いんだなと今回では学びました。 まだまだ未熟なエンジニアですが、どんどん成長していきたいと思います。

また、余談ですがエキサイトのインターンはとても楽しく、充実しています! 興味ある人にはとてもおすすめしたいです。私はまだ引き続きインターンは続くので、残りの時間も成長できるように頑張っていきたいです。

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

PHPDoc の @uses を使って、可変関数の未使用警告を抑制する

エキサイト株式会社の武藤です。

PHP の可変関数

PHP には可変関数という機能があります。

www.php.net

関数名を変数にすることで、動的に呼び出したい関数を変更できます。

<?php

class Demo {

    function foo() {
        echo "foo";
    }
}

$functionName = 'foo';
$demo = new Demo();

$demo->$functionName();

使い方によっては有用な機能かもしれません。

しかし、intelliJ (PhpStrom) でファイルを開いてみると、未使用関数としてグレーアウトされてしまいます。

f:id:excite-mthiroshi:20210913201930p:plain
intelliJ で可変関数呼び出しをすると未使用関数として扱われる

未使用のコードは、運用開発の際にノイズになるので、可能な限り削除してしまう方がよいでしょう。しかし、可変関数呼び出しのコードがある場合、プロジェクト全体に検索をかけて、使われていないことの確認が必要です。 また、未使用関数の場合、定義元へコードジャンプも効かないため、修正するファイルの切り替え等の作業が煩雑になってしまいます。

PHPDoc の @uses を使おう

intelliJ で可変関数がグレーアウトされてしまうのは、静的解析を基に未使用と判断されるからです。 そこで、 PHPDoc の @uses を使って適切に呼び出し関係を補足してあげましょう。

f:id:excite-mthiroshi:20210913202215p:plain
呼び出し元で @uses をつける

可変関数が未使用関数ではなくなり、色が付きました。また、これでコードジャンプもできるようになりました。

最後に

PHP の可変関数について、intelliJ で表示される未使用警告の回避方法を説明しました。

長年運用されてきたソフトウェアは、使われなくなったコードが溜まってきてしまいます。Doc 以外にも、多くの言語で静的解析ツールが用意されていますので、それらを上手く駆使して無駄がないコードを保っていきましょう。

参考

PhpStorm の静的解析機能をさらに活用するための3つのアノテーション | バシャログ。

@uses & @used-by — phpDocumentor

Spring Bootでクエリパラメータの順序が異なるURIを比較する

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 UriComponentsのreplaceQueryParam()を使用してクエリパラメータの値を書き換えたときに、書き換える前と書き換えた後とでクエリパラメータの順序が異なってしまい、 単体テストで落ちてしまいました。 本記事では、クエリパラメータの順序が異なるURIを比較する方法について共有します。

概要

クエリパラメータの順序が異なる2つのURIを用意します。 このとき、uri1とuri2を比較するとfalseになります。

URI uri1 = URI.create("https://example.com?page=2&per_page=10");
URI uri2 = URI.create("https://example.com?per_page=10&page=2");

System.out.println(Objects.equals(uri1, uri2)); // → false

これは、URIがクエリパラメータを文字列で保持しており、equalsで比較したときにクエリパラメータは"page=2&per_page=10""per_page=10&page=2"の文字列比較を行っているため、falseになってしまうからです。 クエリパラメータの順序が異なったとしても同一のものとして比較したいときは、少し工夫する必要があります。

解決策

URIをUriComponentsに変換することで容易に比較することができるようになります。 UriComponentsではクエリパラメータをMap<K, List<V>>のラッパーであるMultiValueMapで保持しているため、クエリパラメータの順序が異なるものを比較したときにtrueを返します。

UriComponents u1 = UriComponentsBuilder.fromUri(uri1).build();
UriComponents u2 = UriComponentsBuilder.fromUri(uri2).build();

System.out.println(Objects.equals(u1, u2)); 
// → true

実際にUriComponentsのクエリパラメータの中身を見てみると、確かにMap形式で保持されていることが確認できます。

System.out.println(u1.getQueryParams());
// → {page=[2], per_page=[10]}

注意点

MultiValueMapは1つのキーに対して複数の値を扱うことができます。 そのため、クエリパラメータに同一のキーが複数指定されている場合、Listに変換されるため、falseを返す可能性があります。

URI uri3 = URI.create("https://example.com?page=2&per_page=10&page=3");
URI uri4 = URI.create("https://example.com?page=3&per_page=10&page=2");

UriComponents u3 = UriComponentsBuilder.fromUri(uri3).build();
UriComponents u4 = UriComponentsBuilder.fromUri(uri4).build();

System.out.println(u3.getQueryParams());
// → {page=[2, 3], per_page=[10]}

System.out.println(u4.getQueryParams());
// → {page=[3, 2], per_page=[10]}

System.out.println(Objects.equals(u3, u4));
// → false

したがって、同一のキーが複数指定される場合は注意しなくてはなりません

おわりに

UriComponentsを利用してクエリパラメータの順序が異なるURIを比較する方法についてまとめました。 クエリパラメータの順序が異なるURIを比較したいときって一度はあるのかなと思います。 URIをUriComponentsにして比較を行うことで、クエリパラメータ順序が異なっていたとしてもtrueを返すようになります。 「クエリパラメータの順序が異なるURIでも同じものとして扱いたい!」といった方に本記事が参考になれば幸いです。

参考

docs.oracle.com

spring.pleiades.io