AWS Elasticsearchのバージョンを上げたらエラーが頻発するようになった話

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

今回は、最近実際に私の身に起きた非常に困った話、AWS Elasticsearchエラー問題について説明していきます。

なおこの件は、とりあえず現状はエラーは収まっているものの、100%原因が判明したとは言い切れないものとなっています。 あらかじめご了承ください。

始まり

私が携わっているサービスでは、コスト削減のためにElasticsearchに対してReserved Instanceを購入しています。 Reserved Instanceは、簡単に言ってしまえば一定期間の料金を前払いする代わりに割引してもらう、というものです。

先日、以前購入したReserved Instanceの前払い期間が終了したのですが、その頃ちょうど 6g系インスタンス が発表されました。

6g系インスタンスは、AWSからの情報によれば、

お客様は、現世代 (M5、C5、R5) の対応する x86 ベースのインスタンスと比較して、最大 38% 向上したインデックス作成スループット、50% 削減されたインデックス作成レイテンシー、30% 向上したクエリパフォーマンスの恩恵を受けることができます。

Amazon Elasticsearch Service Graviton2 インスタンスでは、前世代のインスタンスに比べて費用対効果が最大 44% 向上しています。

とのことなので、これを使わない手はありません。

6g系インスタンスは使用できるバージョンが一定以上のものでないといけないので、テスト環境で検証した後に本番環境のバージョンをアップし、その後無事6g系のインスタンスタイプに変更が完了しました。

これでコストも下がるしパフォーマンスも上がるしいい事づくし!と思っていました。

それがそう簡単なことではないとわかったのは、それからしばらく経ったあとのことです。

問題発生

ある日突然、サービスに対するリクエストがエラーになっているという通知が届きました。 急いで調査してみると、なんとElasticsearchからのレスポンスが不安定になっているようです。

Elasticsearchのインスタンスタイプの増強やm5系への変更、不要なリクエストの削除による負荷低減等色々なことを試してみましたが、それでも断続的にエラーが起きる状態が続きました。

問題の解決?

色々と試していく中で、Elasticsearchへのアクセスを最初に受け付けるElasticsearch用APIに問題があるのでは、という考えから、そのAPI(dockerコンテナとして運用しています)の1コンテナあたりのリクエスト数を減らしてスケールアウトするようにしてみたところ、エラーが起きなくなりました。

とはいえそれまではエラーは出ていなかったので、エラー原因の可能性としてはAPI自体の問題というよりも、

  1. バージョンを上げたことによるElasticsearch側の何かしらの処理の変化
  2. 実は色々あってElasticseach自体を一度立て直したが、その際に過去に立ち上げたElasticsearchのインスタンスと何かしらの設定が変わってしまっており、処理が変化

したことによってAPIの1コンテナあたりのリクエスト数の上限が変化した、などが考えられますが、正直に言うと原因が完全に解明できたとは言えず、お手上げ状態です。

最後に

迂闊なバージョンやインスタンスタイプの変更などは、一見問題なさそうでも後々問題を引き起こす可能性がある、ということを身にしみて感じました。 どうすれば防げるかというとなかなか難しい話ですが、心構えだけは持っておくとなにかあった時に冷静に対処できるかもしれません。

みなさんも、バージョンアップ等をするときは不測の事態にくれぐれも注意しましょう。

Jetpack Composeでネットワーク上の画像をUIに表示する

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

今回はJetpack ComposeのUIで、URLから画像を表示する方法についてお話します。

現在、画像の読み込みに関しての機能はComposeには実装されていませんが、こういった「必要とされているがまだ利用することはできない機能」をサポートするライブラリ群としてAccompanistというものがあります。

こちらに画像表示のためのライブラリも存在しているので、今回はこれを利用していこうと思います。

※ 今回紹介する機能はいずれJetpack Compose本体に実装されることが想定されます。サポート状況については公式のドキュメントを参照してください。

使用するライブラリ

Composeで画像を表示するためのAccompanistライブラリとして、下記の2種類が存在します。

  • Coil
  • Glide

Glide

GlideのCompose用のライブラリです。 ただし、特に理由がない場合には後述するCoilを利用することが推奨されています。

Unless you have a specific requirement to use Glide, consider using Coil instead.

https://google.github.io/accompanist/glide/

Coil

CoilのCompose用のライブラリです。 Glideと比較すると、Coroutineのサポートが強くComposeと相性が良いという理由でこちらの利用が推奨されています。

