nginxの$time_localで出力される日時のタイミングはいつなのか

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

今回は、nginxのアクセスログで出力される日時が、どのタイミングのものなのかを説明します。

疑問

nginxは、 $time_local というフォーマットを使うことで、アクセスの日時をアクセスログに出力してくれます。 ただ、「アクセスの日時」とは具体的にいつなのでしょうか?

「アクセスを受け取った日時」でしょうか?それとも、「アクセスに対してレスポンスを返した日時」でしょうか?

というわけで今回は、それを検証しました。

検証

今回は、 nginx + Golang を使って検証します。 Golang には gin というHTTP web フレームワークを使っています。

コードの抜粋は以下です。

e := gin.Default()

e.GET("/ping", func(c *gin.Context) {
    c.String(http.StatusOK, "pong")
})

やっていることは単純で、 http://localhost/ping というアクセスが来たら、ステータスコード200で pong というレスポンスを返すというものです。 これを、nginx経由でアクセスするようにします。

通常アクセス検証

まずはそのまま、上記のコードを使用してみます。

e := gin.Default()

e.GET("/ping", func(c *gin.Context) {
    c.String(http.StatusOK, "pong")
})

11:59:00 にアクセスしてみました。

f:id:excite-takayuki-miura:20210621120436p:plain

世界標準時になっているので -9時間 ですが、アクセス日時と全く同じ日時が記録されているのがわかります。

sleepによるレスポンス時間増加検証

続いて、以下のように一旦sleepを入れて、レスポンスにかかる時間を延ばして検証してみます。

e := gin.Default()

e.GET("/ping", func(c *gin.Context) {
    // 5秒間sleepする
    time.Sleep(5 * time.Second)

    c.String(http.StatusOK, "pong")
})

13:14:50 にアクセスしてみました。

f:id:excite-takayuki-miura:20210621131523p:plain

世界標準時になっているので -9時間 ですが、アクセス日時から5秒後の時間が記録されているのがわかります。

結論

検証の結果、 $time_local は「アクセスを受け取った日時」ではなく「アクセスに対してレスポンスを返した日時」であることがわかりました。

最後に

特に、時間がかかる系の処理のアクセスログを見るときは、このあたりが正確にわかっていないとちゃんとログを読み解けない可能性があります。 気をつけましょう。

新卒デザイナーが選ぶ!おすすめフォントの話

f:id:excite_ny:20210813153706p:plain

こんにちは、新卒デザイナーの山﨑です! 今回はデザインをする上でよく使うフォントを紹介します。

フォントの話

フォント1つ変えるだけでもデザインの雰囲気がガラッと変わるので、デザイナーにとっては非常に大事な要素の1つです。

資料のフォントが妙なセレクトだと、そればかり気になってあまり集中できないこともあります😂 是非資料作りの参考にしていただければ嬉しいです!

今回の紹介はゴシック体・サンセリフで行きます!理由は私がゴシック体・サンセリフ体が好きだからです♪

こぶりなゴシック・游ゴシック

f:id:excite_ny:20210618183926p:plain

言わずと知れた王道中の王道…「こぶりなゴシック・游ゴシック」です!

こぶりなゴシックはモリサワフォントを購入することで使えますが、游ゴシックはMacのデバイスフォントとして使用されているので、Macユーザーは無料で使うことができます。

こぶりなゴシックと游ゴシックは形としてはほぼ同じで、こぶりなゴシックの方が少し柔らかい印象があります。

こぶりなが便利すぎて、全てにおいて「とりあえずこぶりな!」って感じでデザインしてしまいます…😲

Avenir・Helvetica

f:id:excite_ny:20210618185002p:plain

これも王道中の王道、「Avenir・Helvetica」です😄

こぶりな・游ゴシックとの相性も良いので一緒に使うのも良いと思います♪ 使用用途としては、Avenirはタイトル向きでHelveticaは本文向きですね。

Helveticaは游ゴシックと同じくMacに搭載されているので是非組み合わせて使ってみてください!

