Javaでメソッド参照を使うかラムダ式を使うかの判断

エキサイト株式会社 メディア開発佐々木です。

現在Spring/Javaで開発するにあたって、関数型インターフェースを呼ぶときにラムダ式でもメソッド参照でも書ける場合があります。どちらを使った方が、継続的な開発にいいのかを簡単にですがまとめてみます。

引数なしの場合

関数型インターフェースでArrayListのインスタンスを返却するだけの宣言ですが、下記の2パターンがあります。

// ラムダ式
Supplier<ArrayList<Integer>> supplierList = () -> new ArrayList<>();


// メソッド参照
Supplier<ArrayList<Integer>> supplierList = ArrayList::new;

パッと見た読みやすさでは、メソッド参照の方がカッコいいし良い選択かもしれません。 ArrayList::new と書いてあるだけなので、 new ArrayList() が入ってるんだろうなと容易に想像ができます。一方、ラムダ式では () -> のような引数がないことを明示しなくてはなりません。コードを書く側からすると面倒ですが、読む側からすると、引数がないことを明示してくれていた方が理解しやすいと思います。

引数が複数の場合

引数が複数の場合は、下記のような実装になります。

// ラムダ式
BinaryOperator<Integer> sum = (a , b) -> Integer.sum(a,b);

// メソッド参照
BinaryOperator<Integer> sum = Integer::sum;

メソッド参照では、計算されていることはわかりますが、引数に何が指定されているかはパッと見はわかりません。 あとから使う人がBinaryOperatorを知っていれば引数を2つとることはわかりますが、知らない人は調べる必要があります。片やラムダ式は、引数を2つとることが明示されています。こちらは、BinaryOperatorを知っている必要がありません。

まとめ

EffectiveJavaにはメソッド参照を使った方がいいとありますが、個人的には読む人がわかりやすく汎用的なものを選びたいと思います。メソッド参照はすっきりしていてカッコイイよく、IDEからも変換を促されるかもしれませんが、読みにくかったり等するのであれば、チーム開発という上では使わないという選択もありかなと思います。IDEのアシストをそっとオフにしたいと思います。

下記、参考にさせていただきました。 nowokay.hatenablog.com

@CookieValueでクッキーの値を取得するとき、URLエンコードされる問題の解消

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

Spring BootでCookieの値を取得する時、Controllerで

@CookieValue(name = "hoge", required = false) String hoge

で取得できると思います。

しかし、Stringで受け取る際、勝手にURLデコードされて困る場合があります。

そこで、URLデコードせずに受け取る方法を説明します。

結論から申しますと、Cookieオブジェクトで取得する方法です。

ユースケース

  1. Cookieの値を取得したい。
  2. URLデコードされたくない

題材

コード例

    @GetMapping("test")
    public String test(
            @CookieValue(name = "hoge", required = false) String hoge
    ) {
        return hoge;
    }

入力例

Cookieは以下を設定。

hoge=hogehoge%0D%0Ahogehoge; Path=/; Expires=Sun, 10 Apr 2022 01:47:34 GMT;

出力例

f:id:excite-naka-sho:20210410105422p:plain

%0D%0A→\r\nに自動的に変換されている

条件

  • URLデコードできる文字列をCookieに設定する

改善

コード例

    @GetMapping("test")
    public String test(
            @CookieValue(name = "hoge", required = false) Cookie hoge
    ) {
        return hoge.getValue();
    }

Cookieで取得し、getValue()で中身を返却

入力例

Cookieは同様な設定をする

出力例

f:id:excite-naka-sho:20210410110833p:plain

エンコードされていないことがわかる。

まとめ

URLデコードされたくないケースはそんなにないかもしれませんが、 覚えておいて損はないかと思います。

MapStructで高速なオブジェクトマッピングをする

エキサイト株式会社メディア開発の佐々木です。

以前、下記の記事でJavaのオブジェクトマッピングツールでModelMapperを紹介しました。

excitech.hatenablog.com

