MySQLでMaterialized Viewを実現する

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

tech.excite.co.jp

以前こちらのブログで、「MySQLにおいて、Indexだけではパフォーマンス改善に限界があり、一定以上複雑なSQLやデータ構造に対してはIndex以外の手段を講じる必要がある」ことを説明しました。 今回は、その方法の1つであるMaterialized ViewをMySQLで実現する方法を説明します。

Materialized Viewとは

Materialized Viewとは、SQL実行パフォーマンスの改善等のために作るテーブルのことです。 通常のデータ構造でのIndexだけではパフォーマンス等が改善できないときに使用します。

例を挙げてみます。

以下のような article (記事)テーブルと article_tag (記事タグ)テーブルがあるとします。

article

article_code story category publish_date_time
article1 テスト lifestyle 2022-01-01 00:00:00
article2 テストテスト child 2022-01-01 01:00:00
article3 テストテストテスト beauty 2022-01-01 02:00:00
... ... ... ...

article_tag

article_code tag
article1 芸能
article1 ニュース
article2 子育て
article2 育児
article2 夫婦
article3 ニュース
... ...

これについて、「指定のカテゴリに属しており、かつ指定のタグを持つ記事の一覧を、記事の公開日順に並び替えて取得する」ことを考えてみます。 SQLとしては以下になるでしょう。

SELECT
    article.*,
    article_tag.tag

FROM
    article

INNER JOIN
    article_tag
    ON article_tag.article_code = article.article_code

WHERE
    article.category = "***"
    AND article_tag.tag = "***"

ORDER BY
    article.publish_date_time;

この場合、どんなIndexが適しているでしょうか?

article テーブルに関しては、 category で絞り込んで publish_date_time でソートしているため category, publish_date_time のIndexが良さそうに見えます。

一方で article_tag テーブルに関しては、 tag で絞り込んでいるので tag 単体のIndexか、あるいは article_codearticle テーブルと接続しているので tag, article_code の複合Indexでしょうか?

実はこちら、完璧にハマるIndexは存在しません。

全体の件数が少なかったり、検索したいタグがほとんどの記事に含まれているなどであれば検索速度は早いですが、全体の件数が多かったり、マイナーなタグを検索する場合は速度が大きく落ちてしまいます。

こういった場合にMaterialized Viewを使用します。

元のテーブルのデータを使って、以下のようなテーブルを作ります。

article_tag_materialized_view

article_code category tag publish_date_time
article1 lifestyle 芸能 2022-01-01 00:00:00
article1 lifestyle ニュース 2022-01-01 00:00:00
article2 child 子育て 2022-01-01 01:00:00
article2 child 育児 2022-01-01 01:00:00
article2 child 夫婦 2022-01-01 01:00:00
article3 beauty ニュース 2022-01-01 02:00:00
... ... ... ...

これを使用する場合、SQLは以下のようになります。

SELECT
    article.*,
    article_tag_materialized_view.tag

FROM
    article_tag_materialized_view

INNER JOIN
    article
    ON article.article_code = article_tag_materialized_view.article_code

WHERE
    article_tag_materialized_view.category = "***"
    AND article_tag_materialized_view.tag = "***"

ORDER BY
    article_tag_materialized_view.publish_date_time;

このSQLであれば、つけるべきIndexは簡単に推測できます。

article_tag_materialized_view については tag, category, publish_date_time のIndexが、 article については article_code のIndexがあれば高パフォーマンスとなるでしょう。

このように、Indexだけではパフォーマンスが改善できないデータ取得処理でも、Materialized Viewを用いることで改善することができます。

MySQLとMaterialized View

では、MySQLでMaterialized Viewを作るにはどうすれば良いでしょうか?

残念ながら、MySQLではデフォルトでMaterialized Viewを作る機能は存在しません。

データが増減することがないテーブルであれば最初にテーブルを作ってデータを入れてしまえば終わりですが、多くのテーブルはデータの増減や更新があるでしょう。 そこで、何らかの方法でエンジニアが手を加えて、データの変更があったときにも自動で対応されるようにMaterialized Viewを生成する必要があります。

方法としては、いくつか考えられます。

Triggerを使用する

MySQLにはTriggerという機能があります。 これは、指定テーブルに Insert / Update / Delete が走ったときに、自動的にこちらが指定したSQLを実行させることができる機能です。

例えば先程のMaterialized Viewであれば、 articlearticle_tag テーブルに Insert / Update / Delete が走ったら、関連するデータをMaterialized Viewに Insert / Update / Delete するようなSQLをTriggerとして設定しておけば、自動的にMaterialized Viewのデータが正しいものになります。

この方法には以下のメリット・デメリットがあります。

メリット

  • データ元テーブルのデータ変更時に自動的にSQLが実行されるため、変更が即時Materialized Viewに適用される
  • Triggerも含めてトランザクション単位となるので、トランザクションを使用してる場合、TriggerのSQLが正しければ必ずMaterialized Viewとデータ元テーブルのデータが一致する

デメリット

  • データ元テーブルの Insert / Update / Delete と一体となって実行されるため、Triggerが無いときと比べてそれらの処理に時間がかかるようになる
  • アプリケーションコード側に処理を書くのに比べ、テーブル側に処理を書く場合はその存在が見過ごされやすく、何かあったときにバグの原因になりやすい

バッチを使用する

Materialized Viewを更新するバッチを作成し、cron等で定期実行する方法でもMaterialized Viewを更新できます。

この方法には以下のメリット・デメリットがあります。

メリット

  • アプリケーションコードを使用するため、柔軟な処理を書くことができ、Materialized View変更に対するコストを最小限にすることができる
    • 最低限の変更のみ実行するようにする、複数件の変更を1SQLで実行するなど

デメリット

  • cronで定期実行する場合、反映が即時ではなくcronのタイミング次第となってしまう
  • Materialized Viewへの処理が何らかの理由で失敗してしまった場合、Materialized Viewのデータとデータ元テーブルのデータが乖離してしまう可能性がある

他にもあるかもしれませんが、ざっくり考えられるのはこのあたりでしょう。

個人的には、データ元テーブルに対する変更処理の実行速度の低下が問題にならないのであれば、データ整合性の観点から基本的にはTriggerを使い、そこがどうしても問題になったり、何かしらの理由でTriggerが十分に使えない場合はバッチを使うのが良いと考えています。

なおTriggerを使う場合は、Trigger自体を作成するSQLをバージョン管理するなどして、その存在が忘れられてしまわないよう対策を講じましょう。

最後に

Materialized Viewは非常に便利な概念です。 ですが、安易にたくさん作ってしまうと管理が大変になってしまうというデメリットも抱えています。

可能な限りデータ構造自体を正したりIndexを使ってパフォーマンスを改善するようにし、どうしようもない場合だけMaterialized Viewを使うようにしましょう。

quarkusを使う(テンプレートエンジン編)

こんばんは、エキサイト株式会社中尾です。

今回はテンプレートエンジンについて説明します。