AvenirとHelveticaも本当に便利なフォントで、割と何にでも合うんですよね…めっちゃレギュラーで使っちゃいます。

おまけ

f:id:excite_ny:20210813153750p:plain

ちなみに今回の記事の表紙にはこれらのフォントが使われています!「ヒラギノUD角ゴ」も好きなんですよね…

NHKの人気番組「デザインあ」で使用されているフォントです!

最後に

少ないですがとりあえず以上になります。皆さんのフォント選びの参考になれたら幸いです!

エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!長期インターンも歓迎していますので、興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

JavaでオブジェクトのListから、MapやSetをラムダ式でさくっと作る

エキサイトのエンジニア藤沼です。 JavaでオブジェクトのListから、MapやSetをラムダ式でさくっと作る方法覚書です。

やりたい事。

  1. 下記のオブジェクトのListからidだけを取り出してSetを作る。
  2. 下記のオブジェクトのListからidをキーにした、Map<Long, SampleModel>を作る。
@Data
@Accessors(chain = true)
public class SampleModel {

    /**
     * id
     */
    @NotNull
    @Positive
    private Long id;

    /**
     * タイトル
     */
    @NotNull
    private String title;

    /**
     * 説明文
     */
    @NotNull
    private String description;

}

List<SampleModel> sampleList = List.of(
    new SampleModel()
        .setId(1L)
        .setTitle("タイトル1")
        .setDescription("説明文1"),
    new SampleModel()
        .setId(2L)
        .setTitle("タイトル2")
        .setDescription("説明文2"),
    new SampleModel()
        .setId(3L)
        .setTitle("タイトル3")
        .setDescription("説明文2")
);

forEachを使う 例

foreachを使う場合は、 最終的に入れ込む変数を外側に定義しないといけません。

Set

Set<Long> idList = new HashSet<>();
sampleList.forEach(sample -> idList.add(sample.getId()));

Map

HashMap<Long, SampleModel> sampleMap = new HashMap();
sampleList.forEach(
    sample -> sampleMap.put(sample.getId(), sample)
);

ラムダ式バージョン

ラムダ式でforEach使わずに、簡単にSetやMapが作成できます。この場合、MapやSetの変数定義をせずに、初期化と同時にデータを入れられるのがメリットです。

Set

Set<Long> idList = sampleList.stream().map(
    sample -> sample.getId()
).collect(Collectors.toSet());

Map

Map<Long, SampleModel> sampleMap = sampleList.stream().collect(
    Collectors.toMap(
        sample -> sample.getId(),
        sample -> sample
     )
);

ModelMapperのcustom mappingについて

エキサイト株式会社 メディアサービスエンジニアの中尾です。 前回に続いてModelMapperのちょっとした内容を説明します。 現場ではほとんど使うことはないと思うので、「こんなこともできるんだ」ぐらいでお願いします。

コードは以下になります。

@ExtendWith(MockitoExtension.class)
public class ModelMapperTest {

    @Test
    public void testModelMapper() {
        final ModelMapper modelMapper = new ModelMapper();
        modelMapper.addMappings(new PropertyMap<TestModel, Model>() {
            @Override
            protected void configure() {
                using(toUserId).map(source, destination.getUserId());
                using(toFirstname).map(source, destination.getFirstname());
                using(toLastname).map(source, destination.getLastname());
            }
        });

        final TestModel testModel = new TestModel();
        testModel.setUserId("10000");
        testModel.setUserName("naka sho");

        final Model map = modelMapper.map(testModel, Model.class);
        Assertions.assertEquals(
                10000,
                map.getUserId()
        );
        Assertions.assertEquals(
                "naka",
                map.getFirstname()
        );
        Assertions.assertEquals(
                "sho",
                map.getLastname()
        );
    }

    private Converter<TestModel, Integer> toUserId = context ->
            Integer.valueOf(context.getSource().getUserId());

    private Converter<TestModel, String> toFirstname = context ->
    {
        final String[] s = context.getSource().getUserName().split(" ");
        return s[0];
    };

