modelmapperの曖昧なマッピングを厳密なマッピングに

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

今回はModelMapperのよく使われるオプションについて、説明します。

以下の例は、userIdを別のモデルにマッピングします。 userIdからuserIdなのでもちろんテストは通ります。

@ExtendWith(MockitoExtension.class)
public class ModelMapperConfig{

    @Description("ModelMapperConfig")
    @Test
    void test() {
        final ModelMapper modelMapper = new ModelMapper();
        final TestModel testModel = new TestModel();
        testModel.setUserId("nakao");

        final Model map = modelMapper.map(testModel, Model.class);
        Assertions.assertEquals(
                "nakao",
                map.getUserId()
        );
    }

    @Data
    public static class Model{
        private String userId;
    }

    @Data
    public static class TestModel{
        private String userId;
    }
}

別のモデルにしてみましょう。

    @Data
    public static class Model{
        private long id;

        private String userId;
    }

実行します。

userIdからuseridはマッピングされるのですが、 idにもmappingされます!

ModelMapperのデフォルトマッピング設定は曖昧なマッピングだからです!

ModelMapper - Configuration

useridに関わりそうなプロパティを発見し、勝手にmappingします。怖いですね。。。

変数の型も、同一プロパティに合わせて勝手にmappingします。

@ExtendWith(MockitoExtension.class)
public class ModelMapperConfig{

    @Description("ModelMapperConfig")
    @Test
    void docomoMailReturnTrue() {
        final ModelMapper modelMapper = new ModelMapper();
        final TestModel testModel = new TestModel();
        testModel.setUserId(1);

        final Model map = modelMapper.map(testModel, Model.class);
        System.out.println(map.toString());
        Assertions.assertEquals(
                1L,
                map.getId()
        );
        Assertions.assertEquals(
                1L,
                map.getUserId()
        );
    }

    @Data
    public static class Model{
        private long id;

        private String userId;
    }

    @Data
    public static class TestModel{
        private int userId;
    }
}

上記の事象を防ぐために

        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        modelMapper.getConfiguration().setFullTypeMatchingRequired(true);

を設定しましょう。厳密なプロパティ名、厳密な型のmappingをします。

AWS上の本番DBから通信コストを抑えて大量データを開発DBに入れる

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

今回はAWS上で本番稼働中のRDSから開発用DBクラスター内に新しく作成したスキーマにデータを入れる必要があったため、その際に通信料金をできる限り最小限で抑える方法をご紹介します

料金について

  • インターネット経由で外からDBにアクセスすると料金がかかるので極力VPC内の同一リージョン間で行う

  • 異なるAZ間で通信した場合でも通信料金がかかるので同一AZから通信を行う(料金は微々たるものなので、大量のデータをやり取りする場合のみ考える必要があります)

aws.amazon.com

同一の AWS リージョンでのデータ転送 Amazon EC2Amazon RDS、Amazon Redshift、Amazon DynamoDB Accelerator (DAX)、Amazon ElastiCache インスタンス、Elastic Network Interface、または同じ AWS リージョン内のアベイラビリティーゾーンをまたいだ VPC ピアリング接続間で「受信 (イン)」/「送信 (アウト)」されるデータの転送料金は、各方向 0.01 USD/GB です。

使用構成

  • 本番環境RDS master1台 Read3台+オートスケール

  • 開発環境RDS master1台 Read1台

一時的な移行用DBを作成する

dumpの際はRDSに負荷がかかるため、本番DBのスナップショットから移行用の一時的なDBクラスターを作成しています

RDS → インスタンスを選択 → [スナップショット作成]

スナップショット→[スナップショットを復元]

※この方法だとスナップショット作成以降、本番DBに入ったデータは入らないので注意

Dumpを実行する開発サーバーを用意する

  • 本番環境DBからデータをDumpする際は、本番DBの対象インスタンスと同一のAZにする

  • Dumpしてきたデータを開発DBに入れる際は、対象インスタンスが実行環境と同一AZか確認する

※本番と開発DBの対象インスタンスのAZ異なった場合、DumpデータをS3に置いて開発DBと同じAZの実行環境でS3からDumpデータを取得してデータを入れるなどで対応

データの移行

1.本番DBからデータをDumpする

mysqldump -u [user] -h [本番DBのホスト名] -p [DB名]
  1. 開発DBにデータを入れる
cat [Dumpデータ.gz] | mysql -u [user] -h [開発DBのホスト名] -p -D [DB名]

