AWS Lambdaのhandler内外でのexitの挙動について

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

AWSには、サーバーレスで処理を実行できる「AWS Lambda」というサービス(以降Lambda)があります。 Lambdaでは、実行の起点となるメソッド(handler)を指定することで、Lambdaの実行イベントが走ったときにそのhandlerメソッドが実行される、という流れになっています。

そのためコードの実装は基本的にはそのhandlerメソッドに書くことになるのですが、実は一部の状況ではあえてhandlerメソッドの外にコードを書くことがあります。

今回は、そういった状況で、handlerメソッドの内外で終了処理( exit )を実行したときの挙動の違いについて説明していきます。

Lambdaとは

LambdaはAWSのサービスの1つで、公式ページでは以下のように説明されています。

AWS Lambda は、サーバーレスでイベント駆動型のコンピューティングサービスであり、サーバーのプロビジョニングや管理をすることなく、事実上あらゆるタイプのアプリケーションやバックエンドサービスのコードを実行することができます。

Lambdaでは様々な言語でコードを書くことができるのですが、今回はPythonで試してみます。

言語をPythonで設定すると、以下のようなコードが自動生成されます。

import json

def lambda_handler(event, context):
    # TODO implement
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

handlerメソッドは lambda_handler になっており、これをテスト実行してみると、以下のようなLogが出力されます。

Function Logs
START RequestId: xxxx Version: $LATEST
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.28 ms   Billed Duration: 2 ms   Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 160.50 ms

handlerメソッドが lambda_handler であるため、このLambdaは実行のたびに lambda_handler が実行されることになります。

そのため、実装したいコードはその lambda_handler の中に書いていけば良いのですが、実は場合によっては lambda_handler の外に処理を書いたほうがいい場合もあります。

handlerメソッドの外に処理を書いたほうがいい場合

DBとの接続のコードのサンプルでは、以下のように書かれています。

ハンドラの外部で pymysql.connect() を実行すると、関数がデータベース接続を再利用できるようになるため、パフォーマンスが向上します。

実はhandlerメソッドの外で定義したコードは、handlerメソッド内に定義したコードと異なり、Lambda実行の度に毎回実行されることはありません。

代わりに、Lambdaのコンテナ(Lambdaの実態はコンテナです)が立ち上がった時の最初の一回しか実行されず、以降はそのコンテナが終了するまで結果を保持し続けるため、例えばDBとの接続のような一回実行すれば問題ないような処理を書くにはもってこいの場所になっています。

実際にサンプルコードで試してみます。

import json

print('external printing')

def lambda_handler(event, context):
    print('internal printing')

    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

このコードを実行してみると、初回実行時は以下のようなログが出力されます。

START RequestId: xxxx Version: $LATEST
external printing
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.67 ms   Billed Duration: 2 ms   Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 139.88 ms

二回目以降は以下のようになります。

Function Logs
START RequestId: xxxx Version: $LATEST
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1.21 ms  Billed Duration: 2 ms  Memory Size: 128 MB    Max Memory Used: 39 MB

初回にあった、handlerメソッドの外で定義している external printing の出力が、二回目以降は消えているのがわかります。

handlerメソッド内外でのexitの挙動

さて、突然ですが、コードを書く上で exit をしたい状況はたまに存在します。

例えば、本来なってほしくない結果が得られてしまったときなど、それ以降の処理を続行せずにそこで終わらせたいときなどです。

Lambdaでももちろん exit をすることは可能なのですが、上記の通りLambdaでのコードは、handlerメソッド内外で異なる挙動をします。 では、 exit実行時にはどのような違いがあるのでしょうか?

handlerメソッド内部での exit 実行

handlerメソッド内部で、以下のコードで exit を実行してみます。

import json
import sys

print('external printing')

def lambda_handler(event, context):
    print('internal printing')
    sys.exit(0)
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

初回・二回目以降のログは以下のようになります。

初回

START RequestId: xxxx Version: $LATEST
external printing
internal printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 144.18 ms Billed Duration: 145 ms Memory Size: 128 MB Max Memory Used: 39 MB  Init Duration: 215.69 ms    
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

二回目以降

START RequestId: xxxx Version: $LATEST
internal printing
END RequestId: xxxx
REPORT RequestId:xxxx   Duration: 104.91 ms Billed Duration: 105 ms Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

exit しない時と同様、二回目以降はhandlerメソッド外部の処理は呼ばれていないことがわかります。

handlerメソッド外部での exit 実行

handlerメソッド外部で、以下のコードで exit を実行してみます。

import json
import sys

print('external printing')
sys.exit(0)

def lambda_handler(event, context):
    print('internal printing')
    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

初回・二回目以降のログは以下のようになります。

初回

START RequestId: xxxx Version: $LATEST
external printing
external printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1369.88 ms    Billed Duration: 1370 ms    Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: 19a1b5ac-519a-4d6e-bbaa-afee39439c57 Error: Runtime exited without providing a reason
Runtime.ExitError

二回目以降

START RequestId: xxxx Version: $LATEST
external printing
END RequestId: xxxx
REPORT RequestId: xxxx  Duration: 1328.81 ms    Billed Duration: 1329 ms    Memory Size: 128 MB Max Memory Used: 11 MB  
RequestId: xxxx Error: Runtime exited without providing a reason
Runtime.ExitError

初回に external printing が二回出てしまっているのは謎ですが、少なくとも二回目以降でも external printing が出ている、すなわちhandlerメソッド外部の処理が実行されているのがわかります。

