axios通信時にNuxt.jsのローディングアニメーションを実装する

はじめに

こんにちは!SaaS事業部エンジニアの小川です。

普段はSaaS事業部でKUROTEN.という経営管理SaaSの開発をしています。

今回は、Nuxt.jsでaxios通信時にローディングアニメーションを表示する方法を紹介します。

NuxtのLoading設定(Nuxt標準)

基本的にNuxt.jsでローディングアニメーションを実装する場合、nuxt.config.jsのloadingオプションをtrueにする方法がよく使われていると思います。 ここでいうローディングは、ページ遷移の間の読み込み時に動作します。

# nuxt.config.js
export default {
    loading: true
}

また、こちらのローディングオプションにはオブジェクトを渡してあげることで、ある程度細かく見た目を変えることもできます。(以下では一例を紹介)

# nuxt.config.js
export default {
    loading: {
        progress-bar-height: 2px, // プログレスバーの高さ
        rtl: false // プログレスバーの向きを指定
    }
}

さらに見た目をカスタマイズしたい場合は以下のようにコンポーネントを用意し、 NuxtのLoadingオプションにコンポーネントを設定することも可能です。

// nuxt.config.js
export default {
    Loading: './commponents/Loading'
}

axios通信時にLoadingを動かす

ここからが本題です。 Nuxt.jsでaxios通信時にローディング処理を行いたい場合、以下の2つの手法があります。

this.loading.startとthis.loading.finishで通信中を取得

公式で紹介されている方法で、 axios通信している箇所にstartfinishの処理を追加するだけで実装できるのでお手軽です。 あとは先ほど紹介したNuxtのLoadingオプションをtrueにすれば従来どおりの方法でローディングが動作します。

// ローディング処理を入れたい箇所
this.loading.start ()
hoge () // axiosの処理
this.loading.finish ()

storeとpluginのaxiosでローディングの状態を管理する

上で述べた手法でもaxios通信時にローディング処理を走らせることができますが、 呼び出し処理を追加するたびにstartとfinishの記述が必要になってしまうため 通信処理が複数あると開発者の負担が大きいように思います。

そこで、storeでローディングの状態管理をすることで、axios通信時の処理を一括して記述する方法をご紹介します。

まずはloadingの状態を管理するためのstoreを追加します。

// ./store/loading.js

export const state = () => ({
  loadingCount: 0
})

export const getters = {
  loadingCount: (state) => state.loadingCount,
  isLoading: (state) => state.loadingCount > 0
}

export const mutations = {
  incrementLoadingCount (state) {
    ++state.loadingCount
  },
  decrementLoadingCount (state) {
    if (state.loadingCount > 0) {
      --state.loadingCount
    }
  }
}

export const actions = {
  incrementLoadingCount ({ commit }) {
    return commit('incrementLoadingCount')
  },
  decrementLoadingCount ({ commit }) {
    return commit('decrementLoadingCount')
  }
}

次にaxiosを用いた通信中にLoadingCountが1以上になるようpluginsのaxios.jsに先ほど作成したアクションを追加します。 このpluginsはaxiosのリクエストやレスポンスをフックにして呼び出されるので、共通化したい場合にとても便利です。

// ./plugins/axios.js

export default function({ $axios, store }) {
    $axios.onRequest(config => {
        store.dispatch('loading/incrementLoadingCount')
    })

    $axios.onResponse(() => {
        store.dispatch('loading/decrementLoadingCount')
    })
    
    $axios.onError(e => {
        store.dispatch('loading/decrementLoadingCount')
    })
}

あとは上述で作成したLoadingCountをトリガーにして読み込みのオーバーレイやアニメーションを表示すれば完成です。 以下は通信中にVuetifyのプログレスバーを動かすサンプルです。LoadingCountをトリガーに使うだけなので、自由度は非常に高いです。

// ./components/Loading.vue
<template>
  <v-progress-linear
    :active="isLoading"
    :indeterminate="isLoading"
    :height="loadingHeight"
    absolute
    bottom
    :color="loadingColor" />
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    name: 'Loading',
    data () {
      return {
        loadingHeight: '5',
        loadingColor: '#4DB6AC'
      }
    },
    computed: {
      ...mapGetters({
        isLoading: 'loading/isLoading'
      })
    }
  }
</script>

先程実装したLoadingコンポーネントをHeaderコンポーネントで呼び出し、layoutsのdefaultへ渡すことで、全ページにローディング処理の実装ができます。

// ./components/Header.vue

<template>
  <v-app-bar
    color="primary"
    class="app-bar"
    :height="height"
    fixed
    app>
    <loading />
  </v-app-bar>
</template>

<script>
  import Loading from '@/components/Loading'

  export default {
    name: 'Header',
    components: {
      Loading
    },
    data () {
      return {
        height: '50'
      }
    }
  }
</script>
// ./layouts/default.vue

<template>
    <div>
        <Header />
    </div>
</template>

<script>
import Header from '@/components/Header'

export default {
    components: {
        Header
    }
}
</script>

これでaxios通信が行われている最中にLoadingアニメーションが実行されます。

axios通信時のLoadingの様子

最後に

axios通信時の手法の肝はLoadingCountという状態を使うことなので、簡単に応用が効くと思います。 axios通信時にローディング処理を入れたいという方は一度試してみてはいかがでしょうか!

SQLServerのWHERE句は大文字小文字でもヒットしてしまう問題

エキサイトしばたにえんです。 早速ですが、

WITH hogefuga_table AS (
    SELECT 'hogefuga' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'HOGEFUGA';

こちらhogefugaがヒットされてしまいます。 WHERE句の大文字小文字を判別してくれません。

SQLServerのWHERE句で大文字小文字を判別するためにcollateが使えます。

WITH hogefuga_table AS (
    SELECT 'hogefuga' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'HOGEFUGA' COLLATE Japanese_CS_AS_KS_WS;

とすればWHERE句の大文字小文字を判別してくれるようになり、ヒットされることはありません。

COLLATEには他にも種類があります。 例えば、「ぽ」、「ぼ」、「ボ」、「ポ」の判別を「したくない」場合

WITH hogefuga_table AS (
    SELECT 'ボボぼーボボーボぽ' AS word
)
SELECT * FROM hogefuga_table 
WHERE word = 'ぽぼポーぽぽーぽボ' COLLATE Japanese_CI_AI;

とするとボボぼーボボーボぽヒットしてくれるようになります。 他にもCOLLATEには種類があるので必要に応じて使い分けるといいかもしれません。

git branchの結果を時間順に表示

エキサイトのしばたにえんです。
時間順にソートするgit branchのoptionの紹介です
「さっき作ったブランチが見つからない」、
「ターミナルのタブも消してしまってhistoryでも見つからない」
って時に便利です。

新しい順の表示
git branch --sort=-authordate
古い順の表示
git branch --sort=authordate

新しい順の表示なんかは何かと便利だと思いますので使ってみてください。
git branch --helpで他のオプションも確認できるので調べてみてもいいかもしれません。

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であれば上記のようにフォーマットの指定が必要ですが、言語によってはあまり気にせず受け取れたりする場合もあるでしょう。

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