片付け

  • 一時的に作った移行用DBをクラスターごと削除する

  • 開発環境からDumpデータを削除(S3に保存しておくのもアリかもしません)

終わりに

今回のように既存のDBクラスターを使う必要がない場合は、そのままスナップショットから作ったクラスターのサイズを調整するだけでよいです

また、最初に述べたように大量のデータを通信したり、頻繁にデータ通信が発生するなどの要件がなければAZを合わせる必要はなさそうなため、要件に合わせて変えていくと良いでしょう

第一回Radiotalk配信!!〜新卒と語り合おう〜

はじめまして!エンジニアとして新卒で入社した奥田です。

私は現在、社内の社外広報チームに所属しています。 その一環としてRadiotalkでのライブ配信することになりました! 記念すべき第一回は同期たちと最近の生活や仕事などざっくばらんに配信しました。 興味があれば是非ともRadiotalkをインストールし、アーカイブを覗いてみてください✨

今後も配信や、収録をして様々なコンテンツを届けていきます!

radiotalk.page.link

LogstashをAWS RDSに接続するときの注意点

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

DBからデータをどこかに同期する(例えば、DB内のデータをElasticsearchに同期する)場合、いろいろな方法がありますが、その一つにLogstashを使うというものがあります。 LogstashはElasticsearchと同じくElastic社が提供しているアプリケーションであり、Elastic社の説明によれば

無料かつオープンのサーバーサイドデータ処理パイプラインです。膨大な数のソースからデータを取り込み、変換して、お好みの格納庫(スタッシュ)に送信します。

https://www.elastic.co/jp/logstash

というものとなっています。

Elastic社が提供しているということでElasticsearchと相性がよく、使っている方もいるかと思いますが、今回は実体験をもとに、AWS RDSをソースとしたときのある注意点について説明します。

問題の発生

弊社のあるサービスでは、AWS RDSのデータをLogstashを使ってElasticsearchと同期しています。 最近、変更を加えるためにTest環境にLogstashをデプロイしたところ、大きな変更ではなかったにも関わらずエラーが発生し、正しく動かなくなってしまいました。

エラーをよく見てみるとRDSとの接続エラーのようでしたが、その時の変更点はRDSとのコネクションに関わる部分ではなかったため、どうやらコードの変更が原因ではないようです。 RDSと接続するために使用しているJDBCライブラリの更新なども行ってみましたが、特に改善は見られません。

色々調べた結果、ある記事を発見しました。

解決

stackoverflow.com

この記事によると、 useSSL オプションが悪さをしているとのこと。 本来はSSLでのコネクションでは正しく動かないのですが、デフォルトで使ってしまうので、オプションで useSSL=false をつけることで回避できる、とのことでした。

実際にこのオプションを付けることでコネクションができるようになりました。

最後に

今回はRDS側の設定もLogstash側のRDSとの接続設定も触っていないにもかかわらず突然コネクションができなくなったので、どこでどんな変更が起きた結果エラーになるようになったのかはわかりませんが、なにはともあれLogstashをRDSと接続させるにはSSLを使用しない設定にする必要があるようです。

TabBarの選択ボタンを中央寄せにするアニメーション付き無限スクロール化するTips

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

今回はTabBarをListviewで実装することを前提にお話ししていくことを前提としていきます。

FlutterでTabBarを無限スクロール化する方法はいくつかあり、ライブラリでいうと infinite_listviewindexed_list_view などがあります。 純粋にListviewでたくさんの要素を確保し、中央のoffsetをinitialOffsetで指定するという方法もあります。
今回で言いますと、ボタンをタップした際にそのボタンを中央へ移動するアニメーションを付与した上で無限スクロール化したいという要件です。

各バージョン

Flutter: 2.0.6
iOS: 14.5
Android: 11.0

使用ライブラリ

scroll_to_index: 2.0.0

実装方法

使用ライブラリで挙げたようにアニメーションに scroll_to_index を使用し、無限スクロールは素のListviewで実装します。

ListView.builder(
   controller: AutoScrollController(),
   scrollDirection: Axis.horizontal,
   itemCount: 999,
   itemBuilder: (_, index) {
     return AutoScrollTag(
        key: ValueKey(index),
        controller: AutoScrollController(),
        index: index,
        child: Button(),
      );
   },
),

表示の方はこのようになっています。initialoffsetを中央あたりで指定してあげることによりTabBarの見た目の方は完成です。 この後に、ボタンのタップ処理発火時に

