SolrJを使ってSolr検索をする

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

エキサイトホールディングス Advent Calendar 2021の18日目の記事です。

qiita.com

今回は、Spring Boot プロジェクトでSolrJを使ってSolr検索を実装する手順について説明します。

SolrJ

SolrのJava用のAPIです。 solr.apache.org

Spring Bootには、Solr用のSpring Dataがありますが、新規プロジェクトへの利用は非推奨です。

spring.io

This project is about to move to the Spring Attic and is not recommended for new projects. The last Release (4.3.0) will, as part of the spring data release 2020.0, see patch updates till mid 2022.

そういった経緯もあり、今回はSolrJを採用しました。

実装

build.gradle に下記の行を追加します。

implementation "org.apache.solr:solr-solrj:x.xx.x" 

SolrスキーマJavaで定義します。

@Fieldスキーマのフィールド名からJavaのプロパティーへの変換を設定します。

@Data
public class BookSolrModel {

    @Field("book_id")
    private String bookId;

    @Field
    private String name;

    @Field
    private String description;
}

次に、リクエストからレスポンスの取得です。Spring BootのRepository層での実装例です。

SolrQueryをインスタンス化し、クエリーをセットします。

Solrから取得したBookSolrModelをドメインモデルにマッピングしています。

HttpSolrClient.query()はThrowableなので、例外ハンドリングをしています。

@Repository
@RequiredArgsConstructor
public class BookRepositoryImpl implements BookRepository {

    private final HttpSolrClient httpSolrClient;

    @Override
    public List<BookModel> getBookList(String bookId) {
        try {
            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getResults());

            return bookSolrModelList
                    .stream()
                    .map(bookSolrModel ->
                            new BookModel()
                                    .setBookId(bookSolrModel.getBookId())
                                    .setName(bookSolrModel.getName())
                                    .setDescription(bookSolrModel.getDescription())
                    )
                    .collect(Collectors.toList());

        } catch (SolrServerException | IOException e) {
            return List.of();
        }
    }
}

HttpSolrClientは @Bean によりDIコンテナに登録することで、他のRepositoryでもインスタンスを使い回せるようにしています。

Solrのホストはapplication.yml で定義しておき、 @Value で呼び出します。これにより環境の差異をコードに記載しなくて済みます。

@Configuration
public class SolrConfig {
    @Value("${solr.host}")
    private String solrHost;

    @Bean
    public HttpSolrClient httpSolrClient() {
        return new HttpSolrClient.Builder(solrHost).build();
    }
}

類似要素検索

MoreLikeThisを使った類似検索の例です。

            final SolrQuery query = new SolrQuery();
            query
                    .setQuery("book_id:" + bookId)
                    .setStart(0)
                    .setRows(1)
                    .setMoreLikeThis(true)
                    .setMoreLikeThisFields("name")
                    .setMoreLikeThisCount(3);

            final QueryResponse response = httpSolrClient.query(query);

            final List<BookSolrModel> bookSolrModelList = httpSolrClient
                    .getBinder()
                    .getBeans(BookSolrModel.class, response.getMoreLikeThis().get(bookId));

最後に

簡単にですが、Spring BootプロジェクトでのSolrJの使い方について説明しました。 他の言語で提供されているSolr APIと大きな差はなく、使いやすいものでした。 参考になれば幸いです。

エキサイトでは、エンジニア募集を随時行っております。 www.wantedly.com

引き続きエキサイトホールディングスのアドベントカレンダーをお楽しみいただければ幸いです。 qiita.com

参考

solr.apache.org

SpringBootでEventListenerの実装

エキサイト株式会社エンジニアの佐々木です。2021年Exciteホールディングスアドベントカレンダー17日目を担当させていただきます。

SpringBootでのイベントリスナーの実装についてです。

はじめに

SpringBootでは、アノテーションを用いたイベントリスナーの実装が比較的簡単にできるかと思います。

コード

今回は、イベント、パブリッシャー、リスナーと3つのクラスを書いていきます。

イベント

イベントのオブジェクトのコードは下記になります。

public class DemoEvent extends ApplicationEvent {
    private String message;

    public DemoEvent(Object source, String name) {
        super(source);
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventを継承して、あとはそれに必要な要素やメッセージを取得するメソッドをつけてあげれば実装完了です。

パブリッシャー

イベント発火を管理するパブリッシャーは下記のように記載します。

@Component
@AllArgsConstructor
public class DemoPublisher {

    private final ApplicationEventPublisher publisher; // Springで用意されている

    public void publishEvent(String name){
        publisher.publishEvent(new DemoEvent(this, name));
    }
}

Spring側で用意されているApplicationEventPublisherをDIし、そこにイベントのオブジェクトをセットするメソッドを実装しておきます。

リスナー

リスナーになります。パブリッシャーが発火したイベントを受け取るオブジェクトになります。

@Component
@AllArgsConstructor
@Slf4j
public class DemoListener {

    @EventListener // Springで用意されているアノテーションを付与
    public void onEvent(DemoEvent demoEvent){   // 引数にイベントのオブジェクトを設定する
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp()
        );
    }
}

リスナークラス自体を@ComponentでDIコンテナに登録し、実行したいメソッドに、@EventListnerを付与し、引数にイベントオブジェクトを設定します。

パブリッシャーの呼び出し

パブリッシャーを呼び出す処理を記述します。

@RestController
@RequestMapping
@AllArgsConstructor
public class DemoController {

    private final DemoPublisher publisher;  // パブリッシャーをDIする

    @GetMapping
    public void index(){
        publisher.publishEvent("index");
    }

}

今回は、コントローラにリクエストがあったら、パブリッシャーを呼んでもらう実装にしています。

実行

$ curl http://localhost:8080/

2021-12-17 07:59:16.248  INFO 14746 --- [nio-8080-exec-1] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639695556247 

このようなログが出力されます。ちゃんとDemoListenerからログがでていることがわかります。

リスナーを増やす

アプリケーションが大きくなってきて、リスナーの処理を増やしたいが、別々で処理したいとします。その場合パブリッシャーとリスナーをそれぞれ修正せずとも、リスナーだけ増やせば可能になります。

@Component
@AllArgsConstructor
@Slf4j
public class Demo2Listener {  //クラス名変更

    @EventListener
    public void onEvent2(DemoEvent demoEvent){  // メソッド名も変更(変更しなくてもOK)
        log.info("class: {} , event: {} , timestamp: {} ",
                new Object(){}.getClass().getEnclosingClass().getName(),
                demoEvent.getMessage(),
                demoEvent.getTimestamp());
    }
}

これで、パブリッシャーに気付かれずにリスナーを増やすことが可能になりました。

$ curl http://localhost:8080/

2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.e.listner.Demo2Listener              : class: com.example.excite20211212.listner.Demo2Listener , event: index , timestamp: 1639696174669 
2021-12-17 08:09:34.669  INFO 15034 --- [nio-8080-exec-2] c.e.excite20211212.listner.DemoListener  : class: com.example.excite20211212.listner.DemoListener , event: index , timestamp: 1639696174669 

まとめ

SpringのDIコンテナのおかげで、比較的簡単にイベント処理を疎結合に実装することが可能になりました。

おまけ

イベントは、ApplicationEventの継承がなくても動作しました。

public class DemoEvent  {
    private String message;

    public DemoEvent(Object source, String name) {
        this.message = name;
    }

