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

f:id:excite-mthiroshi:20210929163136p:plain
レプリケーションインスタンスの作成

エンドポイントの作成

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

f:id:excite-mthiroshi:20210929162725p:plain
エンドポイントの作成

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

移行タスクの作成

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

f:id:excite-mthiroshi:20210929165305p:plain
移行タスクの設定

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

f:id:excite-mthiroshi:20210929165453p:plain
タスクの詳細設定

移行タスクを開始して、オンプレ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

開催日時:2020/12/12

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

公式サイト:PHP Conference Japan 2021

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