    private Converter<TestModel, String> toLastname = context ->
    {
        final String[] s = context.getSource().getUserName().split(" ");
        return s[1];
    };

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

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

解説

addMappingsを使って、PropertyMapで元のモデルと先のモデルの設定をします。

configureをOverrideして、usingを使ってConverterを各プロパティごとに設定してください。

設定していない場合はModelMapperがよしなにmappingします。

mapping処理をModelMapper内に内包することができますが、SpringBootでbean化している場合はシングルトンになっておりますので予期せぬところで障害が発生するかもしれません。

ModelMapperのbeanを大量に作ることで避けられるかもしれませんが、そもそもmappingをModelMapper頼りにしてもいいのでしょうか?

現場と相談して使ってください。

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

エキサイト株式会社の高野です。
前回 TabBarの選択ボタンを中央寄せにするアニメーション付き無限スクロール化するTips を書いたのですが、また別の方法で実装しましたのでその方法を紹介します。
(前回と同様にTabBarをListViewで実装する際の一例です。)

各バージョン

Flutter: 2.0.6
iOS: 14.5
Android: 11.0

使用ライブラリ

scrollable_positioned_list

前回の実装

前回の実装ではAndroidの各種端末とiPhoneの指定すべきinitialOffsetが違うのでListViewが生成途中に中央に寄せるメソッドが発火していたので以下のようになっていました。
f:id:exRyusei1026:20210617163231g:plain

修正後実装方法

まず以下のようにControllerのインスタンスを生成します。今回はListenerを使用していないので必要な場合は同様に生成してください

// Controllerの生成
final ItemScrollController itemScrollController = ItemScrollController();
// Listenerの生成
final ItemPositionsListener itemPositionsListener = ItemPositionsListener.create();

そうしましたら、以下のようにbuilderを用いてTabの生成を記述します。

ScrollablePositionedList.builder(
     itemScrollController: scrollController,
     initialScrollIndex: (allTabQuantity / 2) + (allTabQuantity % 2),
     initialAlignment: alignment,
     scrollDirection: Axis.horizontal,
     itemCount: allTabQuantity,
     itemBuilder: (_, index) {
         return Button();
     },
),

initialScrollIndex で中央の要素を指定し、initialAlignment で指定した要素の位置を指定することができます。
alignmentで指定する際にコメントで