https://google.github.io/accompanist/coil/

実装

URLから画像を読み込んでImageに表示するだけであれば、実装はとても簡単です。

// Coin
Image(
    painter = rememberCoilPainter("画像のURL"),
    contentDescription = null
)

// Glide
Image(
    painter = rememberGlidePainter("画像のURL"),
    contentDescription = null
)

他にも画像の加工やアニメーションなど、様々な機能がサポートされています。

公式のドキュメントを参考にしていろいろと試してみるとよいでしょう。

最後に

既存のView実装でできていたことはComposeでもできるようになっていくでしょう。

Composeはまだベータ版であり足りていない機能も多いですが、7月に安定版がリリースされる予定ですし徐々に機能も拡充されていくと思います。

扱いやすい面も多いため、Composeのこれからの発展が楽しみです。

WebPush機能を使ってみる

はじめましてtaanatsuです よろしくお願いいたします。

初記事ということで、今回はWebPush機能を作っていこうと思います。
それでは、やっていきましょうか。

流れ

  1. Push通知を許可するかの確認用リンク作成
  2. manifest.jsonの作成
  3. サービスワーカー用のJavaScriptを作成
  4. 公開鍵と秘密鍵を使ってサーバーキーを作成
  5. WebサーバからPush通知を送信できるようにする
  6. Push通知を無効化する

以上です

Push通知を許可するかの確認用リンク作成

まずはPush通知を受信する、ブラウザ用のコードを書いていきます。
以下のようなHTMLを作成します。

index.htmlとして保存します。

<!DOCTYPE html>
<html lang="ja">
<head>
   <title>WebPushテスト</title>

   <meta charset='utf-8'>
   <meta name='viewport' content='width=device-width,initial-scale=1'>

   <meta name="apple-mobile-web-app-capable" content="yes">
   <meta name="apple-mobile-web-app-status-bar-style" content="black">
   <meta name="apple-mobile-web-app-title" content="WebPusher">
   <link rel="apple-touch-icon" href="icon-152x152.png">

   <!-- ウェブアプリマニフェストの読み込み -->
   <link rel="manifest" href="manifest.json">

   <link rel='icon' type='image/png' href='favicon.png'>

    <script defer src='service-worker.js'></script>
    <script src='webpush.js'></script>
</head>

<body>

    <a href="javascript:allowWebPush()">WebPushを許可する</a>

</body>
</html>

manifest.jsonの作成

こちらも簡易的ではありますが、作成します。

{
    "name": "example webpush",
    "short_name": "webpush",
    "theme_color": "#44518d",
    "background_color": "#2e3659",
    "display": "standalone",
    "scope": "/",
    "start_url": "/"
}

サービスワーカー用のJavaScriptを作成

ここからが本番です。
サービスワーカーを利用して、プッシュ通知の機能をつけていきます。

以下のファイルをservice-worker.jsとして保存します。

/**
 * プッシュ通知を受け取ったときのイベント
 */
self.addEventListener('push', function (event) {
    const title = 'Push通知テスト';
    const options = {
        body: event.data.text(),  // サーバーからのメッセージ
        tag: title,               // タイトル
        icon: 'icon-512x512.png', // アイコン
        badge: 'icon-512x512.png' // アイコン
    };

    event.waitUntil(self.registration.showNotification(title, options));
});


/**
 * プッシュ通知をクリックしたときのイベント
 */
self.addEventListener('notificationclick', function (event) {
    event.notification.close();

    event.waitUntil(
        // push通知がクリックされたら開くページ
        clients.openWindow('https://localhost:8080')
    );
});

公開鍵と秘密鍵を使ってサーバーキーを作成

以下のサイト様で発行できます。
Public KeyPrivate Keyをコピペで持ってきます。

https://web-push-codelab.glitch.me/

これらの情報を用いてサーバーキーを作成します。
以下のコードをwebpush.jsとして保存します。
また、3行目のPUBLIC_KEYに上記サイトで取得したPublic Keyを当て込めてください。

/**
 * 共通変数
 */
const PUBLIC_KEY = 'ここに取得した「Public Key」を書く';


/**
 * サービスワーカーの登録
 */
 self.addEventListener('load', async () => {
    if ('serviceWorker' in navigator) {
        window.sw = await navigator.serviceWorker.register('/service-worker.js', {scope: '/'});
    }
});