ModelMapperは、とてもお手軽で大変便利なのですが、やや速度に問題があり大量のデータ処理等には不向きです。そこで MapStructを紹介します。

ライブラリ追加

build.gradleに下記を追加し、ライブラリを追加します。

dependencies {
  ...
        implementation 'org.mapstruct:mapstruct:1.4.2.Final'
        annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
        annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0'   // lombokを併用する場合は必要
  ...
}

コード全体

コード全体は下記のようになります。

public class DemoMain {

    public static void main(String[] args) {
        InputModel inputModel = new InputModel();
        inputModel.setId(1);
        inputModel.setName("taro");

        RequestModel requestModel = RequestMapper.INSTANCE.toRequestModel(inputModel);
        System.out.println(requestModel);

        Request2Model request2Model = RequestMapper.INSTANCE.toRequest2Model(inputModel);
        System.out.println(request2Model);
    }

    @Data
    static class InputModel {
        private Integer id;
        private String name;
    }

    @Data
    static class RequestModel {
        private Integer id;
        private String name;
    }

    @Data
    static class Request2Model {
        private Integer no;
        private String title;
    }

    @Mapper
    interface RequestMapper {
        RequestMapper INSTANCE = Mappers.getMapper(RequestMapper.class);
        RequestModel toRequestModel(InputModel inputModel);

        @Mapping(source = "name", target = "title")
        @Mapping(source = "id", target = "no")
        Request2Model toRequest2Model(InputModel inputModel);
    }
}

出力結果

出力結果は下記になります。

DemoMain.RequestModel(id=1, name=taro)
DemoMain.Request2Model(no=1, title=taro)

ざっくり解説

データクラス定義

入力のクラスはこのように定義します。

    @Data
    static class InputModel {
        private Integer id;
        private String name;
    }

出力は、プロパティ名が同じものを用意します。異なったものでも可能なので、そのケースものも用意しておきます。

    // プロパティが同じクラス定義
    @Data
    static class RequestModel {
        private Integer id;
        private String name;
    }


    // プロパティが異なったクラス
    @Data
    static class Request2Model {
        private Integer no;
        private String title;
    }

マッピング処理の定義

マッピング処理はインターフェースに記述します。

    @Mapper
    interface RequestMapper {
        // Interfaceに定数を定義する
        RequestMapper INSTANCE = Mappers.getMapper(RequestMapper.class);

        // プロパティ名が同名の場合のメソッド定義
        RequestModel toRequestModel(InputModel inputModel);

        // プロパティ名が異なった場合のメソッド定義
        @Mapping(source = "name", target = "title")
        @Mapping(source = "id", target = "no")
        Request2Model toRequest2Model(InputModel inputModel);
    }

上記のようにインターフェースにアノテーションで定義を書いていきます。プロパティ名が同名である場合は、アノテーションは不要です。プロパティ名が異なった場合は、その数のアノテーション定義を書くことになります。

RequestMapperインタフェース内にオブジェクトの変換処理がまとめられるので、とても見通しがよくなります。また、ModelMapperよりはるかに高速です。(計測している記事) 毎回インターフェースを定義する必要があるので、ModelMapperよりは少々手間がかかりますが、大量データを処理する場合は、速度的に十分リターンがあると思いますので使ってみてください。

メディア開発では、中途採用をはじめ長期インターンの募集もしております。興味があればぜひお声がけください。

www.wantedly.com

CI/CDについて

エキサイト株式会社 新規事業の開発を担当している森脇です。

エキサイトでは2、3年前からオンプレからクラウドへの移行を行っています。 移行したサービス中心にCI/CDを導入するケースが増えてきています。

私が担当している新規のサービスに関しても開発当初から導入をしています。

CI/CDとは

改めてCI/CDとは、Continuous Integration / Continuous Delivery の略で、日本語だと継続的インティグレーション/ 継続的デリバリー、意味としては自動的にテストをして本番へリリースをする、もしくはリリース可能な状態にしておくことですね