    public String getMessage(){
        return this.message;
    }

}

ApplicationEventは、実行時間やシリアライズのインタフェースを実装してくれているので、継承はしていた方がいいと思いますが、DIがよしなに解釈して実行できていました。

最後に

エキサイトホールディングスアドベントカレンダー17日目を担当させていただきました。読んでいただいてありがとうございます。引き続き、アドベントカレンダーをお楽しみいただけると幸いです。

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

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

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

Illustratorのシェイプ形成ツールが便利!

f:id:designsazuka:20211216140127p:plain

はじめに

こんにちは!エキサイトのデザイナーのSAZUKAです。 今年もあと2週間ちょっとで終わりだなんて信じられません… 今年は毎日のようにfigmaを使っていたなと思います。

なのでfigmaについて語ろうか迷ったのですが…今日はIllustratorの話を少しさせてください(唐突)。 Illustratorのツールでいちばん好きなツールがありまして。

\シェイプ形成ツール/

こいつこそIllustratorを使う意味を見出せるのではないかと私は思っています( ˘ω˘ )🤘

調べた限りでは今現在figmaにはこの機能はないと思われます。

重なっている図形はこう作られている

シェイプ形成ツールを使えば、よくある知恵の輪のような図形も一瞬でできます。 f:id:designsazuka:20211216164320p:plain

重なった中抜きした円を2つ用意します。 f:id:designsazuka:20211216164749p:plain

今回は茶色が上に重なるようにしたいので、茶色い円をクリックしスウォッチのカラーを茶色にしておきます。(後からでも変えられます) f:id:designsazuka:20211216164752p:plain

二つの円を選択し、シェイプ形成ツールをクリック。 f:id:designsazuka:20211216164755p:plain

カーソルが触れたところが網掛け状になるので、上部の重なったところでプラスマークが表示されたらクリックします。 f:id:designsazuka:20211216164757p:plain

完成です!! f:id:designsazuka:20211216164800p:plain

オリジナルのロゴを作るときにも活躍

オリジナルのロゴを作るときにも活躍します。 今回はexciteの”e”を使ってぽてっとしたロゴマークを作ってみます。(探せば似たようなフォントはすでにありますがw)

あらかじめ少し太めのフォントを用意し、アウトライン化します。 まずは”e”の先端を丸くしたいので、同じ幅の円を重ねます。 (細かい部分は割愛させていただきます…) f:id:designsazuka:20211216164931p:plain

”e”と円を選択し、シェイプ形成ツールをクリック。 網掛け状になった部分は不要になるので消していきます。 altを押すとマイナスマークが出るのでクリック。 f:id:designsazuka:20211216164934p:plain

同様に左側のとんがりも削除するとこんな形になります。 f:id:designsazuka:20211216164937p:plain

あとはその繰り返しです。 内側の3箇所は逆に凹んだとんがりなので、今度は足してあげます。 f:id:designsazuka:20211216164940p:plain

拡大するとこんな感じ。 円と文字部分の隙間にカーソルを持っていくとプラスマークが出るので クリックします。 f:id:designsazuka:20211216164943p:plain

円を削除するとこのようになりました。 f:id:designsazuka:20211216164946p:plain

水色の部分は黒に修正すれば完成です! f:id:designsazuka:20211216164928p:plain

いかがでしたでしょうか?少し味のある”e”のロゴマークが出来上がりました。

シェイプ形成ツールは他にも出来ることがあるのですが、今回は実践的な内容でお届けしてみました。 Illustratorの自由度は本当に素晴らしいですね。想像力が無限に広がります。興味のある方は是非試してみてください٩( ᐛ )و

MySQLの機能でスロークエリ関連の解析を行う方法の紹介

はじめに

XTechグループ Advent Calendar 2021の16日目は、iXIT株式会社 エンジニアの蝦名がお送りします。
最近ハマっているものは音楽系Vtuberです。VIRTUAFREAK良かった…。

qiita.com

本題

ツールなどを導入しなくてもSlowQueryを解析できる機能がMySQLには存在するので、今回はその一部を紹介します。
ちなみに私が開発しているサービスのMySQLバージョンは5.6です。

1. mysqldumpslow

一言で言うとスロークエリーログファイルを解析して内容のサマリーを出力してくれる機能です。
前提としてスロークエリーログを出力している必要があります。

使い方

コマンド
※合計実行時間が長い順に10件のSQLを出力する
mysqldumpslow -s at -t 10 /opt/fio1/slog/sp-prd-db1-slow.log

出力例(1件):

Count: 23  Time=0.12s (2s)  Lock=0.00s (0s)  Rows=1.0 (23), host
  SELECT COUNT(*) FROM `user` WHERE user.STATUS=N AND (user.ACCESS_DATE>='S' AND user.ACCESS_DATE<='S')

本家のマニュアル

dev.mysql.com

弊サービスでは0.1秒以下のSQLをスロークエリーログに出力しており、
1時間毎のサマリーがメールとslackに送られてきます。
基本的にはmysqldumpslowの出力にある上位のSQLからチューニングしていけばいいので、重宝すると思います。

2. performance_schema

一言で言うとパフォーマンスモニタリング用のストレージエンジンです。
MySQL5.6で強化され、デフォルトで使用されるようになりました。
MySQL5.7でもっと強化されるのですが、残念ながら弊サービスのMySQLのバージョンは5.6です。

performance_schemaを使えばSlowQueryでは可視化されない