上記の結果から、 exit をhandlerメソッド内部で実行するときと異なり、外部で実行する時は全体的に終了されていることがわかります。

DBとの接続設定など、handler外部での処理ごと終了させたい場合はhandler外部で exit を実行し、そうではなく毎回実行したい処理だけ終了したい場合はhandler内部で exit を実行すると良いでしょう。

最後に

Lambdaは便利ですが、上記のように細かい設定も存在します。 活用していけばよりパフォーマンスを改善したりすることもできるので、ぜひ気にかけていきましょう。

SpringBootのInterceptorでテンプレートをモバイルとデスクトップでわける

エキサイト株式会社のエンジニアの佐々木です。SpringBootとThymeleafを使ってリビルドしていますが、既存テンプレートがモバイル用とデスクトップ用でわかれているので、Interceptorを使って対応します。

前提

SpringBoot x Gradleを使います。

build.gradleの依存関係は下記のようになっています。

dependencies {

        implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
        implementation 'org.springframework.boot:spring-boot-starter-web'

        implementation 'org.springframework.boot:spring-boot-starter-validation'
        implementation 'org.springframework.session:spring-session-core'
        compileOnly 'org.projectlombok:lombok'
        developmentOnly 'org.springframework.boot:spring-boot-devtools'
        annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        testImplementation 'io.projectreactor:reactor-test'

}

インターセプターの実装

今回はUser-Agentを使用して、モバイルかデスクトップかをわけていこうと思います。User-Agent内にモバイルと識別できる文字列があったらモバイルのディレクトリにします。

@Slf4j
@Configuration
public class WebTemplateInterceptor implements HandlerInterceptor {

    private static final Pattern MOBILE_USER_AGENT = Pattern.compile("(iphone|android)");
    private static final String MOBILE_TEMPLATE_DIRECTORY = "mobile";
    private static final String DESKTOP_TEMPLATE_DIRECTORY = "desktop";

    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

        if (modelAndView == null || response.getStatus() != 200) {
            HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
            return;
        }

        String header = request.getHeader("User-Agent").toLowerCase();   // ヘッダを取得
        Matcher matcher = MOBILE_USER_AGENT.matcher(header); // User-Agentで判定

        String templateFile = String.join("/"
                , matcher.find() ? MOBILE_TEMPLATE_DIRECTORY : DESKTOP_TEMPLATE_DIRECTORY
                , modelAndView.getViewName()); // mobile と desktop をprefixとして設定する

        modelAndView.setViewName(templateFile);
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
}

HandlerInterceptorインタフェースimplementsし、 必要なメソッドをoverrideして処理を追加します。インターセプターは3種類、preHandle(コントローラ処理開始前)postHandle(コントローラ処理終了後)afterCompletion(レスポンス返却後)になります。 今回は、templateのパスを変更するので、コントローラの処理が終わったあとのpostHandlerメソッドをoverrideしました。また、正常なHTTPステータスの時のみパスを変更するようにしています。エラーページについては、モバイルもデスクトップも同じテンプレートになっているのでこのように対応しています。

インターセプターを登録する

インターセプターを実装したら、DIコンテナに登録が必要になります。

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

    private final WebTemplateInterceptor webInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(webInterceptor);
    }
}

WebMvcConfigurerインターフェースimplementsして、addInterceptorsをoverrideして、先程作成したInterceptorをDIして、登録します。これで設定完了になります。

テスト

テストしてみます。User-Agentに指定した文字列を投入すると下記のようにモバイルとデスクトップが振り分けられます。

SPの場合
$ curl http://localhost:8080/ -H 'User-Agent: iphone'
----
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<h1>Mobile</h1>
<div><h2>common</h2></div>
</body>
</html>
----

PCの場合
$ curl http://localhost:8080/ -H 'User-Agent: sample'
----
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Sample</title>
</head>
<body>
  <h1>Desktop</h1>
  <div><h2>common</h2></div>
</body>
</html>
----

まとめ

レスポンシブが実装されていれば必要ないのですが、今回はこのような形で回避することにしています。この処理をすると、IntelliJでのThymeleafのコードジャンプが使えなくなるので、不便ですが仕方ありません。次のときはレスポンシブで作りたいところです。

最後に

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

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

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

GitHubとIntelliJでmermaid記法が使えるようになりましたね!

いつものtaanatsuです。

GitHubのissueやPullRequestなどでmermaid記法が使えるようになっていますね!

f:id:taanatsu:20220304144332p:plainf:id:taanatsu:20220304144412p:plain

使い方は簡単で、コードブロックの名前をmermaidにするだけです

```mermaid
  graph TD;
      A-->B;
      A-->C;
      B-->D;
      C-->D;
```

これ、IntelliJでも使えるようです!

上記の様にプラグインの追加からもいけますが、
実はmarkdownに直書きするとレコメンドが出てきます。

f:id:taanatsu:20220304145542p:plain

レコメンドをクリックすると勝手にプラグインがインストールされるので、IntelliJを再起動すると…

f:id:taanatsu:20220304145611p:plain

あら素敵!
マークダウンファイルのプレビューにmermaid記法のものがレンダリングされました!

ということで便利機能のご紹介でした。
ではまた次回〜

【Canvaで簡単】SNS発信用テンプレを作ってみた話

こんにちは。21卒デザイナーの山﨑です。 今回は「CanvaでSNS発信用テンプレを作ってみた話」について書こうと思います。

制作したのはこちらです。著名人のインタビュー記事の宣伝なので、写真メインでスタイリッシュになるよう意識しました。