CI/CDを導入すると、バグが減ったり、変更を自動的にリリースしたりすることができとても便利です。

CI/CD構成

f:id:moriwaki111:20210402095607p:plain

GitHub ActionとAWSのCodePipelineを使ってCI/CDを構築しています。 プルリクをフックにして、GitHub Actionでlinttestを実行します、テストがOKだったものに関してコードレビューをするようにしています。 (図では省略していますが、GitHub Actionとマージの間に人為的なレビューがあります)

コードレビューでLGTMになれば、mainブランチにマージされ、AWSのCodePipelineが動いて、本番環境へデプロイされるようになっています。

よかった事

  • バグが減った事
  • 仕様に注力してコードレビューが行える事 (インデントとか使ってない変数があるとかを見なくて良くなった事)
  • 自動で網羅的にテストが行える事

課題

  • フロントのテストでは、apiの通信をmock化しており、レスポンスをjsonで保存し使用している、apiの改修があった時にjsonの更新を手動で行わないといけない

課題はありつつ、CI/CDを導入すると、自分たちも楽できるし、ユーザさんにとっても質の高いサービスを提供できるので、導入がまだ行われていないサービスは是非やる良いと思います

「連想配列」と「ドメインモデル」の違い

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

エキサイトは昔からPHPがよく使われてきましたが、特に古いコードだとその中で連想配列が頻繁に利用されています。 一方で最近ではエキサイト内でもドメイン駆動設計が考慮されることが増え、連想配列ではなくドメインモデルが利用されることが増えてきました。

ここでは、「連想配列」と「ドメインモデル」の違いはなんなのか、簡単に説明していきます。

連想配列とドメインモデルの共通点

連想配列もドメインモデルも、「1~複数のデータを格納する」という役割は共通です。 例えば「記事」のデータだと、タイトル・本文・公開日など複数のデータの複合で成り立っています。 こうしたデータについて、それぞれ別々に変数に入れるのではなく、連想配列やドメインモデルといった形でまとめて保存することで、取り扱いを容易にすることができます。

連想配列

$article = [
    'title' => '記事タイトル',
    'story' => '記事本文',
    'publishDate' => '2021-01-01 00:00:00'
];

ドメインモデル

class Article {
    private $title;
    private $story;
    private $publishDate;
    
    public function __construct($title, $story, $publishDate) {
        $this->title = $title;
        $this->story = $story;
        $this->publishDate = $publishDate;
    }

    public function getTitle() {
        return $this->title;
    }

    public function getStory() {
        return $this->story;
    }

    public function getPublishDate() {
        return $this->publishDate;
    }
}

$article = new Article('記事タイトル', '記事本文', '2021-01-01 00:00:00');

連想配列とドメインモデルの違い

では次に、連想配列とドメインモデルの違いを、それぞれの利点から見ていきます。

連想配列の利点

連想配列の利点は、何と言ってもその使いやすさかと思います。 上記の例を見て分かる通り、同じデータを入れるのでも、ドメインモデルに比べて連想配列を使うほうが圧倒的に簡単にコードを書くことができます。

ドメインモデルの利点

入りうるデータが確定している

連想配列は簡単にコードを書くことはできますが、その反面好きな時に好きなようにデータを入れることができるため、どんなデータがその連想配列に入っているかが分かりにくくなってしまっています。 連想配列を使っている場合、今どんなデータが入っているかを確認するため、要所要所で var_dump を使うという方も多いのではないでしょうか。 その点ドメインモデルは、最初にクラスを作る段階で入れるデータを決めているため、どの時点であっても入りうるデータは確定しています。

IDEの補完が効く

ドメインモデルのようにプロパティやメソッドを通してデータを取得するようにすることで、IDEが補完を効かせてくれます。 連想配列だと文字列でキーを指定することになり、どうしてもtypoや勘違いによるミスが起こりえますが、ドメインモデルで書くことによってそのリスクを大幅に減らすことができます。

ドメインルールを入れることができる