  • トランザクションの実行時間
  • 一回の実行時間は短いが、大量に実行して塵積になっているもの

などが取得できます。

使い方

MySQL5.5以上を使用していると、performance_schemaというデータベースが存在するかと思います。

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| hoge               |
+--------------------+
6 rows in set (0.00 sec)
mysql> show tables;
+----------------------------------------------------+
| Tables_in_performance_schema                       |
+----------------------------------------------------+
| accounts                                           |
| cond_instances                                     |
| events_stages_current                              |
| events_stages_history                              |
| events_stages_history_long                         |
| events_stages_summary_by_account_by_event_name     |
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
| table_io_waits_summary_by_index_usage              |
| table_io_waits_summary_by_table                    |
| table_lock_waits_summary_by_table                  |
| threads                                            |
| users                                              |
+----------------------------------------------------+
52 rows in set (0.00 sec)

performance_schemaデータベース内にはテーブルが大量にあるのですが、
それぞれどんなテーブルなのかは以下のドキュメントを参照してください。

dev.mysql.com

performance_schemaは私もあまり触れていないので、良さげな記事をいくつか紹介します。

thinkit.co.jp

tech.stmn.co.jp

本家のマニュアル

dev.mysql.com

最後に

どちらもとっつきやすさはピカイチですね。
見てみるだけでも面白いと思いますので、
知らなかった!という方はぜひご自身のサービスで遊んでみてください。

【休日にリフレッシュ】都内でオススメの美術館5選

はじめに

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

エキサイトホールディングス Advent Calendar 2021の15日目の記事を担当させていただいています。

qiita.com  

今回は、「都内でオススメの美術館5選」を紹介します。

西洋絵画や日本画より現代美術やデザインが好きなので、現代美術系の美術館・ギャラリー中心です。

なぜ美術館や展覧会に行くのか?

美術館や展覧会にはポスターデザイン、インフォグラフィックス、壁面に掲示されるキャプションデザインなど、たくさんのデザインで構成されています。

「展示の目的に適したフォントの選び方」や「展示の目的に適したインフォグラフィックス」、展覧会の構成・導線などを考えながら作品を見るととても良いインプットになるので休日には展示に足を運ぶようにしています。

六本木駅から徒歩5分「21_21 design sight」

王道中の王道、「21_21 design sight」です。建物の美しさもさることながら、展示内容はいつも斬新で面白く毎回といっていいほど通っています。

国立新美術館森美術館と比べて館内は小さめで1時間くらいでサクッと見られるので、初心者向きの美術館かなと思います。

21_21 DESIGN SIGHTでは三宅一生、佐藤 卓、深澤直人の3名がディレクターを務めており、デザイン系の展示が開催されることが多いです。

www.2121designsight.jp

清澄白河駅から徒歩9分「東京都現代美術館

現代美術の企画展が面白い美術館です。「TOKYO ART BOOK FAIR」も毎年開催地でもあります。

二階のカフェ「二階のサンドイッチ」の店内も可愛くて展示を回り終えたらそこで一服すると最高です😊

www.mot-art-museum.jp

東京駅から徒歩5分「アーティゾン美術館」

2019年7月1日にブリヂストン美術館から改名し、2020年1月18日にミュージアムタワー京橋内で新たにオープンした美術館です!

西洋美術、日本近代美術を主に収蔵している美術館ですが、現代美術の企画展なども開催しています。

なんと大学生まで入場料が無料…!

www.artizon.museum

新橋駅から徒歩3分「クリエイションギャラリーG8」

リクルートが運営しているギャラリーです。

クリエイションギャラリーG8は、日本のトップレベルのグラフィックデザインブランディングデザインを見ることができ、最先端の表現に触れることができます。

ADC展や1_WALLも展示されていて、クリエイティブ職の方なら一度はきたことがあるギャラリーではないでしょうか。

小さめのギャラリーなので、G8を見た後はガーディアンガーデン→資生堂ギャラリーgggのルーティーンで回るのがオススメです!

rcc.recruit.co.jp

新橋駅から徒歩5分「アド・ミュージアム東京

日本で唯一の広告美術館です。ここ数年あまり行けていないのですが、広告のクリエイティブがたくさん展示されていてとても楽しい美術館です。

アドミュージアム東京は日本の広告の歴史のほかにも企画展があり、世界的に評価が高い広告を見ることが見ることができます。

www.admt.jp

終わりに

いかがだったでしょうか?

美術館は面白い視点の作品を見たり美しい作品を見てリフレッシュできる場所です。気になる美術館があったらぜひ足を運んでみてください!

口下手流のプレゼンスライド制作のコツ

f:id:KAJIJI_Design:20211202185418p:plain こんにちは!SaaS事業部新卒デザイナーのかじもとです🐧
前回はプレゼンのコツとして記事を書かせていただきました(たくさんの方に見ていただけてめちゃ嬉しい)
tech.excite.co.jp


さて、今回はエキサイトのアドベントカレンダーに参加させていただきました🎄
年末や年度末は総まとめの機会、カンファレンスや発表会などなど増えますよね。なので今回は、プレゼンをする上で欠かせない「スライド制作」について書いてみようと思います。
スライドはプレゼンをする上でカンペになるものです。単に話す内容を記せばいいのですが、抑揚のない内容ではなかなか共感を得づらいですよね。 そうならないためのスライド作りのコツを、ここではお伝えします。

スライド制作の4つのコツ


① 話の流れは「つかむ・深める・ゆさぶる・動かす」

これは先輩デザイナーから教えていただいたもので、よくある起承転結と同じようなものです。以前私がLT会で使ったスライドを例に解説しましょう。
このスライドは、配属されて約2ヶ月たったビジネス職・デザイナー職の進捗報告LT会で、「配属した部署で学んだ事」をテーマに発表しました。

つかむ

つかむは、聞き手が話に引き込まれるような「なんだろう?」「続きが気になる」と思えるような内容にします。
f:id:KAJIJI_Design:20211201134037p:plain 例えばタイトルを2段階にしていて、デザインを学んでいた!のにもかかわらず「実際は学べていなかった!」と否定する入り方をしています。
これは発表内容に合わせていて、学生時代学んできたグラフィックの知識を全く活かせていない…というストーリーを表したタイトルにしました。
f:id:KAJIJI_Design:20211201132836g:plain また、配属されてから関わってきた制作物の一覧を自動スクロールさせたのですが、話のスピードに合わせて動かしたこともあり「なんだこれ?!」と思わせることができました😂

深める

深めるは、実体験や例を挙げて具体的な話をしていきます。
f:id:KAJIJI_Design:20211201134114p:plain 私の場合は、デザインが学べていなかったと感じた実体験を2つ取り上げ、具体的にどうダメだったのかをまとめました。
f:id:KAJIJI_Design:20211201134235p:plain ここでは、具体的な話を比較して伝えることで、よりわかりやすくなります。分野で一般的な考え方や理論と見比べてどうなっているか表現すると、別職種で馴染みない聞き手でも伝わりやすくなります。

ゆさぶる

ゆさぶるでは、深めるの内容からどのような行動をしたかをまとめます。前のステップで、聞き手は「こんなふうにできていなかったのか」となっているはず。ここではなるべく聞き手も一緒に「なるほど、その手があったか」と『一緒に解釈』してもらうことが大事です。
今回で言えば「デザインできていない」実体験を、どうやって乗り越えたかをスライドにしました。
f:id:KAJIJI_Design:20211201134250p:plain 文字の見た目である文字組ができていなかったため、直すための方法である図形置き換えの技を図にしています。このビフォーアフターによって、具体的にどんなアクションをしたのか追体験してもらいました。

動かす

動かすでは、最終的に聞き手に伝えた内容を覚えてもらうフェーズです。 これまで「深める」「揺さぶる」で伝えた内容は、ぜひとも実践につなげて欲しい、そのためには印象に残すことが欠かせません。ここではその熱い思いを、簡潔なメッセージにしましょう。
ただし「終わりよければ全てよし」のように、短いスローガンだけ言い渡されても印象には残りません。深める・揺さぶるで伝えたことに見出しをつけてあげる作業が「動かす」が担う役割となっています。
f:id:KAJIJI_Design:20211201134304p:plain 私の場合は、学んだことをテーマとしていたので、文字組をなんとなく作り失敗した体験の戒めとして「雰囲気で作らない」と銘打ちました。

② いいたい言葉を全部載せない

①では、スライド制作にあたる骨組みと肉付け方法をお伝えしました。次は付けた肉の削ぎ落とし作業です。
よく、資料が少なくならないように全部載せてしまう、カンペのように読み上げる文字が全て入っているスライドを見かけます。そういった見せ方が効果的な場面もありますが、LTのような短い発表では時間が足りなくなってしまいがちです。
さらにオンラインが増えた今、聞き手には発表者の身振り手振りよりも、画面に映るプレゼンスライドの情報量が多いでしょう。そこに不要な情報が紛れていたら…?おそらく聞き手はスライドに集中してしまい、発表者の言葉は流れてしまいます。
f:id:KAJIJI_Design:20211201134941p:plain 「お役所ポンチ絵」が情報量の多いスライドの代表格でしょうか。例えば、コーヒーについてまとめたスライドがあるとしましょう。
1枚のスライドで載せられる情報は「種類をまとめた表」「サイフォン式コーヒーの入れ方」といった、1要素を載せるくらいが妥当です。対して情報量が多いスライドは、コーヒーの種類も銘柄も淹れ方もいっぺんに示しているような物です。発表の短い時間の中では、情報を見るだけで手一杯になってしまいます。
f:id:KAJIJI_Design:20211201141104p:plain そうならないためにも、出てこない内容はスライドから省いてしまいましょう。1画面に載せる情報はなるべく少なくすることを意識してみてください。不安な場合は、スキップスライドや補足スライドとして別で用意するのがおすすめです。

③ 意図したグラフィックにする

デザインあるあるで「デザイナーはイケてるグラフィックを作れる」と思われがちですが、どれもきちんとした理由を持って作っています。装飾を施す引き出しが多いだけで、根本は質素なものを作っている方がほとんどだと思います。
これは②と通じていて「不必要な情報を置かない」作業と同じですが、どちらかというと「理解を促す図をわかりやすいものにする」作業になります。

私自身もグラフィック制作に慣れていないとき(今でも慣れているわけではないのですが…)、何もない余白を恐れ、開いたスペースにイラストを入れていました。しかしこれが落とし穴。スペースが空いたからといって、とりあえず埋め合わせて素材を追加したことで、明らかに「埋めました」感が出てしまうのです。
f:id:KAJIJI_Design:20211201142731p:plain 言葉だけではわからない情報(例えば表や図形など)はグラフィックとして補助となる情報源に。逆にグラフィックではわからない情報(例えば見た人によって印象が変わるもの)は文字として書き示すことが大切です。

④ 完成したらプレ・プレゼン

時間がなくなると発表練習が後回しになりがちですよね…。
前回記事の最後に「発表前に自分のプレゼンを聞き直す」とまとめたのですが、最低1回は本番と同じような環境で発表をしてみてください。プレ・プレゼンを行う目的として、作ったスライドの内容が頭からお尻までブレていないか確認する事、言いたい内容に必要な情報の過不足がないか見る事の2つが挙げられます。

1枚1枚のスライド制作をしていると具象的なところに集中してしまい、スライド全体を通しての確認を忘れてしまいがちになりますよね。最後に一度でいいので「言いたい内容が散らばりすぎていないかな?」「最初の内容が終わりではズレていないかな?」を確認する『一歩引いて客観視』をぜひしてみてください。
普段、スライドがうまくまとまらない…となる方はこちらを意識してみるとモヤモヤが解消できるかもしれません。
(スライド制作で絵コンテのようなものを作るのもおすすめです)

おわりに

ここまで長々と書きましたが、この記事でスライド制作に悩む方のお手伝いができたら嬉しいです。長文したためた甲斐が出るので…
最後に、「プレゼン」や「発表する」行為はとても緊張するものですよね。私自身も毎度そうで、緊張して膝やら指先を震わせてしまいます。

自分の中に留めておくだけではもったいないアイデアが、世の中にはたくさんあります。おそらく読んでくださった方の中にも、誰かが求めている素敵なアイデアがきっとあります。それを、外に出すきっかけの一つにプレゼンがあると思います。
ぜひ、素敵なアイデアを伝える機会として発表の場に登壇してみてください。陰ながら口下手デザイナーも応援させていただきます🚩

Spring BootのRedisキャッシュで、Master/Replicaを呼び分ける方法

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

アドベントカレンダーも14日目となり、中盤を過ぎてきました。 今回はRedisとSpring Bootの話になります。

Redisの可用性を高めるために有用な手段ですので、参考にしていただければ幸いです。

はじめに

Redisでは、同じデータに対して、書き込み・読み込みが両方可能なMasterと、読み込み専用のReplicaを別々に設定することができます。 Masterを基本的に書き込み専用に、Replicaを読み込み専用に使用することで、可用性を高くすることができます。

今回は、そのような構成になっているRedisをキャッシュとして使う際に、Spring Bootでどのように呼び分けるかを解説していきます。

RedisのMaster/Replica

Redisは、オンメモリのkey-valueなデータベースです。 MySQLなどに比べて非常に高速で、一方でリレーショナルではないため、複雑なデータ結合が不要なキャッシュなどで使用されることが多いアプリケーションとなっています。

非常に高速とはいえ、もちろん負荷が高まればアクセスを捌ききれなくなることもあります。 その際、いくつか対応方法があります。

スペックを上げる

Redisに使用するサーバ等のCPUスペックやメモリサイズを上げることで、Redisが捌けるアクセス数を増やすことができます。

数を増やす

Redisを動かしているサーバ等の数を増やすことで、1台あたりのアクセス数を減らし、結果としてRedis全体が捌けるアクセス数を増やすことができます。

どちらにするかはその時々ですが、「数を増やす」対応だと、例えば1台落ちたときに他で対応できたりなど可用性が高まりやすい利点があります。 一方でデータに不整合が出ないよう、「1台のみの書き込み・読み込み両用のMaster」と「1台以上の読み込み専用のReplica」を用意し、書き込みはMasterで、読み込みは基本的にReplicaで行うようにするため、呼び分けをする必要が出てきます。

Spring Bootでは、次の方法で呼び分けることができます。

Spring Bootでの呼び分け方法

基本的なRedisキャッシュの設定に以下の設定を加えれば、Master/Replicaで呼び分けてくれるようになります。

package sample;

import io.lettuce.core.ReadFrom;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStaticMasterReplicaConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@Configuration
@EnableCaching
public class RedisConfig {
    /**
     * Redis接続用設定のFactory
     *
     * @return Lettuce接続用設定のFactory
     */
    @Bean
    public LettuceConnectionFactory redisConnectionFactory() {
        // 環境ごとに値が違う場合は、application.yml等を使って分けてください
        String masterHost = "masterHost";
        Integer masterPort = 6379;
        String replicaHost = "replicaHost";
        Integer replicaPort = 6379;
        Integer database = 0;

        // ReadFromには他にもいくつかタイプがあるので、適したものを指定してください
        LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
                .readFrom(ReadFrom.REPLICA_PREFERRED)
                .build();

        // Amazon ElasticacheではRedisStaticMasterReplicaConfigurationが適していますが、構成によってはRedisStandaloneConfigurationが適している場合もあります
        // LINK: https://spring.pleiades.io/spring-data/redis/docs/current/reference/html/#redis:write-to-master-read-from-replica
        RedisStaticMasterReplicaConfiguration serverConfig = new RedisStaticMasterReplicaConfiguration(masterHost, masterPort);
        serverConfig.addNode(replicaHost, replicaPort);
        serverConfig.setDatabase(database);

        return new LettuceConnectionFactory(serverConfig, clientConfig);
    }
}

基本的にはこれだけで、キャッシュに書き込む時はMaster経由で、キャッシュから読み込む時はReplica経由で、それぞれアクセスできるようになります。

最後に

Redisを複数台で動かすことは、可用性を高めるために非常に有効な手の1つです。 Spring Bootの場合は上記の方法で簡単に呼び分けができるようになるので、ぜひ試してみてください。


アドベントカレンダーはまだまだ続きます! 明日以降も見ていっていただけると幸いです!

qiita.com

DBeaver でいい感じに date 型に bind parameter する方法(日付型のみ)

エキサイトホールディングス Advent Calendar 2021 の14日目は、エキサイト株式会社の大澤 が担当させていただきます。

やりたいこと

SELECT :date  FROM dual;

このとき :date に対して 2021-12-07 15:01:00 の日付型をbindして実行したい

解決方法

SELECT :date  FROM dual;

:datetimestamp '2021-12-07 15:01:00' を bind して実行する

f:id:hibikiosawa4388:20211207152726p:plain
date に timestamp xxxx を bind

詳細

弊社では 一部で Oracle の DB を利用しています。 また、 Named parameters な ORM を採用しているため、 生 SQL を取得する場合は上記のようなクエリが取れます。

このときに DatetimeImmutable 型を バインドしている日付型については、 DBeaver (GUI な DB viewer) で うまくバインドできないと思っていました。

なので下記のように to_date でクエリをいちいち書き換えていましたが面倒でした。

SELECT to_date(:date, 'yyyy-mm-dd hh24:mi:ss')  FROM dual;

操作していてたまたま timestamp '2021-12-07 15:01:00' と書くことで用が済むことを発見したためここに記します。

応用使用例 (フラッシュバックを複数同時検索)

with timestamp_all as (
  select :timestamp1 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp1)
  UNION ALL
  select :timestamp2 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp2)
  UNION ALL
  select :timestamp3 as "タイムスタンプ", x_pk, x_changed_column  from XXXXXX as of timestamp (:timestamp3)
)
select * from timestamp_all;