挙動は大体こんな感じになります。(容量の問題で一部しか載せられませんでした…)

さてこのアニメーションですが、実はCanvaで簡単に作ることができます。

制作方法

Canvaの左上の「パン(※通常はアニメートという名称)」をクリックすると、サイドから色々なアニメーションの挙動が出てきます。

その中から好きなアニメーションを選べば、勝手にアニメーションを作成してくれます👏

CanvaはストーリーやInstagramの記事デザインも簡単に作れるので、これからもSNSの発信クリエイティブはCanvaで制作したいと思います😃

終わりに

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

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

それではまた!

www.wantedly.com

Excite × iXIT TechConで「Spring Bootという強すぎるフレームワークについて」を発表しました!

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

先日、弊社内で Excite × iXIT TechCon というカンファレンスが行われました!

tech.excite.co.jp

私は「Spring Bootという強すぎるフレームワークについて」というタイトルで発表させていただきました。

Spring Bootとは何なのか、そしてその強み・弱みは何かを説明させていただいているので、ぜひ見てみていただけると幸いです。

社内限定のカンファレンスでしたが、良い経験となりました。

今後もこういった催しがあると、更なる技術組織の活性化やレベルアップに繋がっていきそうです!

【失敗談】MyBatisでNOLOCKと同じことをやりたかった

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

はじめに

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

現在は、既存サービスのリビルド(PHP / BEAR.Saturday → Java / SpringBoot)を担当しています。 SpringBootでDBアクセスをするにあたってMyBatisを利用していますが、MyBatisのMapperではNOLOCKヒントに対応していないため、 簡単なSQL文であったとしてもその都度NOLOCKヒントを付与したクエリーを書く必要があります。 そこで、MyBatisを使用しつつREAD UNCOMMITTEDでSELECT文を実行する方法について考えてみました。

本記事では、上記方法と、それを導入できなかった理由について紹介します。

トランザクション分離レベルを確認する

SQL Serverでは下記を実行することでトランザクション分離レベルを確認することができます。

DBCC USEROPTIONS

現状ではREAD COMMITTEDが設定されていることが確認できました。 この状態でSELECT文を実行すると、行ロックが発生してしまう可能性があります。 そのため、行ロックを回避するためにNOLOCKヒントが付与されたクエリー(= READ UNCOMMITTEDで実行されるクエリー)が多くアプリケーションに存在します。

SQL ServerでNOLOCKヒントを付与する

SQL ServerではNOLOCKヒントを付与することで、READ UNCOMMITTEDでSELECT文を実行することができます。 クエリーは下記のように記述します。

SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = 100;

しかし、MyBatisで自動生成されるMapperにはNOLOCKヒントが付与されません。 そのため、READ UNCOMMITTEEDでSELECT文を実行したいときはMapperアノテーションを使用してクエリーを記述しなくてはなりません。

@Mapper
public interface MyArticleMapper {
  @Select("SELECT * FROM articles  WITH(NOLOCK) WHERE article_id = #{articleId}")
  Optional<Article> getArticle(Long articleId);
}

クエリー実行毎にトランザクション分離レベルを設定する

SQLクエリーで表すと下記のようになります。

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN TRANSACTION;

SELECT * FROM articles  WHERE article_id = 100;

COMMIT TRANSACTION;

Javaでは下記のように記述することができます。

@Override
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
public Article findArticle(Long articleId) {
    return articleMapper.selectByPrimaryKey(articleId);
}

実際に上記2つを実行してみると、どちらもREAD UNCOMMITTEDでSELECT文を実行することができました。 そのため、リポジトリでクエリーを実行する箇所に@Transactionalアノテーションを付与するとよさそうです。

実行結果

1つのエンドポイントで2つのクエリーを実行している箇所の実行時間を比較した結果、下記の結果を得ることができました。

Webページでは1ページで複数のAPI呼び出しを行っているため、実際にページが表示されるのが何倍にも遅くなってしまうようになりました😨 これだけの性能差があると、クエリー実行毎にトランザクション分離レベルを設定する方法は実運用に耐えられないことがわかりました。

おわりに

いつもはうまく解決できたことを記事にしていますが、今回はうまくいかなかったことについて紹介してみました。 クエリーを実行するときにトランザクション分離レベルを設定するのはコストがかかることなんだと実感できました。 SQL ServerでREAD UNCOMMITTEDでSELECT文を実行するときは、素直にNOLOCKヒントを付与するのがよさそうです。 最後まで読んでいただき、ありがとうございました!

過去最大規模の社内カンファレンス「Excite × iXIT TechCon」を開催しました!

f:id:KAJIJI_Design:20220225103706p:plain

はじめに

エキサイトでエンジニアをしている おおしげ( @_ohshige ) です。
このたび、エキサイトHD(エキサイトとiXIT)の技術者向けの社内カンファレンス「Excite × iXIT TechCon」を開催し、大成功と言ってもいい内容となったのでご報告いたします。

これまでは勉強会やLT会といった小規模なものは開催されてきましたが、それを超える規模のものとしては初めての開催になりました。
これまでの勉強会等についてはこちらを御覧ください。

TechConの概要

開催の背景とか目的とか準備期間についてとか色々とお話する前に、まずはどのような会だったのかを軽く説明しておこうと思います。

参加者

エキサイトHDの技術職全員 (技術職以外の社員は任意参加)

開催時間

10:30-18:00