/**
 * WebPushを許可する仕組み
 */
async function allowWebPush() {
    if ('Notification' in window) {
        let permission = Notification.permission;

        if (permission === 'denied') {
            alert('Push通知が拒否されているようです。ブラウザの設定からPush通知を有効化してください');
            return false;
        }

        if (permission === 'granted') {
            alert('すでにWebPushを許可済みです');
            return false;
        }
    }
    // 取得したPublicKeyを「UInt8Array」形式に変換する
    const applicationServerKey = urlB64ToUint8Array(PUBLIC_KEY);

    // push managerにサーバーキーを渡し、トークンを取得
    let subscription = undefined;
    try {
        subscription = await window.sw.pushManager.subscribe({
            userVisibleOnly: true,
            applicationServerKey
        });
    } catch (e) {
        alert('Push通知機能が拒否されたか、エラーが発生したか、iPhoneでの実行かが原因でPush通知は送信されません。');
        return false;
    }


    // 必要なトークンを変換して取得(これが重要!!!)
    const key = subscription.getKey('p256dh');
    const token = subscription.getKey('auth');
    const request = {
        endpoint: subscription.endpoint,
        publicKey: btoa(String.fromCharCode.apply(null, new Uint8Array(key))),
        authToken: btoa(String.fromCharCode.apply(null, new Uint8Array(token)))
    };

    console.log(request);
}



/**
 * トークンを変換するときに使うロジック
 * @param {*} base64String 
 */
function urlB64ToUint8Array (base64String) {
    const padding = '='.repeat((4 - base64String.length % 4) % 4);
    const base64 = (base64String + padding)
        .replace(/\-/g, '+')
        .replace(/_/g, '/');

    const rawData = window.atob(base64);
    const outputArray = new Uint8Array(rawData.length);

    for (let i = 0; i < rawData.length; ++i) {
        outputArray[i] = rawData.charCodeAt(i);
    }
    return outputArray;
}

ここまでで、フロントエンドの準備は完了です。
サーバ環境で実行してみます。

PHPがインストールされていれば、簡易サーバを簡単に立ち上げることができるのでそれを利用してみてもいいでしょう、

$ cd index.htmlがおいてあるディレクトリ
$ php -S localhost:8080

上記コマンドの場合、 http://localhost:8080 にアクセスするとページが表示されるかと思います。

「WebPushを許可する」を押してPush通知を許可してみます。
これでPush通知を受信する手はずは整いました。

ここでブラウザコンソールを見てみると、

  • endpoint
  • publicKey
  • authToken

の情報が表示されると思います。
これらはPush通知を送信する際に必要になりますので、適当な場所にコピペして残しておいてください。
webpush.jsの52〜54行目にこれらを表示するコードが仕込んであります)
なお、本来の使い方としては、これらはDBなどに保存しておき、PHPなどから利用します。

WebサーバからPush通知を送信できるようにする

ここからはPush通知を送信するお話。
PHPでサーバからブラウザに向けてPush通知を送信していきます。

まずは以下のライブラリをcomposerを利用して取得します。
https://github.com/web-push-libs/web-push-php

$ composer require minishlink/web-push

次に、Push通知を送信するコードを作成していきます。 SendPush.phpとして保存します。 コード中のキーなどをの情報は適宜変更してください。

<?php
require_once 'vendor/autoload.php';

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

const VAPID_SUBJECT = 'ここにあなたのWebサイトのURL(http://localhost:8080/ など)';
const PUBLIC_KEY = '公開鍵( https://web-push-codelab.glitch.me/ で取得したもの )';
const PRIVATE_KEY = '秘密鍵( https://web-push-codelab.glitch.me/ で取得したもの )';

// push通知認証用のデータ
$subscription = Subscription::create([
    'endpoint' => 'ブラウザのコンソールで表示されていた「endpoint」',
    'publicKey' => 'ブラウザのコンソールで表示されていた「publicKey」',
    'authToken' => 'ブラウザのコンソールで表示されていた「authToken」',
]);

// ブラウザに認証させる
$auth = [
    'VAPID' => [
        'subject' => VAPID_SUBJECT,
        'publicKey' => PUBLIC_KEY,
        'privateKey' => PRIVATE_KEY,
    ]
];

$webPush = new WebPush($auth);

$report = $webPush->sendOneNotification(
    $subscription,
    'push通知の本文です'
);