  /// * 0 aligns the left edge of the item with the left edge of the view
  /// * 1 aligns the left edge of the item with the right edge of the view.
  /// * 0.5 aligns the left edge of the item with the center of the view.

このように書かれています。0.5を指定すると要素の左端が中央に揃えられるとのことですが、自分の期待しているのは要素の中央を画面の中央に揃えて欲しいのです。
なのでalignmentを中央に来るように指定しなければなりません。
そのために MediaQuery.of(context).size.width とボタンサイズを用いて画面のWidthに対するボタンの半分のWidthの割合を導出していきます。式としては、

halfButtonRatio = ButtonWidth / 2 / MediaQuery.of(context).size.width;

以上のようになります。
この halfButtonRatio を0.5から引くことによってボタンの中央が画面の中央に来るalignmentを導出することができました。
また、タップイベントは

itemScrollController.scrollTo(
  index: index,
  duration: duration,
  alignment: 0.5 - halfButtonRatio,
);

以上でタップの際に中央にアニメーションをすることができます。
f:id:exRyusei1026:20210617163320g:plain

まとめ

一部の例としてTabBarを無限スクロールにする方法でした。
ライブラリで良さそうなものや別の実装方法がありましたらコメントしていただけると幸いです。

現在、エキサイト株式会社では採用に絶賛力を入れています。
インターンでは以下のものが公開されています。
▼入門コース
https://www.wantedly.com/projects/630117
▼実戦コース
https://www.wantedly.com/projects/644989

よければお気軽にご応募ください。

Android 12での通話に関する通知アクション

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

今回はAndroid 12から追加となった、新しい通知スタイルの種類についてお話しします。

※ 今回紹介する内容は Android 12 Beta におけるものです。正式版では仕様が変わっている可能性もあるため詳細については公式のドキュメントを参照してください。

今までの通知アクション

Androidアプリでは通知に対してアクションを設定することができます。

例えば通話の着信に対して応答するなどです。

しかし、基本的にはアクションの見た目をカスタマイズすることはできません。

コード上ではアイコンを設定することもできますが、実際にはAndroid 7.0以降の場合には表示されません。

Android Developers Blog: Notifications in Android N

You’ll note that the icons are not present in the new notifications; instead more room is provided for the labels themselves in the constrained space of the notification shade. However, the notification action icons are still required and continue to be used on older versions of Android and on devices such as Android Wear.

そのため、下記のスクリーンショットのようにぱっと見ただけではどちらが期待するアクションなのか分かりにくいような見た目になってしまいます。

f:id:katsuhiro-ito:20210615184356p:plain

コードはこちらです。

val notification = NotificationCompat.Builder(this, "notification_channel")
    .setSmallIcon(R.drawable.ic_launcher_foreground)
    .setContentText("着信中")
    .addAction(
        NotificationCompat.Action.Builder(
            null,
            "応答する",
            createDeclineIntent(),
        ).build()
    )
    .addAction(
        NotificationCompat.Action.Builder(
            null,
            "拒否する",
            createAnswerIntent(),
        ).build()
    )
    .build()

private fun createDeclineIntent(): PendingIntent {
    TODO("Not yet implemented")
}
private fun createAnswerIntent(): PendingIntent {
    TODO("Not yet implemented")
}

これからの通知アクション

Android 12では新たに通話に関する通知のスタイルが追加されました。

Features and APIs Overview  |  Android 12 Beta  |  Android Developers

こちらを利用すると下記のような見た目になります。

f:id:katsuhiro-ito:20210616102007p:plain

コードはこちらです。 以下では実装のポイントについて説明していきます。

// ①
val person = Person.Builder()
    .setName("太郎")
    .build()

val notification = Notification.Builder(this, "notification_channel")
    .setSmallIcon(R.drawable.ic_launcher_foreground)
    .setStyle(
        // ②
        Notification.CallStyle.forIncomingCall(
            person,
            createDeclineIntent(),
            createAnswerIntent()
        )
    )
    .build()

① 通話相手の設定

Personクラスを用いて、通話相手の情報を設定します。

名前の設定は必須で、オプションとしてアイコンや連絡帳のURIを設定することも可能です。

Person.Builder  |  Android Developers

② 通話用の通知スタイルの設定

通話用の通知スタイルであるNotification.CallStyleはファクトリメソッドから生成します。

通話中の通知スタイルなども使えますが、今回は着信通知を実装するのでforIncomingCallを使います。

①で用意したPersonクラスを最初の引数に渡し、応答時と拒否時に呼び出すPendingIntentも引数に渡します。

実装としてはこれだけで、あとは自動でアクションボタンを用意してくれます。

Notification.CallStyle  |  Android Developers

カスタマイズについて

現状では通話用の通知スタイルにより追加されるアクションボタンはカスタマイズができません。

そのため、文言やアイコンをアプリのブランドに合わせて変更するということができなくなっています。

まだベータ版ですし、今後そういった機能が追加されていくのかもしれません。

まとめ

AndroidはOSのバージョンアップごとにカスタマイズの制約が厳しくなってきている傾向があり、通知も例外ではありません。

その中で公式が表現を広げるための機能を追加してくれることはありがたいことです。

通知というのはAndroidの中でもシステムよりの機能なので、ある程度の統一感があった方がユーザも混乱せずに安心して使うことができます。

統一された表現の中でも、アプリごとの特色を可能な限りで出していけたらいいですね。

MySQL5.6におけるdatetimeの挙動を実際に試してみた

はじめに

MySQL5.6におけるdatetimeの挙動で???ってなったので、仕様を確認しつつ、実際に挙動を確かめてみました。

datetimeの仕様

デフォルト値に関しては、ドキュメントにこのように記載されています。

DATETIME は、NOT NULL 属性で定義されていないかぎり (この場合、デフォルトは 0 です)、デフォルトは NULL です。

SELECTに関しては、ドキュメントにこのように記載されています。

NOT NULL として宣言された DATE および DATETIME カラムでは、次のようなステートメントを使用することで、特殊な日付 '0000-00-00' を検索できます。
SELECT * FROM tbl_name WHERE date_column IS NULL

datetimeの挙動

先程の仕様を実際にテーブルを作って、データを入れて確かめてみます。

datetimeがNOT NULLでデフォルト値なしの場合

CREATE TABLE `datetime_not_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_not_null` (`id`)
VALUES(1);
// created_atには0000-00-00 00:00:00が入ります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(2, NULL);
// エラーになります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

SELECT * FROM datetime_not_null WHERE created_at IS NULL
// idが1, 3, 4のデータを取得できます

datetimeがNOT NULLでデフォルト値ありの場合

CREATE TABLE `datetime_not_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime NOT NULL ON UPDATE CURRENT_TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_not_null` (`id`)
VALUES(1);
// created_atにはデフォルト値の現在時刻が入ります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(2, NULL);
// エラーになります

INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_not_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

datetimeがNULL許容で、デフォルト値なしの場合

CREATE TABLE `datetime_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_null` (`id`)
VALUES(1);
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(2, NULL);
// created_atにはNULLが入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

datetimeがNULL許容で、デフォルト値ありの場合

CREATE TABLE `datetime_null` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `created_at` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

INSERT INTO `datetime_null` (`id`)
VALUES(1);
// created_atにはデフォルト値の現在時刻が入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(2, NULL);
// created_atにはNULLが入ります

INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(3, '');
INSERT INTO `datetime_null` (`id`, `created_at`)
VALUES(4, 'aaa');
// created_atには0000-00-00 00:00:00が入ります
// 「Data truncated for column 'created_at' at row 1」という警告が出ます

おわりに

MySQLのdatetimeに 0000-00-00 を入れると、思わぬ動作をする可能性があるので、注意が必要です。
また、この機能はSQL標準ではなく、MySQL独自のもので、DB移行時に移行先のDBが 0000-00-00 に対応していなかったり、ORMがデフォルトで対応していなかったりして面倒なのでMySQLがdatetimeでサポートしている値(1000-01-01 00:00:00 ~ 9999-12-31 23:59:59)をデフォルト値として入れる、モードの設定で 0000-00-00 を入れらないようにするなどしたほうがいいと思います。

MyBatisによるコード自動生成で、Javaの予約語を回避する方法

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

JavaにはMyBatisというライブラリがあり、それを使うことでDBとの接続用コードを自動で生成することができます。 DBとのやり取りをするアプリケーションにとっては非常に便利な機能であり、使っている方も多いと思うのですが、この「自動」というところに落とし穴があります。

例えば、Javaでは予約語となっている package というカラム名が存在する時、どうすればよいのでしょうか?

今回は、テーブル内でJava予約語が使われている場合に、いかにそれを回避してMyBatisによる自動コード生成をうまく機能させるかを説明します。

MyBatisとは

MyBatisは、いわゆるORマッパーの一つで、これを使用することで簡単にアプリケーションをDBと接続できるようになります。 あらかじめ使用するテーブルを一通り揃えておき、かつMyBatisの設定ファイル( generatorConfig.xml )を用意することで、そのテーブル構成をもとに接続用のコードを自動生成してくれるので、エンジニアとしては生成されたコードを使って簡単にテーブル操作ができるようになるという代物です。

多くの場合は生成するだけですぐ使えるようになるのですが、テーブルのカラムにJava予約語package など)が使用されていると問題が発生します。