Github ActionsでのECSへの手動デプロイ

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

アプリケーションのデプロイ方法は、エンジニアなら誰もが頭を悩ませるものでしょう。 コマンドラインAWS CodePipeline、Jenkinsなど、様々な方法が考えられます。

今回は、ECSへのデプロイ方法としてGithub Actionsを選択したときの方法と注意点について説明していきます。

Github Actions

Github Actionsは、Github上で任意の処理を実行できる機能です。 公式には以下のように説明されています。

Automate, customize, and execute your software development workflows right in your repository with GitHub Actions. You can discover, create, and share actions to perform any job you'd like, including CI/CD, and combine actions in a completely customized workflow.

手動実行はもちろん、指定時間に実行したり、ブランチやプルリクエストに何かしらのアクションがあったタイミングで自動実行させることもできます。 例えば

  • デフォルトブランチにブランチがマージされたら、自動的にタグを切る
  • プルリクエストが新しく作られたら、ユニットテストを実行する

などが可能です。

今回やりたいのは、「手動実行」でECSにデプロイする方法となります。

ECSへのデプロイ

Github Actionsは、 .github/workflows ディレクトリ配下に実行処理を書いたYAMLファイルを置くことで設定することができます。 Github Actionsの実行単位は workflow と呼ばれ、このディレクトリ配下に置かれる1ファイルごとに1workflowが設定されることになります。