quarkusにはQUTEがあります。

ja.quarkus.io

特徴として

  • ネイティブビルドに対応(間違っているかもしれない)
  • テンプレートファイルを変えたら、リロードすれば反映される
  • @CheckedTemplate でテンプレートの整合性チェックが可能

基本的なif文、for文は以下で実装ができます。

    {#for sample in samples} 
      <h2>{sample.name ?: 'Unknown'}</h2> 
      <p>
      {#if sample.valid}
        {sample.data}
      {#else}
        <strong>Invalid sample found</strong>.
      {/if}
      </p>
    {/for}

renderAsync で非同期でレンダリングすることも可能です。

template.data("name", "neo").renderAsync();

apiサーバーとして利用することが多いと思うので詳しく試しませんが、htmlごと返すようなAPIがあったら使えるかもしれません。※そんなことはないと思いますが。

MySQLの「Indexが当たる」とはどういうことなのか

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

MySQLを使用してデータを取得する際、その取得速度を高めるためにほとんどの場合Indexを使用することになります。

シンプルなSQLでデータを取得するのであれば、そのSQLに当たるIndexはわかりやすいでしょう。

ですが、複雑なSQLだとそう簡単には行きません。 そういったSQLにIndexを適用しようとして、うまくパフォーマンスが上がらず苦労した経験がある方も多いのではないでしょうか。

今回は、そもそも「SQLにIndexが当たる」とはどういうことなのかを考え、それを元にうまくIndexを当てる方法を考察していきます。

1つのカラムを条件とするSQL

以下のような article テーブル(記事テーブル)を考えてみます。

article_code story category publish_date_time
article1 テスト lifestyle 2022-01-01 00:00:00
article2 テストテスト child 2022-01-01 01:00:00
article3 テストテストテスト beauty 2022-01-01 02:00:00
... ... ... ...

このようなテーブルから、特定の category の記事を取得するSQLは以下になります。

SELECT
    *
FROM
    article
WHERE
    category = "***";

この場合の適切なIndexは簡単です。 category をキーとするIndexがあれば良いでしょう。

ちなみに、なぜ category をキーとするIndexであれば、上記のSQLの取得速度が早くなるのでしょうか? それは以下のように、 category でソートされたデータが作られるからです。

article_code story category publish_date_time
*** *** beauty ***
*** *** beauty ***
*** *** beauty ***
*** *** beauty ***
... ... ... ...
*** *** child ***
*** *** child ***
*** *** child ***
*** *** child ***
... ... ... ...

category でソートされているため、特定の category のデータが非常に見つけやすく、結果としてSQLの実行速度が早くなるわけです。

これは例えば、我々が紙の単語辞典から特定の単語を見つけたいときに、あいうえお順で単語が並んでいれば見つけやすいのと同じ道理です。

1つのカラムを条件とし、別のカラムで並び替えるSQL

では次に、category を条件として取得したデータを publish_date_time 順に並び替えることを考えましょう。

SQLとしては以下になります。

SELECT
    *
FROM
    article
WHERE
    category = "***"
ORDER BY
    publish_date_time;

これを高速化するためには、どんなIndexがあれば良いでしょうか。

今度は、Indexからどのようなデータが作られれば高速化するか、という観点から考えていきます。

category で絞り込み、その後 publish_date_time で並び替えるのであれば、つまり最初からそのようなデータとなっていれば良いわけです。 すなわち、以下のようなデータが好ましいということになります。

article_code story category publish_date_time
*** *** beauty 2022-01-01 02:00:00
*** *** beauty 2022-01-05 01:00:00
*** *** beauty 2022-01-11 01:00:00
*** *** beauty 2022-01-15 01:00:00
... ... ... ...
*** *** child 2022-01-01 01:00:00
*** *** child 2022-01-03 02:00:00
*** *** child 2022-01-13 02:00:00
*** *** child 2022-01-13 03:00:00
... ... ... ...

このようなデータであれば、 category を絞り込むのも容易ですし、絞り込んだ時点ですでに publish_date_time で並び替えられているのでそのまま使用すれば良いことになります。

そして、このデータを実現するIndexが category,publish_date_time の複合Indexとなります。

つまり、このIndexが今回のSQLにおいて適切なIndexとなるわけです。

このように考えていくと、案外Indexの当て方が簡単に思えてきませんか?

では別のケースを考えてみましょう。

IN句を条件とするSQL

複数の category でデータを絞り込みたい場合は、IN句を使用します。

SELECT
    *
FROM
    article
WHERE
    category IN ("***", "***");

この場合に適切なIndexはなんでしょうか?

これもそこまで難しくはなく、一番最初のSQLのように category で並び替えられていれば問題ありません。

article_code story category publish_date_time
*** *** beauty ***
*** *** beauty ***
*** *** beauty ***
*** *** beauty ***
... ... ... ...
*** *** child ***
*** *** child ***
*** *** child ***
*** *** child ***
... ... ... ...

これで十分高速化されるでしょう。

IN句で絞り込み、別のカラムで並び替えるSQL

では次に、複数の category で絞り込んだ後に publish_date_time で並び替える場合はどうでしょうか?

SELECT
    *
FROM
    article
WHERE
    category IN ("***", "***")
ORDER BY
    publish_date_time;

2番目のSQLのようなデータ( category,publish_date_time の複合Index)で考えてみましょう。

article_code story category publish_date_time
*** *** beauty 2022-01-01 02:00:00
*** *** beauty 2022-01-05 01:00:00
*** *** beauty 2022-01-11 01:00:00
*** *** beauty 2022-01-15 01:00:00
... ... ... ...
*** *** child 2022-01-01 01:00:00
*** *** child 2022-01-03 02:00:00
*** *** child 2022-01-13 02:00:00
*** *** child 2022-01-13 03:00:00
... ... ... ...

これで十分でしょうか?

実際にこれで並び替えることを考えると、

  1. category ごとにデータを絞り込む
  2. category で絞り込んだレコードをあわせ、 publish_date_time で並び替える

ことが必要になるのが想像できます。

ここで重要なのは「 publish_date_time で並び替える」ことが必要になってしまっている点であり、結果として十分な速度が出ない可能性があります。 もちろん全体のデータ件数が少なければ十分かもしれませんが、件数が増えていくとどこかで速度が不十分になることが予想されます。

では、どうすればよいのでしょうか?

理想としては、「複数の category で絞り込んだ結果を、 publish_date_time で並び替えたデータ」を作る、などでしょうか。 すなわち、

article_code story category publish_date_time
*** *** beauty 2022-01-01 02:00:00
*** *** beauty 2022-01-05 01:00:00
*** *** beauty 2022-01-11 01:00:00
*** *** beauty 2022-01-15 01:00:00
... ... ... ...
*** *** child 2022-01-01 01:00:00
*** *** child 2022-01-03 02:00:00
*** *** child 2022-01-13 02:00:00
*** *** child 2022-01-13 03:00:00
... ... ... ...
*** *** beauty,child 2022-01-01 01:00:00
*** *** beauty,child 2022-01-01 02:00:00
*** *** beauty,child 2022-01-03 02:00:00
*** *** beauty,child 2022-01-05 01:00:00
*** *** beauty,child 2022-01-11 01:00:00
*** *** beauty,child 2022-01-13 02:00:00
*** *** beauty,child 2022-01-13 03:00:00
*** *** beauty,child 2022-01-15 01:00:00
... ... ... ...

などです。 ですが残念ながら現状、すべての組み合わせでIndexを作る、という機能は存在しません。

つまり、ここが「Indexだけでパフォーマンスを改善する」方法の限界点と言えます。

改善方法としては、

  1. もし category の組み合わせパターンが決まりきっているのであれば、その組み合わせを1つのデータとして持つデータ構造を作り、それをもとに新しくテーブルを作る
  2. NoSQLを使用する

などが挙げられるでしょう。

最後に

Indexは、一見難しそうに見えますが、実は人がたくさんのデータの中から何かを見つけたり、並び替えたりするのと道理としては大差ありません。 そのため、仕組みさえ理解していれば、複雑なSQLでも適切なIndexを考えるのは不可能ではないのです。

一方で、だからこそIndexだけで解決できないSQLも存在します。

きちんと仕組みを理解し、適切なIndexとは何かを考えるのと同時に、Index以外の手段が必要な場合があることも想定してデータ構造やSQLを考えていきましょう。

SliverGrid、SliverListを使用したUI実装

こんにちは。最近AndoridからFlutterでの開発に移転した、エキサイト株式会社の奥田です。

業務においてリストやグリッドを一緒にスクロールさせたいという要望がありました。 しかし、ListView、GridViewで実装すると別々にスクロールされてしまいます。 そんな問題をSliverList&SliverGrid解決することができたので、実装方法について記述していきます。

動作バージョン

  • Flutter 2.8.1

  • Dart 2.15.1

なぜSliverListとSliverGridを使用するのか?

なぜListView、GridViewではなくSliverListとSliverGridを使用するかというと、下記に掲載した動画からの引用ですが、

ListViewとGridViewはコンテンツを別々に表示するのには最適です。 しかし、もしリストやグリッドを一緒にスクロールさせたり、もっと複雑なスクロールをさせたい場合はSliverListとSliverGridを使用するのが最適です。

つまりは今回の目的であるリストやグリッドを一緒にスクロールを実現するのに最適であることがわかります。

www.youtube.com

実装部分について

下記掲載のコードで動画のようなUI、同時スクロールが可能になります。

実際にbuildした画面も掲載しますので、挙動を確認したい方はお手元の環境で実装してみてください。

class CollapsingList extends StatelessWidget {
  const CollapsingList({Key? key}) : super(key: key);
  
  @override
  Widget build(BuildContext context) {

    final gridListItem = [
      Container(color: Colors.red, height: 150.0),
      Container(color: Colors.purple, height: 150.0),
      Container(color: Colors.green, height: 150.0),
      Container(color: Colors.orange, height: 150.0),
      Container(color: Colors.yellow, height: 150.0),
      Container(color: Colors.pink, height: 150.0),
      Container(color: Colors.cyan, height: 150.0),
      Container(color: Colors.indigo, height: 150.0),
      Container(color: Colors.blue, height: 150.0),
    ];
    
    final listItem = [
      Container(color: Colors.red),
      Container(color: Colors.purple),
      Container(color: Colors.green),
      Container(color: Colors.orange),
      Container(color: Colors.yellow),
    ];
    
    return CustomScrollView(
      slivers: [
        SliverGrid.count(
          crossAxisCount: 3,
          children: gridListItem,
        ),
        SliverFixedExtentList(
          itemExtent: 150.0,
          delegate: SliverChildListDelegate(listItem),
        ),
      ],
    );
  }
}

f:id:pomupomupurinkun:20220127202948p:plain

まとめ

SliverGrid、SliverListを使用することで当初の目的であったリストやグリッドを一緒にスクロールさせたいを解決することができました。 アイテムの個数が動的に変更されても対応することが可能だったり、表示部分の描写だけをしてくれたりと使い勝手の良さそうな印象を受けました。 公式のドキュメントを掲載しておきますので、より深く学習されたい方は目を通すことをおすすめします。

docs.flutter.dev

最後に

弊社では絶賛採用強化中です。もしご興味がある方がいましたら下記リンクよりアクセスいただけると幸いです。(カジュアルからもOKです)

www.wantedly.com

MySQLでテーブルを作る際に注意すべきこと

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

アプリケーションを作る上で、データを格納するためにMySQLを立ててテーブルを作成する機会は多いと思います。 今回は、テーブルを作成する際に注意すべきことをいくつか書いていきます。

一意なデータにはユニーク制約をつける

当然といえば当然ですが、データ構造上一意になるデータにはユニーク制約をつけましょう。

「データを追加・更新するときに気をつけるから大丈夫」と思っていても、何かのタイミングでうっかり重複してしまう可能性は捨てきれません。 また、ユニーク制約がある事自体が仕様となり、後から使用する人に親切な作りにもなります。

参照順序を考える

MySQLでは、 utf8_general_ci のようなアルファベットの大文字・小文字を区別しない参照順序や、 utf8_bin のような大文字・小文字を区別する参照順序、絵文字を認識してくれる utfmb4 系の参照順序など、さまざまな種類の参照順序があります。

大文字・小文字の区別の例で考えると、多くの場合アプリケーションコードでは文字列を扱うときに大文字・小文字は自然と区別されて扱われるので、MySQLutf8_general_ci (大文字・小文字を区別しない参照順序)を使っており、かつそれを認識しないまま使用していると、思わぬところで不具合が生じる恐れがあります。

入れるデータにとってどの参照順序が適切かを考え、適切なものを選択しましょう。

日付を扱う際は timestamp ではなく datetime を使う

日時を扱う場合、 timestamp 型と datetime 型のどちらかを選択することができますが、 timestamp ではサポート範囲の関係で、2038年以降は日付が正しく表示されないという問題があります。

datetime 型を使うようにしましょう。

AutoIncrementを使用するカラムの型に適切なものを選ぶ

AutoIncrementを使用するカラムでは、数値を使い切らないよう適切な型を選ぶ必要があります。

多くの場合は int 型で良いかもしれませんが、頻繁にdelete/insertが走るようなテーブルであれば bigint 型の方が良いでしょう。

何をカラムとして持ち、何を別テーブルに持つか判断する

例えば記事データを持つテーブルを作る時、記事のタイトルや本文は記事テーブルのカラムとして持っておいても不思議ではありません。

一方で、記事のタグはどうでしょうか? 多くの場合、タグは1つの記事に複数持つものです。 その場合、 tag1 tag2 ... などのカラムを持つべきでしょうか?

こういったカラムについては、記事テーブルにカラムとして持つより、記事タグテーブルとして別テーブルに持ち、記事テーブルと連結して使うほうが良いでしょう。

このあたりをすべてそのまま記事テーブルにカラムとして作るようにすると、ほとんどNULLしか入っていないカラムができたり、使われているのかどうかわからないカラムが大量にできてしまったりします。

可能な限りデフォルトNotNullにする

NULLは、MySQLにとってもアプリケーションコードにとっても特別な値であり、取り扱いが難しいデータです。 MySQLの多くの型ではデフォルト値を入れることが可能なので、可能な限りカラムはNotNullとし、デフォルト値を入れるようにしましょう。

最後に

最近よくMySQLを触る機会があったので、気になったところを並べてみました。

データ構造は一回作って使われ始めると修正するのが非常に困難なものなので、最初に作るときにきちんと熟考するようにしましょう。

chromeでも利用可能なオレオレ証明書をワンライナーで作成する

システム開発部の @nukisashineko (ぬさし) です。

オレオレ証明書の作成が年々面倒になってたりします。
特に chrome は厳しくて、chromeでも利用できるオレオレ証明書をワン・コマンドで作成できる方法はなかなかありません。

今回は、chrome でも 利用可能なオレオレ証明書をサクッと作成できる方法が判明しました。 コマンドについて共有させていただこうと思います。

作成コマンド

mkdir -p certs && cd certs

# 作りたいオレオレ証明書のドメイン
export MY_DOMAIN=xxxx.examle.com 

# Key Access で表示される 表示名
export MY_CA_NAME=xxxx-example-com-ca

# オレオレ証明書を作成 ( ※ 期限は 1 年)
mkdir -p ${MY_DOMAIN}/certs && \
    docker run --rm -it \
        --name certs \
        -v $(pwd)/${MY_DOMAIN}/certs:/certs \
        -e SSL_SUBJECT=${MY_DOMAIN} \
        -e SSL_DNS=${MY_DOMAIN} \
        -e CA_EXPIRE=365 \
        -e SSL_EXPIRE=365 \
        -e CA_SUBJECT=${MY_CA_NAME} \
        stakater/ssl-certs-generator:1.0
  • stakater/ssl-certs-generator:1.0 を利用するのが肝です
    • 有名な paulczar/omgwtfssl は 最新のchrome に対応してません。

作成されたファイルの確認

tree .
.
└── xxxx.examle.com
    └── certs
        ├── ca-key.pem
        ├── ca.pem
        ├── ca.srl
        ├── cert.pem
        ├── key.csr
        ├── key.pem
        └── openssl.cnf

オレオレ証明書を keychain accessコマンドラインから追加

sudo security add-trusted-cert -d \
  -r trustRoot \
  -k /Library/Keychains/System.keychain \
  ./xxxx.examle.com/certs/ca.pem

keychain access を確認

f:id:hibikiosawa4388:20220124151512j:plain
keychain_access_only_example_com

参考資料

第5回定期勉強会「Clean Architecture勉強会」

f:id:e125731:20220121183201p:plain
第5回定期勉強会「Clean Architecture勉強会」

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

今年第1回目の定期勉強会のお題は「Clean Architecture」でした。

最近、カンファレンスや求人サイト等でも、Clean Architectureという文字はよく見かけるようになったと思います。 弊社でもClean Architectureを参考にしたプロジェクトが増えております。 それでも、全メンバーが知っているわけではありませんので、 Clean Architectureにそってプロジェクト開発しているメンバーに説明いただきました。

なお過去開催分は以下になりますので、よければ御覧ください。

tech.excite.co.jp

勉強会内容

https://www.amazon.co.jp/Clean-Architecture-%E9%81%94%E4%BA%BA%E3%81%AB%E5%AD%A6%E3%81%B6%E3%82%BD%E3%83%95%E3%83%88%E3%82%A6%E3%82%A7%E3%82%A2%E3%81%AE%E6%A7%8B%E9%80%A0%E3%81%A8%E8%A8%AD%E8%A8%88-Robert-C-Martin/dp/4048930656www.amazon.co.jp

Clean Architectureの本の内容にそいながら、 「SOLID原則」「非循環依存関係の原則」「安定依存の原則」「安定度・抽象度等価の原則」について説明いただきました。 Clean Architectueとは、どんなソフトウェアにも適用可能な共通のルールのことであり、 依存の方向は常に内側制御の流れと依存方向は分離して考えることが大事と説明いただきました。

質問タイムには、 「クリーンアーキテクチャを気にせず、フレームワークに依存しながらゴリゴリ書いたほうが早いのではないか?」という質問があり、「初速はゴリゴリ書いたほうが早く思えるが、サービスが成長し複雑になっていくと改修が大変になるので、 クリーンアーキテクチャになるように書いたほうが良く、実際にクリーンアーキテクチャのルールに則って書いたほうがコードは読みやすくなった。」と回答されていました。

話を聞いて私は、Clean Architectueのルールに則って実装するのは難しく時間がかかりそうですが、 挑戦したほうが、サービスの成長を止めないアーキテクチャになりそうだと思いました。挑戦していきたいです!

最後に

今回の勉強会は、知らない人にとっては知識を得ることができ、 知っていた人にとっては改めて理解を深める機会になったのではないかと思います。 今後とも様々な勉強会を開催していくのでよろしくおねがいします。

quarkusを使う(ExceptionMapper編)

こんばんは

お久しぶりです。エキサイト株式会社 中尾です。

最近は趣味でQuarkusを使っています。 本記事ではQuarkusの例外処理について紹介します。

Red Hatの皆様、コメントください、DM待っています。

今回はエラーのハンドリングということで、exceptionのcustom handlerを作っていきます。

ExceptionMapperを継承して、BadRequestExceptionを発生させます。

package org.my.hobby.controller.exception.handler;

import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

@Provider
public class BadRequestExceptionHandler implements ExceptionMapper<BadRequestException> {

    @Override
    public Response toResponse(BadRequestException e) {
        return Response.status(Response.Status.BAD_REQUEST).
                entity(new ErrorMessage(e.getMessage())).build();
    }
}
package org.my.hobby.controller.exception.handler;

import java.io.Serializable;

public class BadRequestException extends
        RuntimeException implements Serializable {

    private static final long serialVersionUID = 1L;

    public BadRequestException() {
    }

    public BadRequestException(String message) {
        super(message);
    }

    public BadRequestException(String message, Throwable cause) {
        super(message, cause);
    }

    public BadRequestException(Throwable cause) {
        super(cause);
    }

    public BadRequestException(String message, Throwable cause,
                               boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}
package org.my.hobby.controller.exception.handler;

public record ErrorMessage(
        String message
) {
}

これでcontroller内でthrow new BadRequestException("invalid emai");などで、ErrorMessageを返すことができます。

SpringBootのプラグインを入れたらControllerAdviceも使えますが、使いません(不自由を楽しむのもまた一興)

次回はこっちでやっていきます。

https://quarkus.io/guides/resteasy-reactive

【Canva】ブログ用テンプレ素材をCanvaで作ってみた

はじめに

こんにちは!21卒デザイナーの山崎です。

今回は「テックブログのアイキャッチ画像をCanva化+テンプレ化して、誰でも簡単にアイキャッチ画像を作れるようにした話」をしようと思います。

Figmaでテンプレを作る問題点

今までブログ用アイキャッチ画像のテンプレートはあったのですが、そのデータ全てはFigmaにあった為、誰でも使えるという状況ではありませんでした。

これまで投稿した記事の表紙がFigmaに格納されています。

学生時代に自作した素材たちです。これを使って普段アイキャッチ画像を制作しています。

Figmaでテンプレートを作る問題点は2点ありました。

Figmaを誰でも自在に使えるわけではない

②権限問題で誰でも編集できるわけではない

①はFigmaはエンジニアやビジネスにとってはハードルが高いツールであり、テンプレートが置いてあっても「誰でも」「すぐに」使えるという点においてFigmaは適したものではありませんでした…

②はエキサイトではFigmaを企業用の「organization plan」で使っており、編集権限はクリエイティブ職のみなのでエンジニア・ビジネスはこのデータにアクセスしても閲覧しかできず、「データをコピーして一旦自分のワークスペースに持ち帰らなければいけない」という一手間がありました。

編集権限を与えると月一人当たり4500円かかってしまうので気軽に付与することもできず…

Canvaにした理由

Canvaを採用した理由は3点あります。

① 誰でも直感的に操作できる

②テンプレート機能で誰でも簡単に使える

②Canva Proが最大5人で使えて月200円/人

Canvaはバナー制作に特化している為、Figmaに比べて初心者にも直感的に扱うことができ、テンプレートも豊富でフォントも309種類使うことができます。(個人的にフォントワークスのフォントがあらかじめ揃ってるのはすごくよかったです)

テンプレートの制作

ただツールをCanvaに切り替えるだけではなく、テンプレートを用意して気軽にアイキャッチ画像を作れるようにしなければいけません。

そこで、Figmaにあったアイキャッチ素材をSVGで書き出してテンプレートも何種類か作ってCanvaに移行してみました。

説明文

テンプレート

Canvaにはテンプレート機能があるので、テンプレート用のリンクを使えばわざわざデータをコピー→自分のワークスペースにペーストしなくてもテンプレートを使うことができます。

終わりに

エンジニア・ビジネスの方達にCanvaを通じて手軽にクリエイティブを作れる手助けになれたら嬉しいです。

最後に、エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

GoのcobraでCLIツールを作った

はじめに

エキサイト株式会社 バックエンドエンジニアの山縣です。

既存サービスのリビルドを進めていく上で、Redisのキャッシュ操作を行うためのツールが欲しいという要望がでました。 そこで、Goのcobraを使ってキャッシュ操作を行うCLIツールを作成しました。 本記事ではcobraの使い方と実際に作成したCLIツールのサンプルについて紹介します。

環境

  • Go: 1.17
  • go-redis: 8.11.4
  • cobra: 1.3.0

cobraコマンドの導入は、下記リンクをご参照ください。

github.com

サブコマンドの作成

$ cobra init で初期設定を行い、$ cobra add [command] で任意のコマンドを追加することができます。

# 初期設定
$ cobra init
Your Cobra application is ready at
/Users/example/workspace/tutorial/go-cobra

# サブコマンドを追加
$ cobra add get
get created at /Users/example/workspace/tutorial/go-cobra

これにより、下記のようなディレクトリ構成でいくつかファイルが生成されました。

$ tree
.
├── LICENSE
├── cmd
│   ├── get.go
│   └── root.go
├── go.mod
├── go.sum
└── main.go

1 directory, 6 files

データ取得

package cmd

import (
    "context"
    "fmt"
    "log"

    "github.com/go-redis/redis/v8"
    "github.com/spf13/cobra"
)

var getCmd = &cobra.Command{
    Use:   "get",
    Short: "get the value of a key",
    Long:  "get the value of a key",
    Run: func(cmd *cobra.Command, args []string) {
        key, err := cmd.Flags().GetString("key")
        if err != nil {
            log.Fatal(err)
        }

        client := redis.NewClient(&redis.Options{
            Addr: "localhost:6379",
        })

        value, err := client.Get(context.Background(), key).Result()
        if err != nil {
            log.Fatal(err)
        }

        fmt.Print(value)
    },
}

func init() {
    rootCmd.AddCommand(getCmd)
    getCmd.Flags().StringP("key", "k", "", "cache key")
}

データ登録

package cmd

import (
    "context"
    "log"
    "time"

    "github.com/go-redis/redis/v8"
    "github.com/spf13/cobra"
)

var setCmd = &cobra.Command{
    Use:   "set",
    Short: "set the string value of a key",
    Long:  "set the string value of a key",
    Run: func(cmd *cobra.Command, args []string) {
        key, err := cmd.Flags().GetString("key")
        if err != nil {
            log.Fatal(err)
        }

        value, err := cmd.Flags().GetString("value")
        if err != nil {
            log.Fatal(err)
        }

        client := redis.NewClient(&redis.Options{
            Addr: "localhost:6379",
        })

        err = client.Set(context.Background(), key, value, time.Minute).Err()
        if err != nil {
            log.Fatal(err)
        }
    },
}

func init() {
    rootCmd.AddCommand(setCmd)
    setCmd.Flags().StringP("key", "k", "", "cache key")
    setCmd.Flags().StringP("value", "v", "", "cache value")
}

実行

バイナリファイルとして利用するため、まずはビルドした後に実行します。

# ビルド
$ go build

# データを登録
$ ./cache set --key 'item' --value 'test-value'

# データを取得
$ ./cache get --key 'item'
// test-value

作成したCLIを使用して、データの登録および取得ができることを確認できました 。

ロスコンパイル

開発環境はMacですがLinux環境で動作することを想定しています。 Goではクロスコンパイルができるため、Linux環境で動作するようにコンパイルしています。

$ GOOS=linux GOARCH=amd64 go build

おわりに

cobraの使い方と実際に作成したCLIツールのサンプルについて紹介しました。 実際に業務で使用するためには、ホストやポートの指定、タイムアウト、取得できなかったときの処理など考慮する必要があります。 cobraを使うことで、かんたんにコマンドを作成することができるため、今後も積極的に使っていきたいです。

MySQLの自動Index選択にどこまで任せるべきか

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

MySQLでは、SQL実行時に適切なIndexを自動的に選んでくれる機能(オプティマイザ)があります。 ただこれは、万能というわけではありません。

今回は、手動でIndexを選んだ方が早い例を挙げ、そのことを確認していきます。

なお今回は、 MySQL5.7 環境にて確認を行っています。

MySQLオプティマイザ

MySQLでは、こちらから何も指定しなくても、実行するSQLをもとに検索対象のテーブルに存在するIndexを自動的に選択してくれます。 これは、オプティマイザという機能によって実現されています。

実行するSQL

EXPLAIN
SELECT
    *
FROM
    test
WHERE
    test.code = "test_code";

Explain結果

id select_type table partitions type possible_keys
1 SIMPLE test const test_index
key key_len ref rows filtered Extra
test_index 302 const 1 100.00

上記の例の場合は、 SQLtest.code カラムを定数で検索するものであり、かつ test.code カラムにIndexが存在するため、自動的に test.code カラムのIndexを選択してくれています。

このように簡単なSQLであれば適切にIndexを選択してくれますし、ある程度複雑でもオプティマイザに任せておけば問題ない場面は多いです。

ただし、常にオプティマイザに任せていればいいかというとそうではありません。

オプティマイザでは不適切なパターン例

例えば、「指定期間内に公開されている、特定の提供元・カテゴリの記事を取得するSQL」を考えてみます。

実行するSQL

EXPLAIN
SELECT
    article.*

FROM article

# packageという単位で提供元・カテゴリを組み合わせている
INNER JOIN master_package_list
    ON article.source = master_package_list.source
    AND article.category = master_package_list.category

WHERE
    # 指定期間内で有効な記事を取得する
    article.deleted_at IS NULL
    AND article.status = 1
    AND article.publish_start_date BETWEEN (NOW() - INTERVAL 3 MONTH) AND NOW()
    AND article.publish_end_date >= NOW()

    AND master_package_list.package = "sample_package"

# 最後に記事公開日順に並び替える
ORDER BY article.publish_start_date DESC

LIMIT 10;

オプティマイザに任せると以下のようなExplain結果になります。

Explain結果

(わかりやすさのため、 possible_keysref は一部省略しています)

id select_type table partitions type possible_keys
1 SIMPLE master_package_list ref package_index,source_category_index
1 SIMPLE article ref sample_index,publish_start_date_index
key key_len ref rows filtered Extra
package_index 62 const 40 100.00 Using temporary; Using filesort
sample_index 206 master_package_list.source,master_package_list.category,const,const 885 9.23 Using index condition; Using where

rowsfiltered を見る限り、そこまで大きな問題があるようには見えません。

ただしこのSQLでは ORDER BY article.publish_start_date DESC の並び替えを行っており、このカラムが今回のIndexには適切に含まれていないために、 Using temporary; Using filesort (一時テーブルに保存して並び替え)が動いてしまっています。

では今度は、手動でIndexを指定してみましょう。 なお変更部分は、 USE INDEX 部分のみです。

実行するSQL

EXPLAIN
SELECT
    article.*

FROM article
    # article.publish_start_date だけのIndexを指定
    USE INDEX (publish_start_date_index)

INNER JOIN master_package_list
    ON article.source = master_package_list.source
    AND article.category = master_package_list.category

WHERE
    article.deleted_at IS NULL
    AND article.status = 1
    AND article.publish_start_date BETWEEN (NOW() - INTERVAL 3 MONTH) AND NOW()
    AND article.publish_end_date >= NOW()

    AND master_package_list.package = "sample_package"

ORDER BY article.publish_start_date DESC

LIMIT 10;

Explainは以下のようになります。

Explain結果

(わかりやすさのため、 possible_keysref は一部省略しています)

id select_type table partitions type possible_keys
1 SIMPLE article range publish_start_date_index
1 SIMPLE master_package_list ref package_index,source_category_index
key key_len ref rows filtered Extra
publish_start_date_index 5 99670 0.33 Using index condition; Using where
source_category_index 199 article.source,article.category 1 3.10 Using where

rowsfiltered だけを見ると、先程のExplain結果に比べて明らかに悪化しているように見えます。 改善点で言えば、 Using temporary; Using filesort がなくなったくらいでしょうか。

上記の2つを見比べると、一見前者のIndexを選ぶのは妥当だと思うかもしれません。

ですが実際は、 SQLの実行速度は圧倒的に後者が早い という結果になるのです。

これは、今回のテーブルの内容であれば「最後の並び替えのコスト」が「最初・途中の絞り込みのコスト」に比べて大きいために、絞り込みを効率化するよりも最初からソートされているIndexを使った方が実行速度としては早かった、ということが原因として考えられます。

もちろんこれは逆に言えば、 article テーブルに保存されているレコード数や master_package_list のデータ構造が異なっていれば、同じSQLであってもオプティマイザの選定の方がSQLの速度が早い場合もありえるということになります。

ただ、この結果から、

  • オプティマイザはある程度合理的にIndexを選択するが、それが必ずしもSQL実行速度を最速化する結果につながるとは限らない

ということがわかります。

最後に

オプティマイザは、MySQLにおいてとても有用な機能です。 ですが、SQL文やそこで使用するテーブルのデータ構造、レコード数等によっては、手動でIndexを指定したほうが実行速度が早くなる場合も存在します。

SQLを実行してみて速度が遅いと感じた場合は、「手動でIndexを選択することも手の一つである」として認識しておくと良いのではないでしょうか。

Spring Bootで、DBのPrimary/ReplicationインスタンスにSQLを振り分ける2つの方法

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

DBでは、可用性の担保のために PrimaryReplicationインスタンスをそれぞれ用意することが多々あります。 こうしたDBをSpring Bootで扱う際、適切にSQL実行先を振り分けないとせっかく分けた意味がなくなってしまいます。

今回は、適切に Primary / Replication インスタンスSQLを振り分ける方法を2通り紹介します。

DBの可用性と Primary / Replication インスタンス

DBは、多くのアプリケーションにとって、アプリケーション上で扱うデータが入っている重要なサービスであり、DBに障害が起きてしまった場合サービス自体に障害が起きてしまうケースがほとんどです。 そのため、DBの可用性を高めることは、DBを扱うアプリケーションにとってはかなり重要性の高い要件と言えます。

DBの可用性を高める方法の1つに、 Primary / Replication インスタンスを別々に用意する、というものがあります。

Primary インスタンスではDBへのデータの書き込み、及び読み込みの両方が可能であり、常にオリジナルのデータが入っています。 データの整合性を保つため、基本的に Primary インスタンスは1台のみで、かつ書き込みはこのインスタンスのみにしか行なえません。

Replication インスタンスではデータの読み込みのみが可能であり、 Primary インスタンスのデータを常時同期するようになっています。 Replication インスタンスではあくまで複製されたデータのみを扱い、かつ読み込みのみを行うため、 Primary インスタンスと異なり複数台作ることが可能です。

多くのアプリケーションでは書き込みに対して読み込みの量のほうが圧倒的に多いため、上記の構成にした上で、基本的に Primary インスタンスでは書き込みのみ、 Replication インスタンスでは読み込みのみを行うようにし、アクセス量に応じて Replication インスタンスの数を増やすことで負荷を分散して、可用性を担保します。

Spring Bootで上記のような構成のDBを扱う際も、もちろん Primary インスタンスでは書き込みのみ、 Replication インスタンスでは読み込みのみを行うようにする必要があります。

Spring Bootで Primary / Replication インスタンスを持つDBを扱う方法

Spring BootでこういったDBを扱う際は、 JDBC の機能を使用する方法と、 DataSource クラスをカスタマイズする方法の2通りが主に存在します。

JDBCの機能を使用する方法

おそらく最も簡単な方法が JDBC の機能を使用する方法です。

JDBCには replication の機能があり、以下の2工程でPrimary / Replication に適切に振り分けてくれるようになります。

1. @Transactional を使用する

Primary インスタンスにアクセスしてほしいメソッドには @Transactional() アノテーションを、 Replication インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true) アノテーションを付与する

2. アクセス先DBのURLを以下のように指定する

jdbc:mysql:replication://primary_db:3306,replication_db:3306/sample_schema

これだけで、適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。

メリット

  • 非常に簡単に指定できる

デメリット

  • JDBCのreplication機能が使えない場合は使用できない
  • コネクションプールで保持されるコネクション数が PrimaryReplication で同数になってしまう
    • Primary インスタンスは1台しか作れないため、 Primary の最大同時接続数にコネクション数を合わせると Replication 用のコネクションが不足したり、 Replication で必要なコネクション数に合わせると Primary の最大同時接続数が足りなくなってしまう恐れがある

DataSource クラスをカスタマイズする方法

DataSouce を使う方法は少し複雑ですが、JDBCに比べて柔軟な対応が可能になります。

DataSource を使う場合も、JDBCと同じく @Transactional で振り分け先を指定します。 変わるのはその後です。

1. @Transactional を使用する

Primary インスタンスにアクセスしてほしいメソッドには @Transactional() アノテーションを、 Replication インスタンスにアクセスしてほしいメソッドには @Transactional(readOnly = true) アノテーションを付与する

2. DataSource 周りをカスタマイズする

DataSource 周りをカスタマイズします。 ここでは HikariDataSource を使用していますが、おそらく他の DataSource を使っても大丈夫だと思います。 また、MaximumPoolSize 等の細かい設定も、必要に応じて変更してください。

今回はコード内に直接各種プロパティを書いていますが、実際に使用する際はそういったプロパティは application.properties などに書いておき、このコード内で呼び出すようにするほうが管理しやすいでしょう。

import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionSynchronizationManager;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import javax.sql.DataSource;

@Configuration
public class DataSourceConfig {
    private final String primaryDataSourcePropertiesName = "primaryDataSourceProperties";
    private final String replicationDataSourcePropertiesName = "replicationDataSourceProperties";
    private final String primaryDataSourceName = "primaryDataSource";
    private final String replicationDataSourceName = "replicationDataSource";
    private final String routerDataSourceName = "routingDataSource";
    private final String mainDataSourceName = "dataSource";

    public enum DataSourceType {
        PRIMARY, REPLICATION
    }

    /**
     * TransactionのreadOnlyをもとにDataSourceをルーティングするためのカスタムルーティングクラス
     */
    public static class CustomRoutingDataSource extends AbstractRoutingDataSource {
        @Override
        protected Object determineCurrentLookupKey() {
            return TransactionSynchronizationManager.isCurrentTransactionReadOnly()
                    ? DataSourceType.REPLICATION
                    : DataSourceType.PRIMARY;
        }
    }

    /**
     * Primary用DataSourceのプロパティを作成
     * @return Primary用DataSourceのプロパティ
     */
    @Bean(name = primaryDataSourcePropertiesName)
    @Primary
    public DataSourceProperties primaryDataSourceProperties() {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSourceProperties.setUsername("user");
            dataSourceProperties.setPassword("password");
            dataSourceProperties.setUrl("jdbc:mysql://primary_db:3306/sample_schema");

            return dataSourceProperties;
    }

    /**
     * Replication用DataSourceのプロパティを作成
     * @return Replication用DataSourceのプロパティ
     */
    @Bean(name = replicationDataSourcePropertiesName)
    public DataSourceProperties replicationDataSourceProperties() {
            DataSourceProperties dataSourceProperties = new DataSourceProperties();
            dataSourceProperties.setDriverClassName("com.mysql.cj.jdbc.Driver");
            dataSourceProperties.setUsername("user");
            dataSourceProperties.setPassword("password");
            dataSourceProperties.setUrl("jdbc:mysql://replication_db:3306/sample_schema");

            return dataSourceProperties;
    }

    /**
     * Primary用DBエンドポイントのDataSourceを作成
     * @return Primary用DBエンドポイントのDataSource
     */
    @Bean(name = primaryDataSourceName)
    @Primary
    public DataSource primaryDataSource(@Qualifier(primaryDataSourcePropertiesName) DataSourceProperties dataSourceProperties) {
        HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        hikariDataSource.setMaxLifetime(600000);
        hikariDataSource.setMaximumPoolSize(10);
        hikariDataSource.setReadOnly(false);

        return hikariDataSource;
    }

    /**
     * Replication用DBエンドポイントのDataSourceを作成
     * @return Replication用DBエンドポイントのDataSource
     */
    @Bean(name = replicationDataSourceName)
    public DataSource replicationDataSource(@Qualifier(replicationDataSourcePropertiesName) DataSourceProperties dataSourceProperties) {
        HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        hikariDataSource.setMaxLifetime(600000);
        hikariDataSource.setMaximumPoolSize(20);
        hikariDataSource.setReadOnly(true);

        return hikariDataSource;
    }

    /**
     * SQLを実行するDataSourceを、Transactionによってルーティングするための設定を作成
     * @param primaryDataSource Primary用DBエンドポイントのDataSource
     * @param replicationDataSource Replication用DBエンドポイントのDataSource
     * @return SQLを実行するDataSourceを、Transactionによってルーティングするための設定
     */
    @Bean(name = routerDataSourceName)
    public DataSource routingDataSource(
            @Qualifier(primaryDataSourceName) final DataSource primaryDataSource,
            @Qualifier(replicationDataSourceName) final DataSource replicationDataSource
    ) {
        CustomRoutingDataSource routingDataSource = new CustomRoutingDataSource();
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put(DataSourceType.PRIMARY, primaryDataSource);
        dataSourceMap.put(DataSourceType.REPLICATION, replicationDataSource);

        routingDataSource.setTargetDataSources(dataSourceMap);
        routingDataSource.setDefaultTargetDataSource(primaryDataSource);

        return routingDataSource;
    }

    /**
     * TransactionによってDataSourceをルーティングするDataSourceを作成
     * @param routingDataSource SQLを実行するDataSourceを、Transactionによってルーティングするための設定
     * @return TransactionによってDataSourceをルーティングするDataSource
     */
    @Bean(name = mainDataSourceName)
    public DataSource dataSource(@Qualifier(routerDataSourceName) DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    /**
     * トランザクションマネージャを作成
     * @param dataSource TransactionによってDataSourceをルーティングするDataSource
     * @return トランザクションマネージャ
     */
    @Bean
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(mainDataSourceName) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    /**
     * SQLセッションファクトリを作成
     * @param dataSource TransactionによってDataSourceをルーティングするDataSource
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean
    @Primary
    public SqlSessionFactory sqlSessionFactory(@Qualifier(mainDataSourceName) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        return sqlSessionFactoryBean.getObject();
    }
}

少し長くなりましたが、この設定で適切にSQLを割り振ってくれます。 これには以下のメリット・デメリットが存在します。

メリット

  • JDBCのreplicationを使えなくても使用することが出来る
  • Primary / Replication でコネクションプールのコネクション数などを完全に別々に設定することが出来る

デメリット

  • 設定が少し複雑

最後に

今回は、 Primary / Replication 構成のDBにSpring Bootからアクセスする方法を2つ紹介しました。 個人的には、

  • アクセスがかなり少なく、JDBCのreplicationが使えるアプリケーション:JDBCを使用する方法
  • それ以外:DataSource をカスタマイズする方法

が適切だと思っており、そのため、多くの場合 DataSource をカスタマイズする方法が適切なのではないかと考えています。

もちろん上記で挙げた以外の思わぬ副作用がある可能性もありますし、アプリケーションごとに様々な要件もあると思いますので、一概に「どちらがいい」と言うのは難しいところです。 このあたりは、各アプリケーションごとに判断していただけると良いかと思います。

この記事が何かしら参考になりましたら幸いです。

JEP378を読んで、テキスト変換の方法をおさらいする

エキサイト株式会社エンジニアの佐々木です。Java17が昨年リリースされ、JEP378のテキストブロックがLTSとして入ったので改めてJEP378を読むとテキスト変換の方法がいくつか書いてあったので、復習がてらおさらいします。

openjdk.java.net

テキストブロックとは?

Javaには複数行のテキストをまとめて扱える仕様がありませんでした。複数行ある場合は、1行ずつ文字列を連結する必要がありました。

String s = "<h2>%s</h2>" +
                "<h3>%s</h3>" +
                "";

テキストブロックは、これをまとめて扱えるようにする仕様変更になります。""" 文字列 """ このような形で記述します。

String s = """
                <h2>%s</h2>
                <h3>%s</h3>
                """

ちょっとしたSQLやテンプレートなど複数行で書きたいことは多いと思います。ちょっと便利になりました。では、テキストブロック内に変数を入れたい場合はどうするのかを見ていきましょう。

前提

リクエストパラメータで設定したパラメータでH2H3をそれぞれ表示する簡単なWebアプリケーションを作ります。下記のようなリクエストとレスポンスになる想定です。

curl http://localhost:8080?h2=hogehoge&h3=fugafuga

<h2>hogehoge</h2>
<h3>fugafuga</h3>

String.format を使用する

JDK1.0からあるメソッドになります。String.format(String s , args...) の形になります。書いてみて結構読みづらいので、他の方法がある場合はそちらを見たほうがいいですね。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3) {

        return String.format("""
                <h2>%s</h2>
                <h3>%s</h3>
                """, h2, h3);
    }
}

String.formatted を使用する

テキストブロック内の文字列を変数で置換したいときには、formattedメソッドを使います。テキストブロック内の%sを置き換えてくれます。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3) {

        return """
                <h2>%s</h2>
                <h3>%s</h3>
                """
                .formatted(h2, h3);
    }
}

今回は引数が2つなのでこれでいいですが、引数が多くなってくると対応付けが煩雑になってきます。

String.replace を使用する

変換する変数が多くなってきた場合は、JEP378JEP 378: Text Blocksにも記載がありますが、String#replaceで代替するようです。

@RestController
public class DemoController {

    @GetMapping
    public String index(@RequestParam String h2, @RequestParam String h3){

        return """
                <h2>$h2</h2>
                <h3>$h3</h3>
                """
                .replace("$h2", h2)
                .replace("$h3", h3);
    }
}

Kotlinのようにはいかないですが、変数が多い場合の解決方法としてはいいかなと思います。

まとめ

JEP378を見てみると使い方に関することが結構書かれていて勉強になりました。Javaは半年に1度アップデートがかかりますので、今後もウォッチしていこうと思います。

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。

カジュアル面談はこちらになります! meety.net

募集職種一覧はこちらになります!(カジュアルからもOK) www.wantedly.com

【Figma】デザインのアクセントに!バナーにテクスチャを入れる方法

はじめに

エキサイト21卒デザイナーの山﨑です。

今回はFigmaでのバナー制作においてのちょっとした小技「バナーにテクスチャを貼り付けて質感を出す方法」について書こうと思います。

この間制作したバナーを例として解説していきます。(※諸事情で画像加工済です。)

こちらはテクスチャをつけてない場合のバナーです。少しのっぺりした印象ですね…🤔

【用意するもの】

  • バナー(背景は明るめに設定)

  • テクスチャ(紙・フェルトなど素材は何でもOK。商用利用可能なフリー画像サイトやAdobe stockなどで調達します。)

①写真をクリックしてレイヤースタイルを「multiply(乗算)」に変更します。

②そのまま写真をバナーに重ねます。

③写真をレイヤーの一番下に配置すれば完成。

完成版はこちらです。

いかがでしょうか。テクスチャを乗せる前と比べて質感が出るようになりました。

他にもフェルトっぽい質感のテクスチャを乗せて温かみのある印象を与えたり、色々な活用ができると思います。

ぜひバナー制作でご活用ください!

最後に

エキサイトではデザイナー、フロントエンジニア、バックエンドエンジニア、アプリエンジニアを絶賛募集しております!

興味があれば連絡いただければと思います🙇‍♀️

それではまた!

www.wantedly.com

Nimで無理やりMeCab連携してみる

前置き

おはこんばんにちは!
Nim言語大好きな人です。

前回、無理やりPHPでMeCabを動かしました
なので今回は無理やりNimでMeCabを動かしていこうと思います!

MeCabの導入などは、以前の「PHPを使って形態素解析と文章の類似度を出してみる」をご覧くださいませ。

NimとMeCabの連携

import std/osproc

let text = "すもももももももものうち"

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

以上です!!

……と、やってしまうとめっちゃ短いので、ちょっと改造していきます。

エスケープ処理

import std/osproc

let text = "すもももももももものうち"

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

上記のコードだと変数textに、let text = "hoge; ls -la; exit 0"みたいな文字を指定されてしまうと、任意のコマンドが実行できてしまいます。
これはよくない!

ということでエスケープします。

import std/osproc, std/strutils

let text = escape("hoge; ls -la; exit 0")

let result = execCmdEx("echo " & text & " | mecab")

echo result.output

hoge    名詞,固有名詞,組織,*,*,*,*
;       名詞,サ変接続,*,*,*,*,*
ls      名詞,一般,*,*,*,*,*
-       名詞,サ変接続,*,*,*,*,*
la      名詞,一般,*,*,*,*,*
;       名詞,サ変接続,*,*,*,*,*
exit    名詞,一般,*,*,*,*,*
0       名詞,数,*,*,*,*,*
EOS

これでエスケープされました!

では今回はこのあたりで!