大まかな目的
  • エキサイトHD技術職の交流・横のつながり強化・技術成長
  • 外部カンファレンスに向けた練習の場
発表内容
タイトル
セッション 1 オブジェクト指向を知らないメンバーがいる中で、
クリーンアーキテクチャを目指した話
セッション 2 Docker環境がチームに浸透するまで
セッション 3 Spring Bootという強すぎるフレームワークについて
パネルディスカッション DBの設計とか運用ってどうやってる?
セッション 4 MobileAppGuildって知ってる?知らないの?!
セッション 5 宣言的UI時代のソフトウェア設計について考える
セッション 6 (非公開)
セッション 7 slackアラート攻略ガイド
セッション 8 個人的 AWS アップデートランキング
セッション 9 AWSへの移行 1年間の軌跡
LT 1 差分検出を利用したDBマイグレーション
LT 2 エキサイトブログをリビルドする
LT 3 接客態度とサービスイメージの良さの大切さ
LT 4 複数案件(環境)を抱える人向けの便利なDocker小技
LT 5 dotfilesのススメ
LT 6 (非公開)
LT 7 (非公開)
LT 8 さっくりみる、デザインの仕事
LT 9 終わらないSEOとの戦いと向き合うために
LT 10 初めてのバーチャル美肉
LT 11 FirebaseにLINE認証でアカウント情報を作成してみた

開催時間が7時間半で発表が全部で21もあり、技術職は業務として基本的に参加するということからも、規模と全社の巻き込み具合がわかるかなと思います。

開催の背景と目的

エキサイトHDでは技術組織活性のために動いているチームがあり、そこで様々な活動を行ってきました。上述した勉強会もそうですし、これまでこのブログでもいくつか紹介してきました。
今回のTechCon開催もその技術組織活性の一環であり、技術組織活性チームで作り上げたものです。

ブレストレベルの「エキサイトカンファレンスって面白そうだよね」というただただ好奇心と思いつきだけで最初の案が持ち上がりました。
定例としてゆっくり動き出したのが8月なので、約7ヶ月もの期間を使って準備を進めていたことになります。
もちろん最初のうちはブレストベースだったりでそこまで詰め詰めの会議であったわけではないですが、振り返ってみると結構長い時間をかけて準備をしていたんだなといまさら実感しています。

技術組織活性

f:id:KAJIJI_Design:20220227133337p:plain 技術組織活性の一環として始まったものなので、根幹の目的は「技術組織の活性」です。
もう少し具体的に言うと、横のつながりを強化するために他部署のエンジニアが何をしているかを知って、インプット・アウトプットすることで技術的な成長を促すということです。

横のつながりについては、エキサイト内の他部署同士という意味もありますが、エキサイトとiXITの交流という意味合いも強くあります。
2020年8月にエキサイトがiXITの株式を取得してグループ会社化し、今現在同じオフィスで働いている(コロナ禍でリモートワーク中心ではありますが)のですが、エキサイトとiXITの交流が盛んに行われているともなかなか言えない状況でした。 f:id:KAJIJI_Design:20220227133422p:plain

外部カンファレンスへの登壇の練習

そして、もう1つの目的は「外部カンファレンスへの登壇の練習」です。

弊社はこれまで様々なカンファレンスに協賛させていただいてきました。
協賛させていただくことでカンファレンスの成功に貢献できることは光栄なことではありますが、やはりエキサイトから登壇者をもっと出したい(せめてプロポーザルを出したい)という思いがあります。
とは言え、人前で発表するのはとても緊張しますし、準備も大変です。こんな発表でいいんだろうかと考えてしまったり、ネタが無いと思ってしまったり、そうして登壇まで至らないという現状です。

そこで、外部カンファレンスへの登壇の足がかりとして練習の場として、思いっきり社内を巻き込んでしまえというのがもう1つの目的です。
完全に身内だけなので外部カンファレンスほど緊張はしないですし、内容もいつもの小規模なLT会ではなく大規模なカンファレンスっぽいものとすることで外部カンファレンス登壇のハードルを越えるお手伝いを目指しました。

開催にあたって工夫した点

開催するにあたって工夫した点はたくさんありますが、基本的に上述した目的を達成できるように様々なことをやりました。

大きくわけると以下の2点に注意して、様々な工夫を凝らしました。

  • 多様性
  • 本番さながらであり練習台

多様性

技術組織活性につながるように、横のつながりをより強固なものとするために、これまでのLT会とは一線を画するように、多様性はかなり重視しました。
これまでのLT会などはどうしても決まったメンバーが発表しているような感じでしたが、この多様性をかなり意識することで、最終的に今回のカンファレンスでは様々なメンバーにスポットを浴びせることができました。

まず、登壇者の部署・職種・職歴・性別はバラバラになるようにかなり注意深く配慮しました。
最終的には、新卒1年目から技術マネージャーまで幅広く、バックエンドエンジニア・アプリエンジニア・インフラエンジニアだけでなくデザイナーや企画職まで多くの職種があり、もちろん各部署から最低1名は登壇しました。

さらに、登壇以外の方法で技術メンバーにスポットを当てるために、各発表をいくつかのブロックに分けて、それぞれのブロックに司会者を立てました。
この司会者は運営スタッフから選ぶのではなく、そのブロックの内容について把握できるメンバーでありながらも部署・職種・職歴・性別に多様性が出るようにしました。