controller.scrollToIndex(index, preferPosition: AutoScrollPosition.middle);

以上で中央へのスクロールアニメーションができます。

まとめ

いかがでしたでしょうか? 中央のOffsetをinitialOffsetで指定するとその端末でしか中央にならないので今後initialOffsetを動的に設定する記事も書けたらいいなと思っています。 元々タブ要素の三倍確保しoffsetを監視して、常に中央のタブ要素を表示するという方法を試みていました。 前に要素を追加した時などoffsetが変わらず無限スクロールを実装できませんでしたので他にも無限スクロールを実装する方法があるならばご教示していただけると幸いです。

Java9から導入されたMatcherのreplaceAllを使って文字列を置換する

f:id:excite-kazuki:20210605183312p:plain

はじめに

エキサイト株式会社 21卒 バックエンドエンジニアの山縣です。 PHPで書かれたAPIからSpringBoot / Javaでリビルドを進めていく上で、 独自のイメージタグからHTMLのimgタグに置換する処理をJavaで実装することになりました。 シンプルでスッキリとしたコードを書くことができたので共有します。

導入

下記図のように、ある文字列の中から正規表現に一致する文字列を使用して別の文字列に置換したいとします。 このとき、文字列.replaceAll(正規表現, 置換する文字列) では正規表現に一致した文字列の中身を考慮することができないため、意図した結果を得ることができません。 そのため、何らかの方法で正規表現に一致した文字列を取得した上で置換する必要があります。

f:id:excite-kazuki:20210601193055p:plain

今回扱うデータ

今回扱うデータは下記のとおりです。 独自のイメージタグがあり、イメージタグの中にパス、長さ、高さの3つの要素が含まれているとします。

@Data
public class ImageTag {
    private String src;
    private int width;
    private int height;

    public String toImg() {
        return String.format(
                "<img src=\"%s\" width=\"%d\" height=\"%d\"/>", 
                src, width, height
        );
    }
}

また、イメージタグの正規表現imageTagPatternとして扱い、 抽出したイメージタグからImageTagクラスのインスタンスを生成するメソッドをtoImageTagとして扱います。

// [IMAGE|src|width|height] に一致するような正規表現(省略)
Pattern imageTagPattern = Pattern.compile("...");

// [IMAGE|src|width|height]を受け取り、ImageTagのインスタンスを生成するメソッド
public ImageTag toImageTag(String imageTagStr) {
  /* src, width, heightをsplit()などを使用して抽出 */
  return new ImageTag()
            .setSrc(src)
            .setWidth(width)
            .setHeight(height);
}

結論

Java9から導入されたMatcherのreplaceAll(Function<MatchResult, String>)使用することで、正規表現に一致した文字列を置換することができます。 下記コードでは、e正規表現と一致したもの(MatchResult)を表し、返り値(String)が置換するものを表しています。 今回のケースでは、正規表現と一致した文字列のイメージタグからImageTagインスタンスを生成し、インスタンスからimgタグを作成しています。 (インスタンスを生成せずに、直接変換することもできます。)

何をしたいのかがひと目でわかり、とても見通しの良いコードに仕上げることができました👏

public String replace(String text) {
    return imageTagPattern
            .matcher(text)
            .replaceAll(e -> toImageTag(e.group()).toImg());
}

Streamを使用した他の実装方法

replaceAll(Function<MatchResult, String>)を使用せずに実装する方法についても紹介します。

イテレータを作成して繰り返し置換する方法

Java9から導入されたMatcherのresults()を使用することで、正規表現に一致した文字列をStreamで取得することができます。 イテレータを作成して引数で受け取ったtextに対して繰り返し置換を行うようにしてみました。 replaceAll(Function<MatchResult, String>)を使用したコードと比較すると、コードが長くなってしまいますが、 比較的わかりやすいコードになるのかなと思います。

public String replace1(String text) {
    final Iterator<String> imageTagIterator = imageTagPattern.matcher(text)
            .results()
            .map(e -> e.group())
            .iterator();

    while (imageTagIterator.hasNext()) {
        final String imageTagStr = imageTagIterator.next();
        final ImageTag imageTag = toImageTag(imageTagStr);
        text = text.replace(imageTagStr, imageTag.toImg());
    }
    return content;
}

AtomicReferenceを使用して繰り返し置換する方法