カラム名等に予約語が入っていた場合

あるテーブルに、 package というカラムが存在しているとしましょう。 その場合でも、コードの自動生成自体はエラーが発生することはなく、問題なく生成されます。

問題が起きるのは実際にコードを実行したときです。 packageJava予約語であり、あらかじめ指定された使い方しかできないのですが、自動生成コードでは package をプロパティ名等として使用するようになっているため、そこでエラーが起きてしまうのです。

では、どのようにこの問題を回避すればよいのでしょうか?

解決方法

MyBatisの設定ファイル( generatorConfig.xml )にはいろいろな設定項目があるのですが、その一つに columnRenamingRule というものがあります。 これは名前の通り、カラム名をリネームしたい時に、そのリネームするルールを定めるというものです。

例えば、 package というカラム名をコード内では packageCode として使用したい場合は、以下のように記述することで実現することができます。

...
<generatorConfiguration>
    <context ...>
            <table....>
                <columnRenamingRule searchString="^package$" replaceString="package_code" />
            </table>
    </context>
</generatorConfiguration>

searchString に変換したい文字列を、 replaceString にどのように変換したいかの文字列を入れることで、コード生成時に自動的に変換後の文字列で生成してくれます。

注意点として、

  1. searchString には正規表現を入れる必要があるので、例えばもし packageNamepackageCodeName にしたくないのであれば、始端・終端を定める必要がある
  2. replaceString について、スネークケースの文字列をコード生成時に自動でキャメルケースにするので、 package_codeにしないとコード上では packageCode になってくれない