例えば「タイトルは100文字以内でなくてはならない」といったルールがあったとしましょう。 記事データがいろいろなところで使われる場合は、連想配列を使っているといろいろな場所で下記の条件を書く必要が出てくるかもしれません。

// これを、コードのいろいろな場所で書く必要があるかも
if (100 < mb_strlen($article['title'])) {
    // エラー処理を書く
}

その場合、今後「タイトルは120文字以内でなくてはならない」というルールに変わった際は、この条件が書かれているすべての部分を探し出し、修正する必要があります。 エンジニアであれば一度は体験したという方も少なくないと思いますが、これは非常に大変な作業です。

一方ドメインモデルを使うと、モデルそのものにルールを書くことができます。


class Article {
    private $title;
    private $story;
    private $publishDate;
    
    public function __construct($title, $story, $publishDate) {

        // Articleモデルを作成するときにのみチェックすれば良い
        if (100 < mb_strlen($title)) {
            // エラー処理を書く
        }

        $this->title = $title;
        $this->story = $story;
        $this->publishDate = $publishDate;
    }

    public function getTitle() {
        return $this->title;
    }

    public function getStory() {
        return $this->story;
    }

    public function getPublishDate() {
        return $this->publishDate;
    }
}

このように、ドメインモデル生成時に必ずチェックするようにすれば、ルールを書く部分が1箇所だけで済み、仮にルールが変更になっても修正が非常に容易です。

最後に

以上のことから、多少最初書くのが面倒であったとしても、ドメインモデルを使うようにしたほうが利点が大きいのではないでしょうか。 またこれは、ドメイン駆動設計の概念でもあります。 今後新しく連想配列を使う機会があったら、ぜひドメインモデルで書いてみることをおすすめします。

DBのテーブル構造のアンチパターンと改善

みなさんこんにちは。 エキサイトでエンジニアをしているAです。

エキサイト内で過去に一部テーブル構造の見直しを行い、運用コストの効率化を行ったため今回はその一例をご紹介いたします。

最初にテーブル構造からしっかり考える

最初のテーブル構造は非常に大事です。今回は以下の3点を特に重視していきました。

  • テーブル構成は要件に合わせて基本的に細かく分ける
  • 後々拡張しやすい造りにする
  • テーブルを見るだけでなんのデータか把握しやすい形にする

上記は当たり前のことですが、ここを疎かにしてしまうと後々何の為に使うデータか分からなくなったり、データが増えてしまいデータの更新処理に時間がかかったりします。

後付けで謎テーブルを謎ロジックでJOINしたりで、とんでもなく複雑な運用になってしまいがちです。

テーブル定義は時間をかけて行っていったかどうかで運用の手間が全然変わります。

見切り発車で決めるのだけは絶対にやめましょう。

テーブル例

`example_table` (
id,
name,
profile_json,
snsA_profile_json,
snsB_profile_json,
url_information_json,
TypeA_json,
TypeB_json,
TypeC_json,
...
...
...
active_flag,
delete_flag)

実際に存在していたテーブルの一例です。 この場合、カラムが非常に長いため一つのテーブルに大量のカラムを作成せず、分けられる箇所は分けるようにしていきます。

問題点1 データの更新に無駄に手間がかかる

恐ろしい事にこのケースだとデータをSQLに投げる際に、jsonをカラムにそのまま突っ込んでいます。

そのため、既に登録したデータの中身の情報を変えたい時にデータを投げ直す事になり迂闊にデータを変えることができなくなります。

例えばID1のTypeAの情報を一部変更したいなどの要件があった場合、以下の操作をするしか変える手段はありません。

  • jsonの中身を把握してUPDATEをかける
  • データを再度入れ直して上書きする

この場合TypeAの情報を一部分変えるだけでも苦労します。これだけでもう地獄です。

解決策

今回はTypeAはTypeA_json用のデータをまとめたTypeAテーブルなど分けるように変更を加えました。

その後DBから受け取るAPI側でjsonに変換して送るようにする事で、TypeAJsonカラムに影響されることはなく TypeAの情報は単純なUPDATE文一つで更新できるようになりました。