さらには、発表内容が多様になるためには様々なプロポーザルが提出されないといけない(後述しますが、発表内容はプロポーザルを提出してもらいその中から選出する方式でした)ので、プロポーザルの提出具合をGoogleフォームとスプレッドシートとGASとSlackを使って匿名にした上で見える化し、プロポーザル提出の相乗効果を狙いました。 f:id:KAJIJI_Design:20220227133512p:plain

本番さながらであり練習台

最終的には外部カンファレンスで登壇してもらうために今回のカンファレンスはその練習台となれるように工夫しました。

そのための一番の工夫としては、空気感づくりです。
可能な限り外部カンファレンスと同じような体裁となるようにすることで、より本番に近い環境での練習になるようにしました。
そのために、発表内容はスタッフで指名して作り上げていくのではなく、プロポーザルを提出してもらいスタッフはその中から選出してタイムテーブルを作っていきました。
また、本物に近い雰囲気の醸成のために、カンファレンス専用のサイトやノベルティなど様々なものをつくりました。
f:id:KAJIJI_Design:20220225103753p:plain デザイナーさんには運営スタッフとしてがっつり入っていただき、専用サイト・ロゴ・スライドマスター・Zoom背景・Tシャツ・ステッカーなどをつくり、より本物のカンファレンスかのような仕上がりとなりました。
このような工夫をすることで、緊張感も出て良い空気感となりました。

また、本番さながらな空気感のなか何かに失敗してしまうと良くないので、失敗を極力下げるために手厚いサポートをして、さらに発表者の労力を可能な限り減らすようにしました。
上述の通り、スライドマスターやZoom背景をあらかじめ用意しておくことで、発表者には資料の内容に集中してもらうようにしました。
また、当日はウェビナーを使った開催だったのですが、画面共有やマイク・カメラに慣れ当日焦って失敗してしまわないようにするために、登壇者全員に対して事前に接続テストを実施しました。 司会者もスタッフではないメンバーなので、台本は綿密に練って事前に渡しておき、打ち合わせをすることで、進行に詰まってしまわないように注意を払いました。

さらに、せっかく忙しい合間を縫って発表の準備を進めてもらっているので、それに感謝しながらもさらに外部カンファレンスでも生かせるように、参加者にはしつこいほどに発表者へのフィードバックを促しました。
フィードバックフォームを用意し、あらゆる発表終わりにフィードバックを書いてもらうよう促し、カンファレンス終了後にそれらフィードバックをすべてお渡しするということを行いました。

開催当日

様々な準備を行ってカンファレンス当日を迎え、多少トラブルもありましたが無事に開催することができました!

カンファレンス開催の様子
カンファレンス開催の様子

エキサイトHD全体で技術職は80名ほどいるのですが、参加者は95名にも及びました。
全社に対して告知していたので企画職や営業職の方などもたくさん来ていただけたようで、技術職だけでない横のつながりの強化になったのではないかなと思います。
ウェビナーのコメント欄も常時動いており、大変盛り上がったのですが、すっかりその様子を撮影するのをわすれていたのが悔やまれます。
また、接続テストや台本を綿密に準備したため発表者も司会者にスムーズにすすめることができ、参加者からのフィードバックも発表者はとても喜んでくれました。

実は、一番良かったセッションを投票で決める「ベストスピーカー賞」、一番良かったLTを投票で決める「ベストLT賞」、たくさんフィードバックをしてくれた参加者に対する「ベストフィードバック賞」を用意しており、表彰のときはとても盛り上がりました。
ベストスピーカー賞はオンプレからAWSへの移行の軌跡を大変面白く発表してくれた『AWSへの移行 1年間の軌跡』、ベストLT賞は徹夜でバ美肉を実現し披露してくれた『初めてのバーチャル美肉』、そしてベストフィードバック賞は内定者として参加していた学生が受賞しました!
ベストフィードバック賞を内定者が受賞するとは思ってなかったのですが、本当にたくさんのフィードバックをしてくれてとても真剣に聞いてくれたということが伝わってきました。
多様性を示す良い例にもなったかなと思います。

『AWSへの移行 1年間の軌跡』の発表の様子
AWSへの移行 1年間の軌跡

『初めてのバーチャル美肉』の発表の様子
初めてのバーチャル美肉

カンファレンスの最後にアンケートをとったのですが、95.6%の参加者が満足したと回答してくれました。
また、以下のような大変ありがたいコメントもたくさんいただきました!

  • 他の部署のやっていること、知らない技術分野について知ることができてよかったです。
  • 外部登壇への挑戦のきっかけになりました!
  • 運営がとてもしっかりしていたので、発表者側で参加していて不安なく発表に集中できました。ありがとうございます!
  • 色んなジャンルの話を聞けてよかった。全員の発表のクオリティがとても高かった!運営もとてもスムーズで素晴らしかったです。
  • 次回はぜひ外部に向けた発表もしてほしいです
  • 自分がプロポーザルを出さなかったことを後悔しました

開催してみて、展望

初めての試みでしたが、総じて大成功でした!
これもすべて、質の高い発表をしてくれた発表者・パネリスト、うまく回してくれた司会者、コメントで盛り上げフィードバックもたくさんしてくれた参加者、半年かけて準備をしてくれたスタッフ、全員がしっかりと噛み合ってできたおかげかなと思います。

「多様性」で様々な種類の発表になり、「本番さながらであり練習台」を目指したために質が担保された発表になったのだと思いますし、社内限定だったからこそハードルが高くなりすぎずに日頃の成果を出せたのではないかと思います。