$endpoint = $report->getRequest()->getUri()->__toString();

if ($report->isSuccess()) {
    echo '送信成功ヽ(=´▽`=)ノ';
} else {
    echo '送信失敗(´;ω;`)';
}

これで準備は完了です。
実行してみましょう。

$ php SendPush.php

これでブラウザにPush通知が表示されたと思います。

Push通知を無効化する

次はPush通知を無効化していきましょう。 webpush.jsに以下のコードを追加します。

/**
 * WebPushの許可を取り消す
 */
async function denyWebPush() {
    const subscription = await window.sw.pushManager.getSubscription();
    if (subscription) {
        // Push通知を許可している場合は許可を取り消す
        subscription.unsubscribe();
        const request = {
            endpoint: subscription.endpoint
        };

        // このendpointをキーに、DBから情報を消したりする
        console.log(request);
    }
}

HTMLの方には以下のコードを追記

<a href="javascript:denyWebPush()">WebPushを拒否する</a>

リンクをクリックするとブラウザコンソールにendpointが表示されると思います。
これでPush通知は無効化されました。

試しにPush通知を送信してみてください。

$ php SendPiush.php

すると送信失敗のメッセージが表示されると思います。

ブラウザコンソールに表示させたendpointは、Push通知の許可を行った際に発行されるendpointと同値になるはずです。
DBにPush通知の送信先情報を登録していた場合、このendpointの情報をもとにDBからデータを削除するのに使います。

以上となります。

終わりに

ブラウザでPush通知を行える時代が来ております。
お手軽にPush通知を試せ、サービスにもお手軽に組み込めそうで良い感じですね。(iPhoneは対応してくれませんが…)
極力コピペで作れるようにしておりますので、ぜひともお試しください。

今回はここまで。
それではまた次回お会いしましょう。

参考にさせていただいたサイト様

記事を書く上でお世話になりました。

AWS ECSにおけるログ保存コスト削減のススメ・S3圧縮編

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

以前、こんな記事を書きました。

tech.excite.co.jp

端的に言えば、「AWS ECSのログを保存する時、Fluent Bitをログルータとして使用すればCloudWatch以外にもログを流すことができ、適切にログの流し先を選択すればコストを削減できる」という話でした。

今回は、その中の1つである「S3にログを流す方法」について、ログ保存時に圧縮を行うことで更にコストを削減できる、というお話です。

S3におけるログ保存の流れ

Fluent Bitを使ってS3にログを保存する場合、通常は

  1. コンテナのログをFluent Bitが収集する
  2. 一定時間経過、もしくは一定サイズまでログが溜まったら、S3へ送信

となります。

この時、何も指定をしなければそのままS3へログが送られるのですが、圧縮して送ることができれば、送信するファイルサイズ・及びS3に保存されるファイルサイズが減るため、コスト削減になるという寸法です。

圧縮方法

Fluent Bitの設定ファイルの [OUTPUT] セクションにおいて、以下の3つの点を修正することでファイルを圧縮してS3に送信することができるようになります。

[OUTPUT]
    Name s3
    ...
    s3_key_format /test-$UUID.gz
    compression gzip
    content_type application/gzip

s3_key_format$UUID 変数と gz 拡張子をつける

S3へ出力するときのパス・オブジェクト名となる s3_key_format に、 $UUID 変数と gz 拡張子をつけます。

gz 拡張子をつけないと、S3上のファイルをAthena等で参照しようとする時にgzip圧縮されていると判定してくれないため、指定は必須です。 この時、明示的に $UUID 変数も入れておかないと、自動的にファイル名末尾にUUIDがついてしまい gz 拡張子を指定できないため、こちらの指定も必要になります。

compressiongzip を指定する

compressiongzip を指定することで、gzip圧縮してS3に送信してくれるようになります。

content_typeapplication/gzip を指定する

こちらは必須ではなく、指定せずとも問題はありませんが、正しい content_type を指定したほうが良いと思います。

圧縮結果

圧縮した結果、S3のファイルサイズは約1MBから約100KB程度まで下がりました。

f:id:excite-takayuki-miura:20210628142153p:plain
圧縮前

f:id:excite-takayuki-miura:20210628141804p:plain
圧縮後

さいごに