AtomicReferenceを使用することで、引数で受け取ったtextforEachの中で繰り返し置換することができます。 Javaでは、ラムダ式の中で外部の変数(この場合はtext)を書き換えることができないため、一工夫しないといけません。 どうしてもラムダ式の中で外部の変数を書き換えたいとき以外は、この方法を避けたほうがよいのではないかと思います。

public String replace2(String text) {
    final AtomicReference<String> reference = new AtomicReference<>();
    reference.set(text);

    imageTagPattern
            .matcher(reference)
            .results()
            .forEach(e -> {
                final String s1 = reference.get();
                final ImageTag imageTag = toImageTag(e.group());
                final String s2 = s1.replace(e.group(), i.toImg());
                reference.set(s2);
            });

    return reference.get();
}

まとめ

Java9から導入されたreplaceAll(Function<MatchResult, String>)を使用することで、シンプルな置換処理を記述することができるようになりました。 調べてみると、正規表現に一致した文字列を別の文字列に置換する記事が多くヒットしてしまい、なかなかたどり着けなかったです。 また、Java8では導入されていないため、現在プロダクトで使用しているバージョンと同じドキュメントを読むのが大事だなと感じました。

誰かのお役に立てれば幸いです!

AtomicIntegerを使ってみた

エキサイト株式会社 メディア開発のしばたにえんです。

さっそくですが、下記のコードをご覧ください

class CountTest {

    private int num = 0;

    @SneakyThrows
    void CountThreadNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    num++;
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(num);
    }
}
// 9818

スレッドを10個でそれぞれ1000回ずつ1を加算していくといった処理になりますが、 この結果は10000にはなりません。 intはアトミックではないためです。

概要

そもそもアトミックとは

コンピュータ上のプログラムの動作で、密接に関連する複数の処理が外部から一つの操作に見え、途中の状態を観測したり介入できない性質を、操作のアトミック性、不可分性などという。

要するに 複数のスレッドからのデータを書き込んでも最終的な値を保証しているというものです。

上記の問題を解決するのはAtomicIntegerです。 AtomicIntgerは多くのスレッドを同時に使用できるIntegerクラスです。

実際に使ってみます

class CountTest {

    // private int num = 0;
    private AtomicInteger atomicInteger = new AtomicInteger();

    @SneakyThrows
    void CountThreadAtomicIntegerNum() {
        int threadNum = 10;
        ExecutorService service = Executors.newFixedThreadPool(threadNum);
        for (int n = 0; n < threadNum; n++) {
            service.submit(() -> {
                for (int m = 0; m < 1000; m++) {
                    atomicInteger.incrementAndGet();
                }
            });
        }
        service.shutdown();
        service.awaitTermination(10, TimeUnit.SECONDS);
        System.out.println(atomicInteger.get());
    }
}
// 10000

期待されている値が取れました。

使い方

AtomicIntegerの使い方は主に二つあります
①カウンターとしての役割をアトミック性を保ちながら行う
②比較処理をアトミック性を保ちながら行う

① 1ずつ加算する場合

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// a:1
// b:2
// c:3
// d:4
// e:5
// f:6
// g:7
// h:8

初期値を入れて1ずつ加算する

void atomicIntegerIncrementAndGet() {
    final List<String> characters = List.of("d", "e", "f", "g", "h");
    final AtomicInteger atomicInteger = new AtomicInteger(3);
    characters.stream().forEach(
            charcter -> System.out.println(
                    String.format("%s:%s", charcter, atomicInteger.incrementAndGet())
            )
    );
}
// d:4
// e:5
// f:6
// g:7
// h:8

5ずつ加算する

void atomicIntegerGetAndAdd() {
    final AtomicInteger atomicInteger = new AtomicInteger();
    for (int n = 0; n < 3; n++) {
        System.out.println(atomicInteger.getAndAdd(5));
    }
}
// 0
// 5
// 10

② 比較

void atomicIntegerIncrementAndGetCompare() {
    final List<String> characters = List.of("a", "b", "c", "d", "e", "f", "g", "h");
    int targetNum = 3;
    final AtomicInteger atomicInteger = new AtomicInteger();
    characters.stream().forEach(
            charcter -> {
                if (atomicInteger.compareAndSet(targetNum, atomicInteger.incrementAndGet())) {
                    System.out.println(
                            String.format("%s:%s", charcter, atomicInteger.get())
                    );
                }
            }
    );
}
// c:3

まとめ

マルチスレッドの場合ちょっとしたループ内の加算処理でも上記のようなずれが起きてしまいます。 AtomicIntegerちゃんと使っていきましょう!