ですが、ファイル作成前にまずは下準備をします。

AWS環境

今回はECSへのデプロイということで、関連する以下のようなAWSの環境を用意しておく必要があります。

  • ECR
  • ECS
  • コード上からAWSへアクセスするためのIAMユーザ

Secretsの設定

AWSへのアクセス用のキーやSlack通知用のWebhookなどは、セキュリティ性の高いものであるため、Githubのバージョン管理に含めるべきではありません。 Github Actionsでは、Githubの「Setting -> Secrets」にて、そういった文字列を外部に見られないように保存することができます。

保存した文字列は、Github Actions実行時に ${{ secrets.保存したキー }} の形式で参照することができます。

今回は、

を作成します。

なおSlack通知はデプロイに必須ではありませんが、これがあるとデプロイの運用が容易になるため今回は含めています。

Task Definition

ECSに使用するTask Definition用のJSONファイルも用意しておく必要があります。 イメージ名は後から上書きするので適当で大丈夫ですが、それ以外については適切なファイルを作っておきます。

設定ファイル

ここまで準備できたら、ようやくGithub Actionsの設定ファイルを作成します。

name: Deploy

# 同じ文字列がconcurrencyに指定されているworkflowは、二重で実行できなくなります
concurrency: deploy

# 手動実行をさせるため、workflow_dispatchを指定してください
on:
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      ECR_REPOSITORY: sample-ecr
      CLUSTER_NAME: sample-cluster
      SERVICE_NAME: sample-service
      TASK_DEFINITION: task-definition.json
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # AWS環境へデプロイするため、認証を行います
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # Dockerイメージをビルドし、ECRにpushします。また、outputでイメージ名を出力します
      # イメージのタグ名にはコミットSHAを使用します
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          IMAGE_NAME: ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build -t $IMAGE_NAME:$IMAGE_TAG -t $IMAGE_NAME:latest .
          docker push $IMAGE_NAME:$IMAGE_TAG
          docker push $IMAGE_NAME:latest
          echo "::set-output name=image::$IMAGE_NAME:latest"

      # 出力されたイメージ名で、 あらかじめ作成しておいたTask DefinitionのJSONファイルのイメージ名を上書きします
      - name: Fill the Amazon ECS task definition with image ID
        id: task-def
        uses: aws-actions/amazon-ecs-render-task-definition@v1
        with:
          task-definition: ${{ env.TASK_DEFINITION }}
          container-name: sample
          image: ${{ steps.build-image.outputs.image }}

      # 作成したTaskDefinitionを元にECSへのデプロイを行います
      - name: Deploy Amazon ECS task definition
        uses: aws-actions/amazon-ecs-deploy-task-definition@v1
        with:
          task-definition: ${{ steps.task-def.outputs.task-definition }}
          cluster: ${{ env.CLUSTER_NAME }}
          service: ${{ env.SERVICE_NAME }}
          wait-for-service-stability: true

      # Slackへ通知します
      - name: Slack Notification
        if: always()
        uses: rtCamp/action-slack-notify@v2
        env:
          SLACK_CHANNEL: '#sample'
          SLACK_COLOR: ${{ (job.status == 'success' && 'good') || 'danger' }}
          SLACK_TITLE: "[${{ github.repository }}] にデプロイしました"
          SLACK_MESSAGE: "デプロイ結果:${{ job.status }}"
          SLACK_USERNAME: github actions
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
          MSG_MINIMAL: ref,actions url

これで、準備は完了です。

あとは、Githubの「Actions」から手動で実行すればデプロイが可能です。

注意点

Github Actionsで手動実行する際、いくつか注意点があります。

ブランチしか選択できない(解決済み!)

かつては手動実行する際に選択できるのはブランチのみでしたが、最近になってタグを選択できるようになりました! 特にデプロイではタグによって制御している場合も多いと思うので、これは非常に嬉しいアップデートです!

デフォルトブランチにマージされないと実行できない

手動実行する場合は、デフォルトブランチにYAMLファイルがマージされていないと項目が出てきません。 最初は詰まりやすいところだと思いますので、注意しましょう。

デプロイをしたブランチ / タグがどれだか分かりづらい

ブランチやプルリクエストのアクションで自動実行されるものはGithub Actionsの一覧画面で実行元のブランチやタグが表示されていますが、手動実行のものは表示されないようです。 少し手間ですが、実行ログの「Checkout -> Checking out the ref」から確認できます。

uses で指定しているライブラリではできないことがある

例えば aws-actions/amazon-ecs-deploy-task-definition@v1 では、2021/12/13現在、ECSでのデプロイ時に platform-version の指定ができないようです。 指定したい場合は、自分でAWS CLIを使うようにするなど、方法を変えると良いでしょう。

最後に

Github Actionsは、

  • Githubをバージョン管理に使用している場合、リポジトリと密接につながっているため、エンジニアが見る場所がバラけづらい
  • 完全に独立した環境のため、共用デプロイサーバのように同時実行時のCPUやメモリの心配をする必要がない
  • Github Actions用の様々なライブラリが存在するため、やりたいことを簡単に設定できる