せっかく第1回が成功したのでぜひ続けていきたいところですが、次回はまだ未定です。
この規模で続けるのであればおそらく頻度としては年に1回が限界だと思うので、半年後の動きに期待です。
今回はコロナ禍ということもありオンラインでの開催でしたがオフラインでの開催も目指したいですし、他社様でたくさん開催されているように社内限定ではなく社外に向けた開催も考えていきます。

そして、これを機に、様々なカンファレンスに私を含めて弊社員が挑戦してくれるのではないかと思っていますし願っています。

Spring Bootで、文字列型の空クエリパラメータを受け取るときの注意点

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

Spring BootのAPI等で空のクエリパラメータを受け取るとき、受け取り側の型や「空」の指定方法によって取得される値が違うことはご存知でしょうか?

今回は、その違いについて紹介していきます。

Spring Bootとクエリパラメータ

通常URLでクエリパラメータをつける時は、以下のようにします。

https://sample/?query1=test

これをSpring Bootで受け取る際は、

import lombok.Value;

@Value
public class SampleRequestModel {
    String query1;
}
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;

@GetMapping("sample")
public SampleModel getSample(
        @ModelAttribute @Validated SampleRequestModel sampleRequestModel
) {
    // 処理
}

こうすれば、 sampleRequestModelquery1 に、 test という文字列が入ります。

では、

https://sample/?query1=

このような時はどうでしょうか?

Spring Bootと空のクエリパラメータ

実はこれは、 query1 をどのような型で受け取るかによって結果が異なってきます。

import lombok.Value;

@Value
public class SampleRequestModel {
    String query1;
}

のように String で受け取る場合は 空文字 になりますが、 BooleanInteger 等の場合は Null となるので、注意が必要です。

更に、

https://sample/

のように、そもそもURLにクエリパラメータが存在しない場合は、 String も含めて Null になるのでご注意ください。

最後に

よくよく考えてみれば妥当な挙動ではありますが、慣れないうちや急いでいたりするとすべてNullで来ると勘違いしてしまうこともあると思います。

そうすると、予期せぬ空文字が入ってきてしまいエラーになってしまう可能性もあるので、注意していきましょう。

build.gradle内でIntelliJのGroovyの補完を効かせる

f:id:earu:20220223173437p:plain エキサイト株式会社メディア開発の佐々木です。build.gradle内でIntelliJのGroovyの補完を効かせる方法です。

Gradleとは

Groovy製のビルドツールになります。Javaを始めAndroidC/C++でも使えるようです。モジュールの依存関係の解決やちょっとした処理をGroovyスクリプトで記述・実行できます。

build.gradle

下記の記述をpluginsの前に書き加えるのみになります。

buildscript{
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'org.codehaus.groovy:groovy-all:3.0.9'
    }
}

これで、IntelliJで補完が効くようにになります。

動作確認

実際のbuild.gradleのコードは下記になります。

buildscript{
    repositories {
        mavenCentral()
    }

    dependencies {
        classpath 'org.codehaus.groovy:groovy-json:3.0.9'
    }
}

plugins {
    id 'java'
}

group 'org.example'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
    useJUnitPlatform()
}


task sample() {
    doLast {
        sample();
    }
}

import groovy.json.JsonSlurper

def sample(){
    def json = new JsonSlurper().parseText('{"a":"b"}')
    println(json);
}

groovyを使用しているコードは下記になります。このコードで先程クラスパスを追加した部分を使っています。

task sample() {
    doLast {
        sample();
    }
}

import groovy.json.JsonSlurper

def sample(){
    def json = new JsonSlurper().parseText('{"a":"b"}')
    println(json);
}

実行結果は下記になります。

$ ./gradlew sample

> Task :sample
{a=b}

Javaのライブラリ等も使用可能なので、Shellだとちょっと面倒になってしまうみたいなときに、重宝します。

まとめ

Gradleは、Groovyでできているのでこの設定をしなくてもGroovyのコードは実行できるのですが、IDEの恩恵を受けられないので設定しています。Javaのライブラリ等も使用可能ですし、プラグインもサクッと作ることができます。どこかでプラグインの話も書こうかと思います。

最後に

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

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

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

議事録をGoogleドキュメントにしたい人

f:id:KAJIJI_Design:20220224125103p:plain こんばんは!まだ新卒のデザイナー、鍜治本です。
近々、社内Wikiことコンフルにまとめたいことがあるついでに、あることを思い出したので備忘録としてブログネタにします⚰️

Confuluenceに議事録を書くと激重になる

弊社デザイナーは週1で定例会をしており、その週の業務や共有したいことなどを各自がページに記入し、Confluenceに議事録を積み上げていました。
使い始めてしばらくして気付いたのですが、ConfluenceのWiki内で検索をするとその単語が入ったページを全部持ってきてしまい、「マニュアルを調べたい」「困った時のQ&Aを知りたい」人の妨げになっていました…悲しい🥺

f:id:KAJIJI_Design:20220221231145p:plain

画像は試しに『KUROTEN.』で検索したもの。仕様などまとめているKUORTEN.のスペースがあるのですが、過去に書いた議事録の方が先に出てきてしまっています。
ソートなどで回避可能かもしれませんが、いつ?誰が?どこに?と社内あるある発掘系ページを探すときには苦痛でしかなさそうです。

そして、こういったWikiには要らない情報が増えることで、Confluenceがどんどん重たくなってしまう悲しみ製造機になってしまうのです…😭

積み重ねて増える議事録をGoogleドキュメントでやりくりする