があります。

終わりに

最初からJavaをアプリケーションとして使うことが前提であればカラム名等にJava予約語を使うことはあまりないと思いますが、もともと別の言語のアプリケーションで使っていたDBをJavaで使うようになったときは、このような状況は起こり得ると思います。 幸いにもMyBatisはその状況を考えてくれているので、ぜひ使っていきましょう。

statusBarの表示非表示ハンドリング

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

今回はFlutterを書いていく中でiOSでstatusBarが表示されない(消えてる)問題の解決に当たっていたのでその辺りについて書いていこうと思います。

環境

iOS: 14.5 Flutter: 2.0.6 Android Studio: 4.1.3

解決方法

先にXcodeの方でProjectの設定を確認しておきます。 XcodeでProjectをクリック→TARGETSをクリック→Deployment Info こちらを確認して Status Bar StyleHide status barチェックボックスが外れていることを確認します。 外れていれば次にコードを紹介します。

import 'package:flutter/services.dart';

こちらをインポートし、以下のメソッドを使用していきます。

SystemChrome.setEnabledSystemUIOverlays([]);

こちらを使用することにより、statusBarを非表示にすることができます。 ですが、このまま使うとAndroidの方にも影響が出てしまうので[]のなかに SystemUiOverlay.bottom を追加します。

SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.bottom]);

表示にする場合は、さらに SystemUiOverlay.top を追加してあげることにより表示することができます。

SystemChrome.setEnabledSystemUIOverlays([SystemUiOverlay.top, SystemUiOverlay.bottom]);

自分の好きなタイミングで発火しましょう。

まとめ

これで柔軟にstatusBarの表示非表示をハンドリングできます。 他にもやる方法があるよという部分や疑問に思ったところがあればコメントしていただけると幸いです。

effective java の 防御的コピーの話を自分なりに実装してみた

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

effective java を最近読んでいて、気になった部分を自分でコーディングして理解を深めようと思っています。 その一部を紹介します。

public class Main {