などの利点があります。 みなさんもぜひ使ってみてはいかがでしょうか?

SESのバンス対策

エキサイトでファクタリングの開発をしている、森脇です。

今回はファクタリングで使用しているSESのバンス対策について書こうと思います。

ファクタリング事業とは

BtoB向け後払い決済・請求代行サービスです 法人向けサービスの事業拡大に取り組む企業様の①請求・回収業務の効率化、②未回収リスクの解消、③資金繰りの改善を実現します。

法人向けサービスの事業拡大に取り組む際、顧客候補に対する与信審査や、成約した顧客に対する請求・回収業務、事業運営に必要な運転資金の確保などの付随業務が発生し、100%のリソースを営業先開拓や商品開発に注力することへの阻害要因となってきました。

与信審査/請求書発行/請求書送付/入金消込/督促等の請求・回収業務など付随業務をすべて代行します。加えて、登録された請求については全額を保証し、早期の資金ニーズがある場合には取引内容の登録後、最短翌日入金も可能とし、積極的な事業拡大に取り組む企業が抱える請求・回収業務の負荷や資金繰りに関する課題を解決します。

事業自体はクレディセゾン様と協業で行っております

prtimes.jp

システム概要

システムでは、請求データを登録するだけで、後は全て自動で行うようになっています。 期日になれば、請求書が作られ、メールか郵送で請求書が送付され、入金があれば消し込みを行う、DX化がされています

請求書の送付はSESを使ってメールで送信しております、システム構成はこんな感じです。

システム構成

f:id:moriwaki111:20211213093709p:plain

SESの送信ログをSNSを通して、SQSに溜めAWS Batchを使ってキューを取得して、解析を行い結果をDBに書き込みます。

オンプレ時代の課題

・ログをメールサーバーからどうやって取得するか ・メールのログをどうやってパースするのか ・解析処理をどういうタイミングで行うのか ・ログファイルの何処から処理を開始するか ・などなど

考えることは多岐にわたり、実装も大変です、AWSのサービスを使うとそう言った煩わしい問題がほぼ解決されます。

設定

すごく簡単です、SESのNotificationsの設定にSNSのトピックを設定します。 f:id:moriwaki111:20211213093954p:plain

SNSではサブスクリプション先として、SQSを指定してあげます。 あとはSQSからデータを取り出すバッチを書いて終了です。

テスト

面倒なテストですが、そこもAWSが用意してくれています。 以下の公式ドキュメントに詳しく書かれています。

docs.aws.amazon.com

バンスの確認をしたければ、bounce@simulator.amazonses.com 宛にメールを送ればバンスの処理を行うことができるようになっています。バンス以外にも苦情系のメアドも用意されているので、細かく状況に応じて処理を行うことができるようになっています。

最後に

AWSであれば簡単に実装でき、運用もほぼ手をかけることなく安定した運用が可能になるので、アプリケーションの開発に注力することができます。 今後もAWSのいろんなサービスに触れていきたいと思います。

【社内バリュー浸透施策】バリューポスターを製作した話

f:id:excite_ny:20211206190106p:plain

はじめに

こんにちは、エキサイト21卒デザイナーの山﨑です。 エキサイトホールディングス Advent Calendar 2021 2の12日目の記事を担当させていただいています!

qiita.com

今回はエキサイトのミッション・バリューが決定したので、その周知を図る社内ポスターを製作した話をしようと思います。 私が担当したのはエキサイトのバリュー5つを5枚のポスター連作でデザインしました。

エキサイトバリュー

1. 好奇心を起点にする。

好奇心の芽を大切に探究し続けることで、今を、そして、未来を変えていこう。

2. 当事者意識でやりぬく。 

大きな視野で自分ごと化してやり抜くことで、生まれる機会を成果と成長につなげよう。

3. 世の中に寄り添う。

世の中で、そして、自分や身の回りで、何が起こり、どのような流れがあるのか。世の中に寄り添うことで、小さな変化も大きな流れも感じとろう。

4. 素直さとリスペクトで学ぶ。

物事をありのままに見て感じる心を大切に、身近なこと、遠くのこと、歴史から学ぶ。身近な人、遠くの人をリスペクトし、そして、巨人の肩に乗ろう。

5. 得意なことで繋がり合うチームワークで。

肩書き、部門、社内外。あらゆるボーダーを超えて、一人ひとりが得意なことで繋がりあるチームワークで大きな仕事を形にしていこう。

イメージボード制作+すり合わせ

まず、イメージボードでポスターのイメージを固めていきます。

なるべく路線の違う画像を3〜6パターンほど集めてジャンル分けしていき、自分が参考にしたいと思ったものなどをメモしていきます。

エキサイトのコーポレートカラーは赤・黒・白なので、赤いポスターが多めですね🤔

赤はエネルギッシュでスポーティーなイメージなので、社内を盛り上げるためのバリューポスターとは相性がよかったです。

f:id:excite_ny:20211206184424p:plain

ラフ案制作

ラフ案とも言えない何かを生成しながら進めて行きます。 「バリューを体現した社員をピックアップして作るのが良いんじゃないか?」と思いつつこねくり回した残骸です。

f:id:excite_ny:20211211014554p:plain

ラフ案と言ってもほぼ完成に近いのでこの作業が一番時間がかかります。

社員選定+アポ取り+撮影+レタッチ

デザインが決まったらモデルとなる社員12人の撮影を行います。自前の一眼レフを使って写真撮影しました。

写真が下手くそなせいで何度も撮り直したりと割とバタバタしてしまい、写真の勉強もしないとなと反省しました…😢

デザイン完成+パネル貼り

f:id:excite_ny:20211211015252p:plain

色々あってデザインが完成しました!あとは試し印刷をして色味や誤字などの最終チェックを行います!

社内には大判プリンターがないので、A3の紙を9枚につなぎ合わせたもので印刷しました!(合計紙数は90枚ほどに…)

f:id:excite_ny:20211211015503j:plain

最終調整を終えたらついに完成!

デザインから被写体の撮影、試し印刷までやるのは大学生ぶりだったので、とても懐かしい気持ちになりました。

今回のポスターで少しでもバリュー浸透に貢献できれば良いなと思います!それではまた!

AWS Secres Managerでパスワード等をセキュアに管理する

はじめに

エキサイトホールディングス Advent Calendar 2021の12日目は、エキサイト株式会社の吉川が担当させていただきます! 今回は、AWSのシークレット情報管理サービスの1つである AWS Secrets Manager について触れていきます!

AWS Secrets Manager

AWS Secrets Managerでできること

アプリケーションを構築する上で、DBにアクセスするためのユーザー名やパスワードは特に慎重に取り扱う必要があります。またアプリケーション内でAWS外部のサービスにアクセスする場合も同様で、その外部サービスにアクセスするためのパスワード等は管理に気を使う必要があります。

AWS Secrets Manager(以下AWSは省略します)はこれらのシークレット情報をセキュアに管理・共有するためのサービスです。例えば以下のような流れで利用することができます。

  1. Secrets Managerにパスワードを保存
  2. アプリケーション側はまずSecrets Managerにアクセスしてパスワードを取得
  3. 取得したパスワードを用いてアプリケーションからDBにアクセスする

アプリケーションのソースコード等とパスワードが切り離されるので、よりセキュアに運用することができるのです。

それパラメータストアでよくね?