そこで移行先として選んだのがGoogleドキュメント。
共同編集はもちろん、使い勝手はWordと同じなので、箇条書きやインデントなども自由に調節できます…!

f:id:KAJIJI_Design:20220221234050p:plain Confluenceでこんな感じにまとめていた議事録を…

えいや〜〜〜!!!!🧚‍♂️

f:id:KAJIJI_Design:20220221234143p:plain
ほとんど一緒!

いくつかこだわりポイントも紹介しますね🥰

ヘッダーに図形とテキスト

各ページがのっぺりして見えたので、各ページのヘッダーにグレーの図形とタイトルを入れてみました。
本当はここに日付も入れたかったのですが、セクション切り替えの都合で毎議事録の頭に入れることにしました。

f:id:KAJIJI_Design:20220221235010p:plain

基本的に前回の議事録をコピペ→日付の入れ直し→セクション区切りを追加、だけで議事録のフォーマットができるので便利になりました🙆‍♀️

インデント設定を細かく決める

テキストの始まり位置を指定できるインデントは、わかりにくいのですがルーラー部分に表示されています。
ドキュメントの余白部分や、箇条書きした際のTabインデントがやたら長い場合は、ここで設定を変更できます。

f:id:KAJIJI_Design:20220222000754p:plain

議事録では各自が記入するエリアを表で作っており、狭いスペース内でデカいインデントはカッコ悪いのでピチピチに詰めて調整しました✍️
箇条書きで書き込んでインデントを追加しても大丈夫なように、2〜3段階まで設定したので箇条書きし放題です。

終わりに

ツールによって使い道を見分けるのはなかなか難しいですよね🥺
予めテンプレート化して書き込むだけにくらいにできた方がもっと便利になると思うので、これを機に管理しづらかった議事録をGoogleドキュメントに移行してみてください〜!

サービスを停止せずにテーブルをリプレイスする方法

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

長い間サービスを運用していると、やがてテーブルの構造を変えたいと思うときが来ることがあります。

「Indexを追加する」程度であればそこまで問題ではありませんが、「特定のカラムの照合順序を変更する」などをする場合、そのテーブルへの書き込みがブロックされてしまいます。 一時的にサービスを停止できるのであればそれが最も楽ですが、多くの場合、可能な限りサービスは停止せずに変更したいのではないでしょうか。

今回は、実際に私が携わっているサービスで行った、サービス無停止でのテーブル変更の方法を紹介します。

テーブル変更の経緯

私が携わっているサービスはかなり息の長いサービスであり、その分テーブルも長い間使われて続けています。

その結果、過去に使っていたが現在では使っていないカラムがあったり、不適切な照合順序がついているカラムがあったり、本来必要な制約がついていなかったりと、様々な問題が山積していました。

そこで今回、そういった問題を解消するためにテーブル構造を変更することになったのですが、ここでいくつか問題が浮上しました。

カラムの照合順序の変更中は、テーブルへの書き込みができない

カラムの照合順序を変更すると、その変更中はテーブルへの書き込みがブロックされ、待機状態になります。 変更がほぼ一瞬で済むレベルであれば良いかもしれませんが、今回のテーブルはレコード数が多く、変更に数分掛かってしまうため、その間書き込み不可状態にしておくのは現実的ではありませんでした。

カラム削除後にエラーが起きる可能性がある

変更したいテーブルには「過去に使っていたが現在では使っていないカラム」があり、それを削除することも今回の目的でした。 ですが、厳密には使っていないわけではなく、「使うべきではなく、別テーブルとのJOINで取得するべきカラムであるため削除しても問題ないが、一部では使ってしまっているカラム」であり、そのためそれを使用しないようにするためのアプリケーションコード側の修正が漏れてしまった場合、カラム削除時にエラーが起きてしまう可能性がありました。

一時的にサービスを停止してテーブルを変更し、そのテーブルを使用している箇所が問題ないかテストを行い、問題なければサービス再開、ができるのであればよかったのかもしれませんが、サービスを停止する場合は当然以下のようなことを考えなければなりません。

  1. ユーザ側にサービスを停止する旨を事前に通達する必要がある
  2. サービス停止中はユーザがアクセスできないため、ユーザの利便性としても、サービス側の収益としても問題となる
  3. 万が一変更中に何かが起きた場合、停止時間が長引き、上記の問題が拡大する

そのため、可能な限りサービス無停止でテーブルの変更を行う必要がありました。

テーブル変更の方法

色々と考えた結果、 blue / green の手法を取ることとしました。

すなわち、

  1. 該当テーブルに対する書き込み処理のみを停止する
    • 今回のサービスは、ユーザが行うのは読み込みのみで、書き込みは管理側のみで行うため、書き込み停止によるユーザ側への影響はほとんど存在しない
  2. 該当テーブルのコピーテーブルを作成する
  3. コピーテーブルに対して、構造の変更等を実行する
  4. コピー元テーブルとコピー先テーブルの名前を入れ替える
  5. 問題なければ書き込み処理を再開する
  6. 全体的に問題なければ、コピー元テーブルを削除する

という方法です。

これであれば、先に挙げた「カラムの照合順序の変更中は、テーブルへの書き込みができない」の問題は気にする必要がありませんし、万が一テーブル名を入れ替えた後に問題が発生しても名前を戻せばいいだけなので、「カラム削除後にエラーが起きる可能性がある」の問題もリスクを最小限に抑えることができます。