S3自体がもともと安価なため、よほどログ量が多くない限りは莫大なコスト削減になるわけではないですが、それでもストレージ料金や各種通信料金の削減にはなるかと思います。 なお注意点として、Fluent Bitのバージョンが古いと s3_key_format における $UUID 指定や compression の設定ができない可能性もあるため、新しいバージョンを使用してください。

MiroでオンラインKPTしてみた話

f:id:ixit-kata:20210624162559p:plain

こんにちは。エキサイトHD、iXIT株式会社の片岡です。

4月に育休から復帰したのですが、2年休業してる間に世の中はコロナ禍突入、テレワークメインになり、浦島太郎はついていくのに必死です(汗

 

先日、担当した短期プロジェクトのリリースが終わったので、振り返りでKPTをやろうとしたのですが、そういえば、オンラインでKPTってどうやってやるの?という疑問が。

太郎なりに調べたところ、どうやらMiroというオンラインホワイトボードが使いやすいらしい。WEBでも評判良いし、社内でもボチボチ使ってるところがあるみたい。ということで、ツールはMiro+Zoomを使ってみることに。

KPT準備

今までのオフラインKPTだと、事前に準備しておいてもらうことはあまりなかったのですが、今回は事前にMiroでボードを準備しておいて、KPTそれぞれの意見を前もって付箋で張っておいてもらうようメンバーにお願い。ボードは、「Start/Stop/Continue Retrospective」というテンプレートを使ってみました。

f:id:ixit-kata:20210623224327j:plain

振り返り用のテンプレらしい

 

 KPTメンバーは私入れて4名だったので、4名それぞれ、付箋で色をわけてみることに。Aさん黄色、Bさんはピンク、みたいな。いちいち付箋に名前書かなくても良いので楽だしわかりやすいです。

いざ、オンラインKPT

オンラインKPTの進め方はオフラインとほぼ同じです。違うのは、付箋を書いたり貼ったりする時間がないことくらい。

ただ、今回は、KPTのTが壮大になりそうな予感がしたので、直近の具体的なアクションに落とし込むActionというエリアも追加してみました。

zoom+Miroの共同編集で進行。zoom画面共有は使いません。

 

で、KPT後のアウトプットがこちら。ばばーん。

f:id:ixit-kata:20210624090104j:plain

画像が粗くてごめんなさい

左から、Keep/Problem/Try/Action。手書き箇所は、iPadのApplePencilで書いてます。

以下、MiroをKPTで使ってみた所感です。

メリット

①事前に意見出しができるので、議論に時間をたくさん使える 

KPT、まじめにやるとけっこう時間かかります。付箋書く時間が長くかかるのは仕方ないのですが、その付箋を順番に前に出て貼って発表という一連の動作のロスタイムも地味に多くて、ファシリテーターは時間気にしがち。それが事前に付箋書いて貼ってもらうだけで、こんなにゆっくり議論ができるとは。なんて素晴らしい!

 

 ②付箋の色で誰の意見や担当かすぐわかる

KPTの中で、付箋をカテゴリ分けしていくと、どれがだれの意見かわからなくなりがちですが、人で色分けしてると一目瞭然。オフラインでも付箋の色を変えることは可能ですが、人数多いとなかなか厳しく。営業、デザイン、運用、開発、いろいろな立場のメンバーが集まって開催されるKPTですが、俯瞰してボードを見ると、例えば、ナレッジ蓄積についての意見は運用や開発からが多い、KPIや横展は営業からの意見が多いなど、視点の違いがより明確になって面白いです。人数多いときは、職種によって付箋の色を変えても良いかもしれないですね。

Actionは、意見出しの際は白い付箋で書き出して、そのあと担当が決まったらその方の付箋の色に変更しました。こんな使い方できるのもオンラインならでは!

 

③手書きも使える

今回、PCとiPadで同じアカウントでログインしていたのですが、複数端末からまったく違和感なく同時操作できるので、PCメインで操作しながら、ちょっと手書きでメモしたいところはApplePencil。便利だし、使いどころ多そうです。

 

KPTを内容を残せる&共有できる

 リアルだと消してしまうホワイトボードも、Miroはそのまま残せるので、写真を何枚も撮ったり、議事録を書く手間がなく、ボードURLでチーム外への共有も簡単です。

デメリット

 ①事前に意見出しすることで、議論の中で生まれるアイデアが引き出しづらい

 KPTの良さのひとつは、色々な立場の人の意見を聞いて、新しいアイデアが出てくることです。それが事前に意見出しすることで、付箋を書く時間=考える時間が少なくなり、この種のアイデアが出づらくなってしまうなぁと。ファシリテーターは、この点踏まえたメンバーへの意見出しの働きかけが必要です。

 

うーん。デメリットは1個しか思いつきませんでした。

 まとめ

 やだ、なにこれ使いやすい。

リアルホワイトボード+付箋のKPTの欠点(終わったら消さなくてはいけない、付箋やペンの準備が必要、事前に準備しづらい)をいい感じに補ってくれて、振返りをもっと身近にしてくれる素敵ツール。

こんな世界があったのね!と太郎はちょっと感動。新しい価値観を与えてくれる玉手箱、ワクワクします。弊社もこんなプロダクトを世の中に出していきたいなぁ。がんばろう。

 

Miro、まだまだ使い込んでみて、良さげな使い方を見つけたらまたブログで報告しまーす。とりあえず社内にMiro布教するぞー。おーっ。

 

 

第1回定期勉強会「メディアのコーディング規約適用事例と効果」

会の様子
会の様子

エキサイトのあはれんです。

弊社で始まった社内定期勉強会について紹介いたします。 第1回定期勉強会は「メディアのコーディング規約適用事例と効果」でした。 メディアチームのコーディング規約について説明を聞き、エキサイトHDのメンバーで議論しました。 最大参加人数は51名にも達し、多くのメンバーが参加してくれました。😊🎉

コーディング規約を決めている定例会の議論の活性化方法から始まり、 命名規則等のコーディング規約について数多くのメンバーが意見を出し合っていました。

勉強になったポイント

今回の定期勉強会で、私個人が勉強になったことをピックアップして紹介します。

コーディング規約「早期returnを使用する」

私も早期returnを使用することには賛成でして、 「処理に関係ない条件の状態はさっさとreturnして、気にしないといけない条件の状態を減らしたい」 という気持ちでいつもreturnを使っています。

定期勉強会では、早期returnを使用する理由として以下が挙げられていました。

returnしないでいるとその後もダラダラ書けてしまい結合度が高くなる。 早期returnをすることでダラダラ書けない状態にする。

エンジニアに結合度を高くなるコードを書かせないようにする工夫として、 「早期return」を使うという観点はなかったので、 新たな視点で「早期return」を使う理由が知れてよかったです。

コーディング規約「単純分岐でSwitch文は使用しない」

これは、議論が盛り上がったコーディング規約でした。 その議論の中でも以下の意見には、ハッとさせられました。

switchだと、変数の状態によって処理が変わることがわかる。 ifの条件はなんでも書けるので、変数の状態によって処理が変わるかどうかはすぐに判断できない。

言われてみればそうなのですが、意識していなかった部分でした。 読みやすいコードを書く上で、上記の考え方は重要なことだと思いますので、 これからコードを書くときは意識してみようと思いました。

まとめ

コーディング規約に関して議論することで、コーディング規約の理由を理解することができてよかったです。 これからも社内の定期勉強会で得られた知見やノウハウを紹介していきますので、見ていていただけると幸いです。

ftlに書いたSQLに補完とシンタックスハイライトを効かせたい

はじめに

普段、MyBatis FreeMarkerを使って、ftlファイルにSQLを書いていますが、IntelliJ のデフォルトの状態だと、補完やシンタックスハイライトが効かず、下図のようになります。 f:id:b_murabito:20210621001342p:plain

SQLはSequel ProやSequel Ace、AdminerなどのDBクライアントツールを使って書いたものを貼り付けることが多いのですが、Intellij上での視認性が悪いので、シンタックスハイライトが効くようにしてみます。

設定方法

設定->言語&フレームワーク->テンプレートデータ言語で下図のような設定を追加します。

パスには、ftlファイルまたはftlファイルがあるディレクトリを指定します。

言語には使用しているDBを指定します。

IntelliJ IDEAのCommunity版だと指定できないDBがあるので注意

ここではMySQLを選択しました。

f:id:b_murabito:20210621002149p:plain

設定後はこんな感じになります。 f:id:b_murabito:20210621000916p:plain

補完も効くようになりました。 f:id:b_murabito:20210621001114p:plain

おわりに

無理矢理感はありますが、これでftlファイルに書いたSQLSQLとして認識させられるようになりました!

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はその状況を考えてくれているので、ぜひ使っていきましょう。