はい。上記の説明でそう感じた方もいるのではないかと思います(というか自分がそうでした)。AWSには パラメータストア というサービスがあり、Secrets Managerと同じくセキュアにパスワードなどを管理できます。ということで両者の違いは何なのか、使ってみて比べてみました。

違い1: キーと値のペアで保存する

パラメータストアでは1つのパラメータにつき1つ以上の「値」が設定でき、Secrets Managerでは1つのシークレットにつき1つ以上の「キーと値のペア」が設定できます。図にすると以下のような違いになります。

# パラメータストア
パラメータA
 └値a
 └値b
# Secrets Manager
シークレットA
 └キーa - 値α
 └キーb - 値β

例えばDBの「ユーザー名」と「パスワード」を1つのパラメータ/シークレット内で保存したい場合、パラメータストアでは、この2つをどの順番で保存するか意識する必要があります。Secrets Managerではキー名にnamepasswordなどを設定しておけば順番問わず区別できるので、ちょっと楽ができそうです。

違い2: 認証情報の自動更新ができる

公式ドキュメントに詳しく書いていますが、標準でAmazon RDS、Amazon DocumentDB、Amazon Redshiftの認証情報の更新をサポートしています。こまめにパスワードを変更することはセキュリティ上有効な手段ですが、手動で更新することは結構面倒なので(大きなアプリケーションになると実質不可能だと思います)、これを自動で行ってくれるのはありがたいですね。パラメータストアにはこの機能はなく、LambdaなどからAPIを使って更新する必要があります。

違い3: 料金

パラメータストアは基本的に無料(パラメータ数の上限あり)ですがSecrets Managerは保存とAPIコールに料金がかかります。料金表はこちら。

実際に作ってみた

マネジメントコンソールからポチポチで作ってみました。今回は簡単のため「その他のシークレットのタイプ」で作成します。AWS外部のサービスのシークレット情報はこちらの方法で保存できます。

マネジメントコンソールから「その他のシークレットのタイプ」を選択

シークレットの中身になる、キーと値のペアを作成します。プレーンテキストではJSON形式で複数のペアを一気に作成できるので、コピペで作る場合はこちらを選ぶと良いです。

キーと値のペアを作成

シークレットの名前を設定します。どういった名前でも良いのですが、パラメータストアに倣いスラッシュ区切りでつけておくのがわかりやすいかもしれません(個人的な感想です)。

シークレット名を設定

「その他のシークレットのタイプ」でもLambdaを使って自動更新の設定をすることができます。今回は単に作るだけなので、割愛させていただきます。

シークレットの自動更新を設定する

あとはタグなどの任意の情報を設定して完了です! 作成したシークレットは以下のように確認できます。

シークレットを確認する

おわりに

いかがだったでしょうか?個人的にはRDSの認証情報の自動更新が魅力的でした。パラメータストアと状況に応じて使い分けたいところですね。


それでは明日以降のアドベントカレンダーも引き続きお楽しみください!

https://qiita.com/advent-calendar/2021/excite-hd

弊社採用情報はこちらからどうぞ!

https://www.wantedly.com/companies/excite/projects

DOM操作をするjQueryコードをJestでテストする方法を勉強した話

f:id:e125731:20211206173432p:plain

エキサイトホールディングス Advent Calendar 2021の10日目は、 エキサイト株式会社 エンジニアのあはれんがお送りします。

DOM操作をするjQueryコードをJestでテストするために、勉強としてJestのサンプルコードを動かしてみました。

その際に学んだ点、サンプルコードの実行方法を共有したいと思います。

Jestについて

Jestは、JavaScriptのコードの正しさを保証するために設計されたJavaScriptテスティングフレームワークです。 JavaScript単体で動作するのはもちろん、html要素を操作するものもテストすることができます。

jestjs.io

Jestの導入

Jestを導入するにはnpmまたはyarnが必要ですので、準備をお願いします。

npmの場合は以下のように実行してください。

npm install --save-dev jest

これだけで導入は完了です!簡単ですよね。

詳しい導入方法は、公式ドキュメントにありますのでそちらを参考ください。

jestjs.io

サンプルコードを動かしてみる

jestjs.io

上記のページで、JestでDOM操作する方法を説明しています。

また、サンプルコードを公開していますので、 サンプルコードをダウンロードして、実際に動作を確認することができます。

// 1. プロジェクトをダウンロードする
$ git clone git@github.com:facebook/jest.git
// 2. jQueryのサンプルコードがあるディレクトリに移動する
$ cd examples/jquery/
// 3. Jestを導入
$ npm install --save-dev jest
// 4. テストを実施
$ npm test

> example-jquery@0.0.0 test
> jest

 PASS  __tests__/display_user.test.js
 PASS  __tests__/fetch_current_user.test.js

Test Suites: 2 passed, 2 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        1.579 s
Ran all test suites.

では、サンプルコードを読んでみましょう。

f:id:e125731:20211204231902p:plain
(引用:https://jestjs.io/ja/docs/tutorial-jquery )

今回読むサンプルコードは、displayUser.jsをテスト対象としたコードです。

テストには、準備、実行、評価の3段階があります。

準備の段階では、 テスト対象のDOMとJavaScriptファイル(displayUser.js)を用意しています。 また、今回はテスト対象が別のJavaScriptファイル(fetchCurrentUser.js)を利用していたのでモックしています。 fetchCurrentUser.jsをモックすることでdisplayUser.jsの振る舞いのみをテストすることができます。

実行の段階では、jQueryを利用してユーザーが行うであろうクリック処理を実現させ、テストを実行しています。

テスト結果の評価では、モックでセットしたデータのJohnny Cash - Logged Inが表示されることを確認しています。

このようにして、DOM操作をするコードのテストを行うことができます。

サンプルコードから一歩進んでみる

サンプルコードの説明だけでは味気ないので、テストケースを1つ追加してみたいと思います。

fetchCurrentUser.jsから返ってくるデータのloggedInがfalseならば、Johnny Cash - Logged Outと表示されるか確認するテストケースを追加します。

f:id:e125731:20211204231950p:plain
ログアウト時の画面表示を確認するテストを追加

$ npm test

> example-jquery@0.0.0 test
> jest

 FAIL  __tests__/display_user.test.js
  ● displays a logout user after a click

    expect(received).toEqual(expected) // deep equality

    Expected: "Johnny Cash - Logged Out"
    Received: "  "

      62 |
      63 |   expect(fetchCurrentUser).toBeCalled();
    > 64 |   expect($('#username').text()).toEqual('Johnny Cash - Logged Out');
         |                                 ^
      65 | });
      66 |

      at Object.<anonymous> (__tests__/display_user.test.js:64:33)

 PASS  __tests__/fetch_current_user.test.js

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 3 passed, 4 total
Snapshots:   0 total
Time:        1.589 s
Ran all test suites.
zsh: exit 1     npm test

テスト実行結果は失敗してしまい、文字が表示されていないという結果になりました。

これは、1つ目のテストケースでfetchCurrentUser.jsjQuery等のモジュールがすでにrequireされているので、 2つ目のテストケースでrequireされないためです。

なので、毎回テストケースを実行する際に、requireするように、jest.resetModules()を呼び出す必要があります。

f:id:e125731:20211204231808p:plain
jest.restModulesを追記

上記を追記することでテストが成功するようになりました。

$ npm test

> example-jquery@0.0.0 test
> jest

 PASS  __tests__/display_user.test.js
 PASS  __tests__/fetch_current_user.test.js

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        1.452 s
Ran all test suites.

最後に