ちなみに、今回は書き込みを管理側のみで行うタイプのサービスであり、かつ書き込み停止がそこまで難しくなかったのでこの方法にしていますが、例えばユーザ側でも書き込みがあったり、書き込み停止がかなり難しい場合は、一時的にコピー元・先の両方のテーブルにダブルライトする方法をとっても良いでしょう。

最後に

テーブル変更は、テーブルへのアクセスに制限が掛かったり、不可逆性があったりして、アプリケーションコードの変更ほど簡単には行うことができません。

可能な限り、最初に作る段階でちゃんとしたものを作るべきですが、サービスやエンジニアリングの変化で、どうしても変えなければいけない日が来ることもあります。

今回の blue / green の手法は、常に使えるわけではないとは思いますが、テーブル変更の際の問題点をある程度排除してくれる物となっていますので、もしテーブル変更が必要な際はぜひ考えてみてもらえると幸いです。

Sliverを使用したUI実装におけるHeaderの実装方法について

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

今回のブログ内容は前回の記事の続編ですのでSliverの基本的な実装は下記のブログを閲覧していただけると幸いです。

tech.excite.co.jp

本題になりますが、今回はSliverを使用したUIでSliverToBoxAdapterを使用し、Headerを実装する方法について記述します。

実装したものがこちらになります。 f:id:pomupomupurinkun:20220210105339p:plain

動作バージョン

  • Flutter 2.8.1
  • Dart 2.15.1

SliverToBoxAdapterを使用した理由

A sliver that contains a single box widget.

ドキュメントに記載されているように単一のBoxを含むWidgetであるとわかります。

Headerを使用する場合に使用するには適していると判断しました。(StickyHeader等を実装する場合は別途実装方法を検討する必要があります。)

詳しくはドキュメントを閲覧していただけると幸いです。

api.flutter.dev

実装部分について

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: [
        const _RowHeader(title: 'SliverGrid'),
        SliverGrid.count(
          crossAxisCount: 3,
          children: gridListItem,
        ),
        const _RowHeader(title: 'SliverFixedExtentList'),
        SliverFixedExtentList(
          itemExtent: 150.0,
          delegate: SliverChildListDelegate(listItem),
        ),
      ],
    );
  }
}

class _RowHeader extends StatelessWidget {
  const _RowHeader({
    Key? key,
    required this.title,
  }) : super(key: key);

  final String title;

  @override
  Widget build(BuildContext context) {
    return SliverToBoxAdapter(
      child: Padding(
        padding: const EdgeInsets.symmetric(
          horizontal: 8.0,
          vertical: 4.0,
        ),
        child: Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Text(title),
            TextButton(
              onPressed: () { },
              child: const Text('確認用'),
            )
          ],
        ),
      ),
    );
  }
}

今回の上記の実装内容で記事の初めに掲載したUIの実現ができます。

_RowHeader部分でHeaderに表示するボタン、テキストを実装しています。SliverToBoxAdapterの要素を変更することでテキストのみ、さらに複雑なUIの実装など柔軟にUIを実装することができます。

気になる方は是非お手元の環境で試してみてください。

まとめ

今回はSliverToBoxAdapterを使用することでHeaderを実装しました。複雑なUIになってもSliverGrid、SliverListやSliverToBoxAdapterなどを併用して使用することで実装できそうだなと感じています。要件を達成するために既存のWidgetと比較して、使い分けができれば選択肢が広がるなとも考えています。

最後に

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

www.wantedly.com

IntellJ最新版(2021.3.1)で、Projectツリーに急に表示されるようになったPHPのメソッド一覧を消す方法

taanatsuです。

IntelliJをアップデートしたら、PHPのプロジェクトのProjectツリーに、そのクラスファイルのメソッド一覧が表示されるようになりましたね。 f:id:taanatsu:20220208125252p:plain

今回はこれが(ワタシ的に)邪魔なので非表示にする方法を紹介したいと思います。
それではやっていきましょうか。

表示 / 非表示

やり方は簡単で、Projectツリーの歯車マークから、
Tree Appearance -> Show members から表示/非表示を選択できます。

f:id:taanatsu:20220208125737p:plain

以上です!

それでは快適なIntelliJライフを!

【Java】IntelliJ IDEAで、「.var」したときに自動でfinalをつける

taanatsuです。

IntelliJ開発環境でJavaでローカル変数を作る際によく利用する「.var」。
これをしたときに変数にfinalを自動付与する設定が……新しいバージョンから消えていました。

それを復活させる方法を共有します。

IngtelliJの設定から、
Editor -> Code Style -> Java -> Code Generation に行き、Make generated local variables finalにチェックを入れます。

f:id:taanatsu:20220204192328p:plain
IntellJ IDEAでローカル変数にfinalをつける

これで完了です!

試してみましょう。

"test".varと入力し、Enterを押すと……

final String test = "test";

のようにコードが生成されると思います。

では、今回はこれで!

quarkusを使う(ログ出力編)

夜遅くからこんばんは。エキサイト株式会社中尾です。

quarkusの開発に慣れてきたのですが、いまいちdebug周りが見えなかったので、debugログを全部出すことにしました。

https://quarkus.io/guides/logging

詳しい設定はこちらに記載しているのですが、簡単に説明すると

quarkus.log.category."*".level=DEBUG

にすると全てのdebugログが出力されます。

正直開発の場合は好みではありますが、全て出ていて問題ないかと思います。

理由として、ライブラリの方が原因の可能性もあったり、深い部分の理解にも繋がる可能性があるからです。 ログが出過ぎて困る場合もありますが、その中で必要なログがどれなのかを判別する力もつきます。

おすすめです。