「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

mysqlslap で MySQL の負荷エミュレーションをしてみる

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

オンプレにあるDBを稼働中のRDSに統合する際に、mysqlslapを使った負荷エミュレーションをしたので、使い方を紹介します。

mysqlslapとは

mysqlslapとは、負荷エミュレーションをするコマンドです。クライアントの接続数やクエリ数などを設定して、実行時間を計測してくれます。 dev.mysql.com

mysqlCLIクライアントをインストールした際に一緒にいくつかの便利コマンドが入っているみたいです。mysqlslapもその一つです。

実行例

75クライアントから75クエリーのUPDATE文の実行を10回繰り返す実行例です。

DB設定

DBホスト : demo_db
mysqlユーザ : demo_user
mysqlパスワード : demo_password
ポート : 3306
mysqlslap \
--no-defaults \
--auto-generate-sql \
--engine=innodb \
--create-schema=demo_schema \
--password=demo_password \
--host=demo_db \
--user=demo_user \
--port=3306 \
--number-char-cols=3 \
--iterations=10 \
--concurrency=75 \
--auto-generate-sql-write-number=10 \
--auto-generate-sql-add-autoincrement \
--auto-generate-sql-load-type=update \
--number-of-queries=75
mysqlslap: [Warning] Using a password on the command line interface can be insecure.
Benchmark
    Running for engine innodb
    Average number of seconds to run all queries: 1.200 seconds
    Minimum number of seconds to run all queries: 1.068 seconds
    Maximum number of seconds to run all queries: 1.371 seconds
    Number of clients running queries: 75
    Average number of queries per client: 1

1回の試行の実行時間の平均が1.200秒、最小値が1.068秒、最大値が1.371秒でした。 このように、パラメータを調整して実行時間を計測します。

設定したオプションの説明です。

--no-defaults : デフォルト値を読み込まない
--engine : 使用するストレージエンジン
--create-schema : スキーマ
--password : DBのパスワード
--host : DBのホスト
--user : DBのユーザ
--port : ポート番号
--iterations : 試行回数
--concurrency : クライアント数
--auto-generate-sql : SQLを自動で生成
--auto-generate-sql-write-number : 各スレッドで実行する行挿入の回数
--auto-generate-sql-add-autoincrement : AUTO_INCREMENT カラムを自動生成されたテーブルに追加
--auto-generate-sql-load-type : テストの負荷タイプを指定
--number-of-queries : 各クライアントのクエリー数の指定
--number-char-cols : --auto-generate-sql が指定されている場合に使用する VARCHAR カラムの数

これ以外にも様々なオプションがありますので、公式ドキュメントを御覧ください。

検証したこと

今回は、オンプレDBからRDSで稼働中のDBに相乗りさせる形で移行を考えていました。その場合、オンプレDB分の負荷がRDSにそのままかかってきますので、それに耐えられるかを検証しました。

まずは、オンプレDBにかかっている負荷をzabbixから確認しました。それを基にmysqlslapでRDSに負荷をかけて、実行時間を確認していきました。最終的には、zabbixで確認した負荷より大幅に高い負荷をかけても実行時間が十分に短かったので、移行しても耐えられると判断しました。

そして、実際にRDSへ統合してCloudWatchで負荷を確認したところ、詰まることなく捌けていました。

事前に負荷検証をすることで、安全にDB移行ができました。

最後に

オンプレDBのAWS移行にあたって、RDSに対して負荷検証をしてみました。 mysqlslapで簡単に負荷検証ができるので、ぜひ使ってみてください。