Jestは、ReactやVue.js等のJavaScriptフレームワークのテストを書くためのフレームワークのイメージだったので、 DOMを操作するjQueryコードのテストもこんなに簡単に書けるとは思っていませんでした。

今後、jQueryコードを書く際は、Jestを使ってテストを書いていきたいと思います。

エキサイトホールディングスのアドベントカレンダーはまだまだ続きます。

明日の執筆担当は@Suzuki-Shuheiさんです。 引き続きお楽しみください。

採用情報はこちら↓

https://www.wantedly.com/companies/excite

quarkusを使う(mybatis編)

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

前回の続きです。 mybatisを入れていきます。 mybatisにはserviceと同様@Singletonをつけて、@Injectで呼び出します。 その際、repositoryを間に挟みます。

まず、必要なextentionをgradleに追加します。

implementation 'io.quarkiverse.mybatis:quarkus-mybatis:0.0.10'
implementation 'io.quarkus:quarkus-jdbc-mysql:0.26.1'

続いてデータベースの設定です。

CREATE schema test;

CREATE TABLE book (
    id integer not null primary key,
    title varchar(80) not null,
    author varchar(80) not null,
    created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO `test`.`book` (`id`, `title`, `author`) VALUES (1, 'hobby', 's-nakao');

続いてアプリケーションの設定です、

  • application.properties
quarkus.datasource.db-kind=mysql
quarkus.datasource.username=YYYYYYY
quarkus.datasource.password=XXXXXXX
quarkus.datasource.jdbc.url=jdbc:mysql://localhost/test

※サンプルです。

  • repository interface
package org.my.hobby.repository;

import org.my.hobby.core.Book;

public interface BookRepository {
    Book find(String title);
}
  • repository impl
package org.my.hobby.repository;

import javax.inject.Inject;
import javax.inject.Singleton;

import java.util.Optional;

import org.my.hobby.core.Book;
import org.my.hobby.persistence.BookDto;
import org.my.hobby.persistence.BookMapper;

@Singleton
public class BookRepositoryImpl implements BookRepository {

    @Inject
    BookMapper bookMapper;

    @Override
    public Book find(String title) {
        final Optional<BookDto> book = bookMapper.getBook(title);
        return book
                .map(bookDto -> new Book(bookDto.getTitle(), bookDto.getAuthor()))
                .orElse(new Book("", ""));
    }
}
  • persistence interface

テキストブロック使います。 簡単なselectでも私はsql書きたい人です。 persistenceに全てのSQLをできるだけ書きたいです。

package org.my.hobby.persistence;

import java.util.Optional;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface BookMapper {
    @Select("""
        SELECT
            id,
            title,
            author,
            created
        FROM
            book
        WHERE
            title = #{title}
    """)
    Optional<BookDto> getBook(String title);
}
  • persistence dto
package org.my.hobby.persistence;

import java.time.LocalDateTime;

public class BookDto{
    private Integer id;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    public LocalDateTime getCreated() {
        return created;
    }

    public void setCreated(LocalDateTime created) {
        this.created = created;
    }

    private String title;
    private String author;
    private LocalDateTime created;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }
}
  • service impl

シンプルになります。

package org.my.hobby.service;

import javax.inject.Inject;
import javax.inject.Singleton;

import org.my.hobby.core.Book;
import org.my.hobby.repository.BookRepository;

@Singleton
public class BookServiceImpl implements BookService {

    @Inject
    BookRepository bookRepository;

    @Override
    public Book find(String title) {
        return bookRepository.find(title);
    }
}
  • コントローラー

前回のコントローラーは変わりません。

package org.my.hobby;

import javax.inject.Inject;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import javax.validation.constraints.NotBlank;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.Set;
import java.util.stream.Collectors;

import org.my.hobby.core.Book;
import org.my.hobby.service.BookService;

record BookSearchRequest(
        @NotBlank(message = "Title may not be blank") String title) {
}

record BookSearchResponse(
        boolean success,
        String message,
        Book book) {
    record Book(String title, String author) {
    }
}

@Path("/book/search")
public class BookController {

    @Inject
    Validator validator;

    @Inject
    BookService bookService;

    @POST
    @Produces(MediaType.APPLICATION_JSON)
    public BookSearchResponse search(BookSearchRequest bookSearchRequest) {
        Set<ConstraintViolation<BookSearchRequest>> violations = validator.validate(bookSearchRequest);
        if (!violations.isEmpty()) {
            final String errorMessage = violations
                    .stream()
                    .map(e -> e.getMessage())
                    .collect(Collectors.joining(","));
            final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book("", "");
            return new BookSearchResponse(false, errorMessage, hobbyBook);
        }

        final Book book = bookService.find(bookSearchRequest.title());
        final BookSearchResponse.Book hobbyBook = new BookSearchResponse.Book(book.title(), book.author());
        return new BookSearchResponse(book.isFind(), book.message(), hobbyBook);
    }
}
  • curlでアクセス
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "hobby"
}'

{"success":true,"message":"","book":{"title":"hobby","author":"s-nakao"}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
        "title": "s-nakao"
}'

{"success":false,"message":"not found book","book":{"title":"","author":""}}%
shogo.nakao@localhost:(App-Db-Blog-Fan) $ curl --location --request POST 'http://localhost:8080/book/search' \
--header 'Content-Type: application/json' \
--data-raw '{
}'

{"success":false,"message":"Title may not be blank","book":{"title":"","author":""}}%

取得できました!よかったですね!

一通り、controller,servie,repositoryができました。簡単でしたね。

quarkusでmybatisの設定は以下を参考にしてください。

Quarkus - Using MyBatis :: Quarkiverse Documentation

最後に、弊社では採用もバシバシ実施しているので興味のあるかたがいましたらご応募ください。

www.wantedly.com



Advent Calendar 2021を引き続き楽しんでいただけると嬉しいです。

qiita.com

Spring BootでCacheableが効かなかった

はじめに

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

エキサイトホールディングス Advent Calendar 2021の9日目の記事です。 qiita.com

タイトルのとおりですが、Spring BootでCacheableが効かなかったことがありました。 これについて調査したことと、原因について紹介します。

調査

下記に簡単なメソッドを用意しました。 getItem1メソッドはItemServiceにあるgetItemから呼ばれています。 このときに、下記サービスを呼び出してItemをキャッシュすることを試みます。

// インタフェース
public interface ItemService {
    Item getItem(String name);
}

// 実装
@Service
@RequiredArgsConstructor
public class ItemServiceImpl implements ItemService {
    @Override
    public Item getItem(String name) {
        return this.getItem1(name);
    }

    @Cacheable(cacheNames = "item", key = "#name")
    public Item getItem1(String name) {
        return new Item("1", "item1");
    }
}

結果

getItem1メソッドに@Cacheableを付与したときに、Itemがキャッシュされないことを確認しました。 getItem1メソッドから、getItemメソッドにアノテーションを移動するとキャッシュされます。

原因

これは、Cacheableそのものの問題ではないことがわかりました。 CacheableはSpring AOPを採用した実装であるため、ItemServiceImplの中で自身のメソッド呼びだしていることがキャッシュが効かないことの原因でした。 インタフェースを実装したメソッドに対して素直に@Cacheableを付与したほうがよさそうです。

おわりに

本記事では、Cacheableがうまく動かなかったときの原因について紹介しました。 実行時にエラーが出ずになかなか解決できなかったので、かなり詰まってしまいました。 最後まで読んでいただき、ありがとうございました!