問題点2 状態をカラムを持っている

今回のテーブルにはactive_flagというものが存在します。 active_flagとみたら誰もが「表示、非表示の状態を持つカラムなんだな」と思うかもしれません。

実際にこのテーブルのコメントには「アクティブフラグ」とだけ書いてありました。

パッと見0や1が書いてあるので1が表示で0が非表示なんだなーとなんとなくわかりそうです。

しばらくデータを眺めていると1や0の中に-1や-128という知らない数字が見えました。

どうやらこの表示フラグ、表示のためのフラグなのに4つのカオスな状態を持っているようです。 実際に使われていた箇所を追っていくと、

  • データが入ってきたばかりの状態 => 0
  • 管理画面で公開許可された状態 => 1
  • 管理画面で非許可にされた状態 => -1
  • 削除予定の状態 => -128

と言った形で使われていることがわかりました。

実装当時の状況はわかりませんが、恐らくはじめは1,0で表示管理されていた物が表示要件が新たに出てきたので付け足されていったのだと推測しました。

解決策

結局のところ0,1,-1,-128が要らないので、いっそ分けた方がやりやすいです。

active_flag自体はカーディナリティが低いので、example_tableのactive_flagで状態管理するのをやめました。

activeな状態のIDを下記のようにactiveテーブルに入れることで分かり辛さが解決します。

`example_active` (
id
)

activeにactiveな情報を持つIDだけ入れておけば、テーブルを見るだけでどれが今activeなのかは自明です。

さらに後から追加要件で変な状態を持たせられることもなくなります。

ちなみにアクティブかどうかと、非許可かどうか、削除するかどうかは要件がそもそも別なのでテーブルを分けるべきです。

どうしても値で状態を持たなければならなくなった場合は、当たり前の話ではありますがせめてどの値がどれを示すかのコメントはしっかり残しましょう。

その他、紹介し切れていない改善点などはまだまだありますが ご覧の方々には、最初のテーブル定義にはしっかり時間をかけるという認識を持っていただけたら、後々の運用もやりやすくなる筈なのでこれを機にテーブル構造には時間をかけて考えていただく切っ掛けになれば幸いです。

SQL Serverのdockerコンテナにバックアップ復元する方法(2020)

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

SQL Serverをローカル環境に用意するのにハマったことを記載します。

ユースケース

  1. SQL Serverを使ったローカル開発をしたい。
  2. test環境のデータを、ローカル環境に復元したい。

題材

docs.microsoft.com

1のみの場合、手順通りにやればSQL Serverを使ったローカル開発をすることできます。

しかし、2のtest環境のデータをローカル環境に復元するとき、エラーは出ることがあります。

github.com

理由は、docker-composeのvolumes mountの際、rootユーザになってしまうからです。

それを回避するためにdocker volumeを使いましょう。

docker volume --rm で明示的に削除しない限り、消えることはありません。

以下に、docker-compose.yamlの記載例を表示します。

入力例

docker-compose -f docker-compose-sqlserver.yml up -d

出力例

mcr.microsoft.com/mssql/server:2017-latest   "/opt/mssql/bin/nonr…"   4 days ago   Up 47 hours   0.0.0.0:1433->1433/tcp   tool_sqlserver_1

条件

  • testデータは、既存のtest用DBサーバーからエクスポートする

コード例

version: "3.7"

services:
  sqlserver:
    image: mcr.microsoft.com/mssql/server:2017-latest
    ports:
      - 1433:1433
    environment:
      ACCEPT_EULA: "Y"
      SA_PASSWORD: "abc%ABC%123"
    volumes:
      - "sqlserver-data:/var/opt/mssql/data"
      - "sqlserver-log:/var/opt/mssql/log"
      - "sqlserver-secrets:/var/opt/mssql/secrets"
volumes:
  sqlserver-data:
    driver: local
  sqlserver-log:
    driver: local
  sqlserver-secrets:
    driver: local