    public static void main(String[] args) {

        System.out.println("元のクラス");
        Counter counter = new Counter(100);

        System.out.println("変更可能なクラス");
        MutableCounter mutableCounter = new MutableCounter(counter);
        System.out.println("初期値 : " + mutableCounter.getCounter().get());

        counter.set(10);
        System.out.println("10で更新される : " + mutableCounter.getCounter().get());
        System.out.println("理由はcounterとmutableCounterが等しいため");
        System.out.println(counter.hashCode() + " = " + mutableCounter.getCounter().hashCode());
        System.out.println("trueになる : " + counter.equals(mutableCounter.getCounter()));

        System.out.println();
        System.out.println("不完全な不変クラス");
        counter = new Counter(100);
        BlockCounter blockCounter = new BlockCounter(counter);

        counter.set(10);
        System.out.println("10で更新されない : " + blockCounter.getCounter().get());
        System.out.println("理由はcounterとblockCounterが等しくないため");
        System.out.println(counter.hashCode() + " = " + blockCounter.getCounter().hashCode());
        System.out.println("falseになる : " + counter.equals(blockCounter.getCounter()));
        blockCounter.getCounter().set(20);
        System.out.println("blockCounter.getCounter() から counterを参照できるため、更新することができる : " + blockCounter.getCounter().get());
        System.out.println("当たり前だけど一致する");
        System.out.println(blockCounter.getCounter().hashCode() + " = " + blockCounter.getCounter().hashCode());
        System.out.println("trueになる : " + blockCounter.getCounter().equals(blockCounter.getCounter()));
        System.out.println();

        System.out.println("完全な不変クラス");
        counter = new Counter(100);
        ImmutableCounter immutableCounter = new ImmutableCounter(counter);

        System.out.println(immutableCounter.getCounter().get());
        counter.set(10);
        System.out.println("10で更新できない : " + immutableCounter.getCounter().get());
        immutableCounter.getCounter().set(20);
        System.out.println("20で更新できない : " + immutableCounter.getCounter().get());

        System.out.println("immutableCounter.get()するたびに新しいオブジェクトが作成されるため、ハッシュコードが毎回変わる");
        System.out.println(immutableCounter.getCounter().hashCode()
                + " != " + immutableCounter.getCounter().hashCode()
                + " != " + immutableCounter.getCounter().hashCode());
    }
}

setterとgetterのみを持つクラスです。 数値を設定できます。

public class Counter {
    private int count;

    public Counter(int count){
        this.count = count;
    }

    public int get(){
        return this.count;
    }

    public void set(int i){
        this.count = i;
    }
}

上記のカウンタークラスを使うMutableCounterクラスです。 変更可能なクラスです。

public class MutableCounter {

    private final Counter counter;

    public MutableCounter(Counter sampleCounter){
        this.counter = sampleCounter;
    }

    public Counter getCounter(){
        return this.counter;
    }
}

上記のカウンタークラスを使うBlockCounterクラスです。 少しだけ防御力があがっています。

public class BlockCounter {
    private final Counter counter;

    public BlockCounter(Counter sampleCounter){
        this.counter = new Counter(sampleCounter.get());
    }

    public Counter getCounter(){
        return this.counter;
    }
}

上記のカウンタークラスを使う ImmutableCounterクラスです。 完全に防御されているクラスです。

public class ImmutableCounter {
    private final Counter counter;

    public ImmutableCounter(Counter sampleCounter){
        this.counter = new Counter(sampleCounter.get());
    }

    public Counter getCounter(){
        return new Counter(this.counter.get());
    }
}

実行結果です。

元のクラス
変更可能なクラス
初期値 : 100
10で更新される : 10
理由はcounterとmutableCounterが等しいため
951007336 = 951007336
trueになる : true

不完全な不変クラス
10で更新されない : 100
理由はcounterとblockCounterが等しくないため
834600351 = 471910020
falseになる : false
blockCounter.getCounter() から counterを参照できるため、更新することができる : 20
当たり前だけど一致する
471910020 = 471910020
trueになる : true

完全な不変クラス
100
10で更新できない : 100
20で更新できない : 100
immutableCounter.get()するたびに新しいオブジェクトが作成されるため、ハッシュコードが毎回変わる
1418481495 != 303563356 != 135721597

effective javaに書いている通り、不変クラスが作ることができました。 不変クラスを作れるとかっこいいですね!!! かっこいいけど、柔軟性も欲しい気がするのが私の気持ちです。

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が変わらず無限スクロールを実装できませんでしたので他にも無限スクロールを実装する方法があるならばご教示していただけると幸いです。