新卒デザイナーがエキサイトテックブログをリニューアルした話

f:id:excite_ny:20210510180431p:plain

はじめに

初めまして!4月にエキサイト株式会社に入社した21卒デザイナーの山﨑と申します。

この度、エキサイトテックブログのリニューアルを担当させていただきました。

f:id:excitech:20210510123301p:plain

新卒入社してから初めての仕事で、このリニューアルを通して学びになった事を記していこうと思います。

なぜリニューアルしたのか

今回エキサイトテックブログがリニューアルに至ったのは、以下の2つが理由でした。

  1. テックブログなのにコードが見辛い。
  2. デザインがデフォルトのままで素っ気ない。

これらの問題を解決するために、同じ21卒のエンジニア達に「現状のテックブログのコードの、どの辺りが見辛いのか」をヒアリングを行いました。

f:id:excitech:20210510125650p:plain

f:id:excitech:20210510125656p:plain:w300

「Zenn」や「Qiita」等のエンジニア情報共有サイトのコードが見やすいという意見があったので、この2つのサイトを参考にしながらリデザインしてみました。

f:id:excitech:20210510130837p:plain

従来のテックブログは背景黒+文字白でバチバチした印象だったのですが、背景紺色+文字パステルカラーにカラーチェンジを行い視認性を向上しました。

デザインのパターン出し

リニューアルを行う上で、ヘッダーとサイドバーにあるバナーを追加する事に決定し、4パターンほどラフデザインを製作しました。

f:id:excite_ny:20210510161039p:plain

デザイナーの先輩達にレビューを頂き、右下のアイソメトリックイラストを使ったバナーに決定しました!

学生時代はよく教授に作品をボコボコに言われていたので、初めてのデザインレビューは「どんな風にボコボコにされるんだろう…」と緊張していたのですが、すごく優しく指導してくださって良かったです😭

f:id:excite_ny:20210510161712p:plain:w300

決定したヘッダーに合わせてバナーも製作しました。

CSSにつまづいたり上手く実装できなかったりなど色々ありましたが、無事にリニューアル出来て良かったです!

最後に

デザイナーとしての最初の仕事はこんな感じになります!

学生時代にデザイン先行で色々作ってきた自分としては、デザインに移る前に仕様書を書いたりなど新しい事ばかりで、すごく勉強になることが多かったです!

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

それでは!

www.wantedly.com

nodejsがDockerでいきなりインストールできなくなった話

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

ある日、久々にDockerのイメージを消してビルドした時、以前は普通にビルド出来ていたはずなのにできなくなっていた…。そんな経験はありませんか? 今回は、nodejsに関するそんなお話です。

Jenkinsの掃除と問題の発生

現在弊社の一部のプロジェクトでは、Jenkinsを使ってデプロイ等を行っています。 ただ、長期に渡ってJenkinsを使い続けていると無駄データが少しずつ溜まっていき、ストレージを圧迫して不具合が起きてしまうので、アラートが起きるとJenkins内の不要なDockerイメージ・コンテナの削除等を行うようにしています。

その日もJenkinsの一時停止・掃除を行い、無事再起動が終了してホッとしていたのですが、その後あるプロジェクトのデプロイができなくなる問題が発生しました。 原因究明をしていく中で、Jenkinsの再起動自体に問題があったわけではなく、Jenkins内のあるDockerイメージを削除したことで再ビルドが必要になり、その再ビルドがnodejsのインストール部分で詰まっていることが判明しました。

WARNING: The following packages cannot be authenticated!
  nodejs

当然、以前は普通に動いていたビルドです。 いわゆる、「何もしていないのに動かなくなった」が発生したのでした。

なにが問題だったのか

結論から言うと、これは GPG error と呼ばれる問題でした。 これは、パブリックキーの認証に失敗しているために起きるエラーです。

おそらく以前は自動的にやってくれていたものが、何かしらの環境や仕様の変更等によって自動では認証できなくなったものと考えられます。

解決方法は簡単で、自動で認証してくれないのであれば手動で認証するようにすれば問題ありません。

例えばnodejsのver.13をインストールしたい場合、

こちらを

ENV NODEJS_VERSION 13
RUN curl -sL https://deb.nodesource.com/setup_${NODEJS_VERSION}.x | bash - \
    && apt-get install -y nodejs

このようにすれば

ENV NODEJS_VERSION 13
RUN curl -sL https://deb.nodesource.com/setup_${NODEJS_VERSION}.x | bash - \
    && apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 1655A0AB68576280 \
    && apt-get update \
    && apt-get install -y nodejs

動くはずです。

まとめ

久々に実行してみると動かなかった…というのはたまに起きます。 焦らず、冷静に対応していきたいものです。

参考文献

UbuntuでGPG errorが出た時の対処法の紹介です。

Javaのenumの基本的な使い方

エキサイト株式会社メディア開発佐々木です。Javaの列挙型であるenumの基本的な使い方を共有します。

enumとは

列挙型と言われます。Javaの列挙型はJava1.5の頃から使え、当時の他の言語の列挙型より強力だったという話があります。そして、Javaの列挙型は単なる定数ではなく、オブジェクトとなり、メソッド等を実装することが可能となっています。これにより使い勝手が格段に向上します。

enumの基本

Javaの列挙型は、このような感じで宣言します。

public enum FruitType {
    ORANGE("オレンジ")
    ,APPLE("りんご")
    ,MELON("メロン")
    ,OTHER("その他")
    ;

    private String fruit;

    FruitType(String fruit) {
        this.fruit = fruit;
    }
}

これですぐに使えます。 しかし、これだけだと、使うときに外のクラスで分岐処理してまうことがほとんどです。

FruitType getType(String type){
     if (FruitType.ORANGE.name().equals(type)) {
         return FruitType.ORANGE;
     }
     if (FruitType.APPLE.name().equals(type)) {
         return FruitType.APPLE;
     }
     if (FruitType.MELON.name().equals(type)) {
         return FruitType.MELON;
     }
     return FruitType.OTHER;
}

こういう分岐処理を外部クラスがやると、内部情報が漏れてしまっていて、弱い設計になってしまいます。

enumはこう使うと便利

enumはこのままだと、ただの定数扱いですが、Javaenumは、Enumクラスというものを継承しており、さらにメソッド等を実装することで処理を内部に閉じ込めて保守性も高く使いやすいものになります。

public enum FruitType {
    ORANGE("オレンジ")
    , APPLE("りんご")
    , Melon("メロン")
    , OTHER("その他");

    private String fruit;

    FruitType(String fruit) {
        this.fruit = fruit;
    }

    private static final Map<String, FruitType> map;
    static {
        Map<String, FruitType> typeMap = Arrays.stream(FruitType.values()).collect(Collectors.toMap(e -> e.name(), e -> e));
        map = Collections.unmodifiableMap(typeMap);
    }


    public static FruitType get(String fruit) {
        return map.getOrDefault(fruit, FruitType.OTHER);
    }
}

static変数やstaticメソッドを使用して

FruitType getType(String type){
    return FruitType.get(type);
}

呼び出しはこんな感じになるので、内部情報も隠蔽できていてenumとしての要件も満たせています。

さいごに

今回はenumの初歩の初歩ですが、他の言語にはenumが無いこともあるので、紹介させていただきました。しっかり使いこなせればとても有用なものになると思います。

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

www.wantedly.com

SpringBootでRestTemplateを使った外部APIを実行している実装をテストする。

エキサイト株式会社 メディア事業部エンジニアの中です。

SpringBootでRestTemplateを使った外部APIを実行している実装をテストする方法を記載します。

やり方は単純で、mockitoを使います。

ユースケース

  • RestTemplateを使った外部APIを実行している実装をテストする

前提条件

amazonにペットショップの一覧と金額を返すAPIがあると、仮に設定します。

  • APIのレスポンス
[
  {
    "id": 1,
    "type": "dog",
    "price": 249.99
  },
  {
    "id": 2,
    "type": "cat",
    "price": 124.99
  },
  {
    "id": 3,
    "type": "fish",
    "price": 0.99
  }
]

実装

  • interface
public interface AmazonStore {
    List<PetGoods> getPetGoods();
}
  • Impl
@Component
@RequiredArgsConstructor
public class AmazonStoreImpl implements AmazonStore {

    private final RestTemplate restTemplate;

    @Value("${spring.amazon.store.pet.url}")
    private String url;

    @Override
    public List<PetGoods> getPetGoods() {
        final UriComponents uriComponents = UriComponentsBuilder
                .fromUriString(url)
                .build();

        HttpHeaders headers = new HttpHeaders();
        final HttpEntity<String> entity = new HttpEntity<>(headers);

        final ResponseEntity<PetGoods[]> exchange = restTemplate
                .exchange(uriComponents.toUri(), HttpMethod.GET, entity, PetGoods[].class);

        if (exchange.getStatusCode().isError()) {
            throw new RuntimeException();
        }

        return Arrays.asList(exchange.getBody());
    }
}
@Data
@Accessors(chain = true)
public class PetGoods {
    private long id;
    private String type;
    private float price;
}

入力例

テストコードです

@ExtendWith(MockitoExtension.class)
class AmazonStoreImplTest {

    @Mock
    private RestTemplate restTemplate;

    @InjectMocks
    private AmazonStoreImpl amazonStore;

    @Test
    @Description("amazon ペットリストの一覧取得")
    void getPetGoods() {

        final PetGoodsDto dog = new PetGoodsDto()
                .setId(1)
                .setType("dog")
                .setPrice((float) 249.99);

        final PetGoodsDto cat = new PetGoodsDto()
                .setId(2)
                .setType("cat")
                .setPrice((float) 124.99);

        final PetGoodsDto fish = new PetGoodsDto()
                .setId(3)
                .setType("fish")
                .setPrice((float) 0.99);

        final List<PetGoodsDto> result = List.of(dog, cat, fish);

        final UriComponents build = UriComponentsBuilder
                .fromUriString("http://localhost/petstore/pets")
                .build();

        HttpHeaders headers = new HttpHeaders();
        final HttpEntity<String> entity = new HttpEntity<>(headers);

        PetGoodsDto[] returnMock = {
                dog, cat, fish
        };

        final ResponseEntity<PetGoodsDto[]> responseEntity = new ResponseEntity<>(returnMock, HttpStatus.OK);

        Mockito.when(restTemplate.exchange(
                build.toUri(),
                HttpMethod.GET, entity, PetGoodsDto[].class))
                .thenReturn(responseEntity);

        ReflectionTestUtils.setField(amazonStore, "url", "http://localhost/petstore/pets", String.class);

        final List<PetGoodsDto> petGoods = amazonStore.getPetGoods();

        Assertions.assertEquals(
                result,
                petGoods
        );
    }
}

出力例(テストが成功しているコンソールログです)

BUILD SUCCESSFUL in 2s

ポイント解説

用意するmockを設定します。

    @Mock
    private RestTemplate restTemplate;

テスト対象をInjectMocksで設定します

    @InjectMocks
    private AmazonStoreImpl amazonStore;

返却されるResponseEntityを設定します。

        final ResponseEntity<PetGoodsDto[]> responseEntity = new ResponseEntity<>(returnMock, HttpStatus.OK);

urlは@ValueでSpringBootのプロパティファイルから設定しているので、以前の記事で設定したReflectionTestUtilsを使って、urlを設定します。

ReflectionTestUtilsで変数に値をセットする - Excite Tech Blog

        ReflectionTestUtils.setField(amazonStore, "url", "http://localhost/petstore/pets", String.class);

あとはいつも通りのmockitoを使ってテストをします。

HttpStatus.OK以外を設定すれば、エラーハンドリングのテストもできます。

MyBatisのDynamicSQLにFreeMarkerを採用する

エキサイト株式会社メディア開発佐々木です。

SpringBoot/Javaで既存システムのリビルド開発を行っていますが、ORマッピー(正確にはORマッパーではない)にはMyBatisを採用しています。テーブル構成はなかなか変えられない為、良くも悪くも自由度の高いMyBatisを採用しています。

MyBatisとは

JavaのORマッパーになります。

MyBatisのクエリ生成(デフォルト)

単純な動的クエリはメソッドが用意されているのですが、joinやら分岐やら複雑なSQLになるとそうはいきません。デフォルトはXMLの設定になります。メリットとしてはこういったものになるかと思います。

  • デフォルトの設定
  • 複雑なクエリで複雑なマッピングXML上で完結できる
  • Javaの型とDBの型とのマッピングが柔軟
  • 1ファイルに複数のクエリを書ける

しかし、XMLを採用しているがゆえのデメリットもあります。

  • XML形式だと、比較演算子で面倒(CDATA[]の中で書かないとエラーになる)
  • XMLの記述が冗長で覚えるのが面倒
  • 複雑なクエリで複雑なマッピングを行えてしまい、XML上でしかテストができないので使わないようにしたい

比較演算子でCDATAを使わないといけないなど、割と面倒です。代替手段がないかの検討を行いました。

FreeMarkerの採用

MyBatisではFreeMarkerSQLを書くファイルとしてサポートしていたので、採用しています。早速ファイルの中身を比較してみましょう。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.UserDataMapper">
    <select id="findById" resultType="com.example.demo.UserData">
        SELECT 
                 id
               , name
               , birth_year
               , birth_month
               ,birth_day 
        FROM 
               USER_DATA 
        WHERE 
               id = #{id} 
               and now() <![CDATA[ < ]]> birth_month
    </select>
</mapper>

FreeMarkerではこうなります。

findById.ftl

SELECT 
          id
          , name
          , birth_year
          , birth_month
          , birth_day 
FROM 
          USER_DATA 
WHERE 
           id = <@p name="id" />
          and now() < birth_month

記述量が結構違いますよね。XMLの方が複雑な制御ができたりするのですが、テストしづらいですし、コードを見たときに理解しづらいコードになったりします。メディア開発ではアジリティを大事にしていますので、同じ効果であれば効率のイイものを選択し、リターンがない複雑なものは選択しないように意識しています。

最後に

MyBatisの標準はXMLですが、エキサイトのメディア開発では、使い勝手や開発のアジリティを考えて最適なものを選ぶようにした結果、FreeMarkerを選択しています。小さなことですが、技術や自分たちが抱えてる問題をしっかり把握し、少しずつでも現状にフィットした適切な選択を積み重ねられる組織運営ができればと思います。

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

www.wantedly.com

JavaのSpring Bootにおいて、クエリパラメータのキー名と引数名が異なる場合のフォームクラスの書き方

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

今回は、Java / Spring Bootを使っているとき、GETリクエスト受付時のクエリパラメータのキーと受け取り変数名が異なる場合の、受け取り用クラス(フォームクラス)の書き方について説明します。

課題

突然ですが、あなたは今Java / Spring Bootを使ってAPIを開発しているとします。 RESTのGETリクエストにおいてクエリパラメータを受け付ける処理を書く時、クエリパラメータのキー名がそのまま受け取る変数名として使えるのであればいいのですが、どうしてもキー名と変数名が異なることがあります。 たとえば、クエリパラメータのキーはスネークケースだが変数名はキャメルケースにしたい、という経験がある方は多いのではないでしょうか。

@RequestParam を使って

@GetMapping()
public String getSampleData(
    @RequestParam(value = "data_a") String dataA,
    @RequestParam(value = "data_b") String dataB,
    @RequestParam(value = "data_c") String dataC,
    @RequestParam(value = "data_d") String dataD
) {
    return "Hello world!";
}

と書けるのであればいいですが、キーが大量にあって1つ1つ引数を書いていると可読性に大きな問題がある場合や、受取時に2つ以上の引数を同時に対象とするバリデーションを行いたい場合(たとえば、 data_adata_b のどちらかは必ず値があることをバリデーションしたい場合)は、引数にクラスを指定することになります。

@GetMapping()
public String getSampleData(
    @ModelAttribute SampleModel sampleModel
) {
    return "Hello world!";
}

@Data
public class SampleModel {
    private String data_a;
    private String data_b;
    private String data_c;
    private String data_d;

}

ただし、このままだと変数名がキー名と同じになってしまいます。

解決策1(コンストラクタの引数名をキー名にし、プロパティ名を使用したい変数名にする)

そこで、以下のようにすることでキー名を想定通りにすることが出来ます。

@Getter
public class SampleModel {
    private String dataA;
    private String dataB;
    private String dataC;
    private String dataD;

    public SampleModel(
        String data_a,
        String data_b,
        String data_c,
        String data_d
    ) {
        this.dataA = data_a;
        this.dataB = data_b;
        this.dataC = data_c;
        this.dataD = data_d;
    }
}

クエリパラメータからデータを受け取るのはフォームクラスのコンストラクタであるため、コンストラクタの引数名をキー名と同じにした上で、フォームクラスのプロパティ名を使用したい変数名とします。

ただしこの場合、コンストラクタの引数名はキー名と同じになってしまうことになります。

解決策2(ConstructorPropertiesを使用する)

コンストラクタの引数名の時点で使用したい変数名にしたい場合は、 ConstructorProperties を使います。

@Getter
public class SampleModel {
    private String dataA;
    private String dataB;
    private String dataC;
    private String dataD;

    @ConstructorProperties({"data_a", "data_b", "data_c", "data_d"})
    public SampleModel(
        String dataA,
        String dataB,
        String dataC,
        String dataD
    ) {
        this.dataA = dataA;
        this.dataB = dataB;
        this.dataC = dataC;
        this.dataD = dataD;
    }
}

ConstructorProperties がクエリパラメータのキーを紐付けてくれます。

まとめ

フォームクラスを使ってクエリパラメータのキーを異なる名前の変数に紐付けたい場合は、コンストラクタや ConstructorProperties を使うと良いでしょう。 プロジェクトのルールにもよりますが、引数名から使用する変数名と同じにしたいというケースが多いと思いますので、 ConstructorProperties を使用する場面が多いのではないでしょうか。

Java15のTextBlockとMyBatisのカスタムクエリについて

エキサイト株式会社メディア開発の佐々木です。

Java15で入ったTextBlockでMyBatisのカスタムクエリがアノテーション内でできないかなと思い試してみました。

結果

できた。これは嬉しい。Javaのことだからアノテーションだけ使えないとかはさすがにないと思いましたが、試してみてできたので、やっぱりさすがだなと思いました。

内容

Javaでは、TextBlockがなかったので、カスタムクエリをXMLやFreeMarkerを用いて別ファイルにしていましたが、TextBlockが入ってくれたおかげで、アノテーション内にも、クエリがかけるようになりました。

@Mapper
public interface BookCustomMapper {

    // いままで
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("findByBookId.ftl")
    List<Books> findByBookIdFtl(@Param("id") Long id);


    // TextBlockの恩恵
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("""
            select
             *
            from
              book
            where
              1 = 1
              <#if id?has_content>
              AND book_id = <@p name="id"/>
              </#if>
            """)
    List<Books> findByBookId(@Param("id") Long id);

FreeMarker記法もちゃんと認識してくれているんで便利ですねー。ちょっとしたカスタムクエリには使ってもいいかなと思います。

最後に

現状は、Java11(LTS)を使用しているんで、Java17になったら思う存分使おうと思います。

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

json diffを効率よく、ワンライナーで

エキサイト株式会社 メディア事業部エンジニアの中です。

今回はjson diffを簡単にできる方法を記載しようと思います。

例えば、以下のようなjsonが2種類あったとします。

{
  "userId": 1,
  "id": 1,
  "title": "naka",
  "body": "sho"
}
{
  "body": "sho"
  "title": "naka",
  "id": 1,
  "userId": 1,
}

ただのdiff コマンドだと、以下のように、順番違うので差分が出てきてしまいます。

diff a.json b.json
2,3c2
< "userId": 1,
< "id": 1,
---
> "body": "sho",
5c4,5
< "body": "sho"
---
> "id": 1,
> "userId": 1

そこで、jq sort-keysでkeyで並び替えて比較すると綺麗に差分を比較することができます。

jqについては以下の公式を参考にしてください。

jq

diff <(jq --sort-keys . a.json) <(jq --sort-keys . b.json)

API通しの比較であれば、curlコマンドを中に入れ込めば、いちいちjsonファイルを作らなくても比較ができます。 新旧APIの比較などに使えそうですね。

差分がない場合

diff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys)

差分がある場合

$ diff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/2' | jq --sort-keys)
2,4c2,4
<   "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto",
<   "id": 1,
<   "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit",
---
>   "body": "est rerum tempore vitae\nsequi sint nihil reprehenderit dolor beatae ea dolores neque\nfugiat blanditiis voluptate porro vel nihil molestiae ut reiciendis\nqui aperiam non debitis possimus qui neque nisi nulla",
>   "id": 2,
>   "title": "qui est esse",

差分がない場合は何も表示されないので簡単ですね。 差分がある場合は、どのキーなのかがわかるためすぐに調査できると思います。 vimdiffを使えば、差分のある文字列がどこなのかを特定しやすいので参考に使ってください。

$ vimdiff <(curl -s 'https://jsonplaceholder.typicode.com/posts/1' | jq --sort-keys) <(curl -s 'https://jsonplaceholder.typicode.com/posts/2' | jq --sort-keys)

f:id:excite-naka-sho:20210430184816p:plain

SpringBoot でSQLをテストする(Mybatis)

エキサイト株式会社 メディア事業部エンジニアの中です。

SpringBootでmybatisで発行したSQLのテストのやり方を記載します。

H2というin-memoryデータベースを用意し、schema.sql、data.sqlの初期データを挿入してから、SQLを発行し、正しくデータが取得できているかテストをします。

ユースケース

  • SQLのテストする

前提条件

  • MybatisTestを使う
  • H2を使う

gradleに以下を追加

        testImplementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter-test:2.1.3'
        testImplementation 'com.h2database:h2:1.4.200'

入力

application.yml

  • SQL server を使用するため、ModeをMSSQLServerに設定します。
  • 細かい設定は公式サイトから参照してください。

www.h2database.com

spring:
  datasource:
    initialization-mode: always
    driver-class-name: org.h2.Driver
    url: jdbc:h2:~/test;MODE=MSSQLServer
    username: sa
    password: sa
  h2:
    console:
      enabled: true
mybatis:
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    org.springframework.jdbc: debug
    com.exblog: debug

schema.sql

CREATE TABLE user
(
    user_id  bigint IDENTITY(1,1) PRIMARY KEY,
    name NVARCHAR(20) NOT NULL
);

data.sql

INSERT INTO user VALUES (1, 'nakao');

MapperTestApplication.java

  • h2を起動させるため、テスト用にSpring Applicationの設定を追加します
  • testファイルが置いてあるパッケージに配置してください。
package com.sqlserver.mapper;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MapperTestApplication {

}

UserCustomMapper

  • SQLを実行するinterfaceとftlです。
@Mapper
public interface UserCustomMapper {
    @Lang(FreeMarkerLanguageDriver.class)
    @Select("find_user.ftl")
    UserModel getUser(
            @Param("user_id") Long userId
    );
}

find_user.ftl

select
  *
from
  user
where
  user_id = <@p name="user_id"/>

入力例

テストコードです

@ExtendWith(SpringExtension.class)
@MybatisTest
class UserCustomMapperTest {
    @Autowired
    private UserCustomMapper userCustomMapper;

    @Test
    public void findByStateTest() {
        final UserModel user = userCustomMapper.getUser(1L);
        assertTrue(user.getUserId().equals(1L));
        assertTrue(user.getName().equals("nakao"));
    }
}

出力例 デバッグモードを使うと発行したSQLの詳細が出力されます。

2021-04-29 23:39:04.683 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : ==>  Preparing: select * from user where user_id = ?
2021-04-29 23:39:04.705 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : ==> Parameters: 1(Long)
2021-04-29 23:39:04.725 DEBUG 4206 --- [    Test worker] c.e.mapper.UserCustomMapper.getUser  : <==      Total: 1
BUILD SUCCESSFUL in 3s

SQLのテストは大変

in-memoryデータベースを用意しましたが、事前準備からテスト実施まで大変ですよね。。。 Readのテストなら事前データの投入ですみますが、CUDだったらもっと大変になります。 それはまた別の機会に記載させていただけたらと思います。

MybatisTestについて、もっと知りたい方は以下を参考にしてください。

mybatis.org

JSR303の@ValidとSpringBootの@Validatedの違い

エキサイト株式会社メディア開発の佐々木です。

Javaには、JSR303 Bean Validationという私の好きなValidation仕様があります。@Validをつければ、クラスのフィールドに@Empty@Min(1)のようなアノテーションをつけるだけで、値のバリデーションが可能です。Springにはこれを拡張した@Validatedがあります。この使い方について軽く触れます。

@Validはなに?

これはJavaの標準仕様で、クラスのフィールドにアノテーションをつけて、所定メソッドを実行するとバリデーションを行ってくれます。

@Data
class Form {
   @NotEmpty
   private String name;
 }


Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); // validatorのインスタンス生成
Form form = new Form(); // データクラスのインスタンス
Set<ConstraintViolation<Form>> validate = validator.validate(form); // バリデーションの実行

フィールドに付けられたアノテーションの情報を元にバリデーションをしてくれます。フィールドの仕様とバリデーションがセットになっているので視認性もよく使い勝手はいいです。ただ、使い勝手が良すぎるせいで、ある程度約束を守ってもらわないと設定がバラバラになってしまうという問題もあります。

@Validatedはなに?

カンタンにいうと、@Validを拡張したものが、SpringFramework内にあります。拡張の主なポイントはグループの指定です。

グループの指定とは?

とあるユースケースで、バリデーションを指定したいフィールドを絞りたいことがあるかと思います。1つのデータクラスにリクエストパラメータを入れるのですが、入力フォームが多段になっていたり、簡易フォームと詳細フォームでわかれていたりとあると思います。そういうユースケースに効果を発揮します。

実装

下記のような実装があります。simpleのエンドポイントは、 idnameは必須、detailの方のエンドポイントは、simpleのエンドポイントに加えて、agenickNameが必須になります。groupsという属性を定義することによって、バリデーションをどこまで行うかの識別を行っています。

@RestController
@RequestMapping("valid")
public class ValidController {

    @GetMapping("simple")
    public Form simple(@Validated(value = {SimpleForm.class}) Form form) {
        return form;
    }

    @GetMapping("detail")
    public Form detail(@Validated(value = {SimpleForm.class, DetailForm.class}) Form form) {
        return form;
    }

    @Data
    static class Form {
        @NotNull(groups = {SimpleForm.class})
        private Integer id;
        @NotBlank(groups = {SimpleForm.class})
        private String name;

        @NotNull(groups = {DetailForm.class})
        private Integer age;
        @NotBlank(groups = {DetailForm.class})
        private String nickName;

    }
}

simpleのエンドポイント

@Validated(value = {SimpleForm.class}) Form form が引数に定義されることによって、Formクラスのフィールドにgroups = {SimpleForm.class}アノテーションが付与されているところだけバリデーションが実行されます。

$ curl "http://localhost:8080/valid/simple?id=1&name=sample"

{"id":1,"name":"sample","age":null,"nickName":null}

detailのエンドポイント

simpleのエンドポイントをdetailに変えて、クエリパラメータはそのままに実行してみます。

$ curl "http://localhost:8080/valid/detail?id=1&name=sample"

{"timestamp":"2021-04-29T15:17:56.903+00:00","status":400,"error":"Bad Request","trace":"org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 2 errors\nField error in object 'form' on field 'age': rejected value [null]; codes [NotNull.form.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [form.age,age]; arguments []; default message [age]]; default message [null は許可されていません] ....

上記のようにエラーがバリデーションエラーが発生しています。これは先程のsimpleのエンドポイントとは異なるバリデーション設定がされています。@Validated(value = {SimpleForm.class, DetailForm.class}) Form formのバリデーション設定で、DetailForm.classが追加されています。こちらが追加されている為、 Formクラスに定義してあるとおり、agenickName の定義が必須になっています。

さいごに

@Validatedについてカンタンに説明させていただきました。気をつけねばならないのが、 groupsをつけると@Validは動作しなくなるというのがあります。ただ、@ValidatedはSpring依存になってしまうので、早いところ共通仕様をだしてほしいところであります

AWS RDSのダウンサイジングとオートスケーリング

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

今回はAWS上のRDSのダウンサイジングに加えAurora Auto Scalingを設定しコストダウンを測った話と、その際の注意点や挙動についてご紹介させていただきます。

また、今回はGUI上での設定についてお話させていただきます。

DBインスタンスの追加・削除

現行で既に動いてるRDSを触る場合、インスタンスサイズを変更すると変更中はそのインスタンスでリクエストを捌くことはできません。

そのため、一時的に大きめのインスタンスを先に作って作業すると良いです。

f:id:excite-at-ma:20210428152508p:plain 1段階〜2段階上のサイズにしておくと安心です。

クラスターの設定に合わせてリーダーを作ってくれるため、一時的なリーダーの名前を決めて設定します。

f:id:excite-at-ma:20210428153802p:plain 便利なことに、自動で異なるAZ配置に振り分けてくれます。

書き込みインスタンスを変える時は、事前にフェイルオーバーで読み取りインスタンスに降格させてインスタンスの変更をすると管理しやすいです。

f:id:excite-at-ma:20210428152957p:plain フェイルオーバーの優先順位は各インスタンスの追加設定から変更できます。

オートスケールの設定

オートスケールは、オートスケールを設定したいクラスターを選択し、[アクション]=>[レプリカのAuto Scallingの追加]で設定できます。

f:id:excite-at-ma:20210428154135p:plain

指定のターゲット値を基にインスタンスの台数が増減していきます。 CPU以外にも、DBの接続数を基にスケールイン・アウトを行うことができます。

今回のプロジェクトの場合、比較的早くアクセスが増えてしばらく高負荷の状態が続くケースが多かったです。

そのため、時間はデフォルトの300秒ではなくスケールイン60秒、スケールダウンは少し長めに300秒に設定しています

f:id:excite-at-ma:20210427164118p:plain 実際にオートスケールを設定すると上記のようにapplication-autoscalling-** といったインスタンスが増えます。

アクセスが増えたことにより、2台のインスタンスが立ち上がり大量の負荷で落ちないようになっています。

また、オートスケールの設定自体はクラスタの[ログとイベント]内の[AutoScalling ポリシー]からいつでも変更可能です。

f:id:excite-at-ma:20210428154716p:plain

障害時のフェイルオーバーに対応する

障害などでフェイルオーバーが起きた際、書き込みのインスタンスと読み込みのインスタンスが入れ替わってしまうことがあります。

f:id:excite-at-ma:20210427165908p:plain
入れ替わりの例
上記のように青が読み込み、赤が書き込みだったものが、ある時を境にフェイルオーバーが起きて入れ替わっています。

こうなると書き込みと読み込みのサイズに差があるとリクエストが捌き切れずにパンクしてしまう恐れがあります。

そのため、基本的にはmasterとreadのインスタンスサイズは同じにしておくのが望ましいです。

終わりに

今回のスケールダウンでRDSコストを当初の半分以下にまで抑えることができました。

ただしあまりにもギリギリに調整しすぎると、万が一の障害が起こった際や想定外のアクセスがきた場合に対処が難しくなるため、サイズにはある程度余裕は持たせましょう。

また、サービスにもよりますがオートスケールのターゲット値も50~60%程度が一番汎用的に使えそうです。

可用性を保ったまま低コストの運用を実現するため、サービスに合わせてうまく負荷分散できるようにチューニングしていくことが重要です。

AWSとGitHub Actionsでデプロイをカイゼン

エキサイトのみーです。

アプリケーションのリリースサイクルを速めるためにも、デプロイの改善は重要な要素の1つだと考えています。
今回は、アプリケーションのコンテナ化に際してデプロイをどのように改善させたのか、について紹介します。

これまで

オンプレにおける従来のデプロイは非常に面倒で、ミスしてくれと言わんばかりのものでした。箇条書きにすると、

  • GitHubでPull Requestして、
  • レビューして、
  • mainブランチにマージして、
  • 踏み台サーバにログインして、
  • デプロイサーバにログインして、
  • シェルスクリプト(rsync)を叩く

というような手順。酷いときは、mainブランチにマージしたけどデプロイを忘れる、なんてことも。
mainブランチと本番環境にズレが生じる、みたいなあり得ない状況も度々発生。
そもそもの作業が億劫なのでデプロイ頻度が下がっていく、となるのも当然の結果でした。

アプリケーションをコンテナ化してそれをデプロイするとなると、同じようなやり方では破綻してしまいます。
ということで、より簡単で、より安全なデプロイ方法を検討することになったわけです。

コンテナ化後

試行錯誤の結果、今では以下のような手順に落ち着いています。

  • GitHubでPull Requestして、
  • レビューして、
  • mainブランチにマージされたら、GitHub Actionsで自動デプロイ

これにより、デプロイ作業がGitHubだけでほぼ完結するようになりました。手数も減って、デプロイサーバの管理も不要になって、まさに一石二鳥。
さらにBlue/Greenデプロイも取り入れたことで、より安全にアプリケーションをデプロイできるようになりました。
ここまで来てしまうと、もう昔には戻れません。

構成

大まかな全体構成は以下のような図になっています。今回は左半分の話になりますね。
特別なことは何もしていません。コントロールプレーンはECS、データプレーンはFargate、イメージはECRで管理しています。

f:id:ex-mii:20210426182208p:plain

GitHub Actions

アクションの作成は難しくはないので、全て自作しても良いとは思います。
が、今回はAWSのサービスをフル活用しているので、AWSが公開しているアクションを有効活用することができました。

# GitHub Secretsに登録したアクセスキーなどをセット
- name: Configure AWS credentials
    id: configure-credentials
    uses: aws-actions/configure-aws-credentials@v1
    with:
        aws-access-key-id: ${{ secrets.PROD_AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.PROD_AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ env.AWS_REGION }}

# ECRにログイン
- name: Login to Amazon ECR
    id: login-ecr
    uses: aws-actions/amazon-ecr-login@v1

# イメージをビルドして、ECRへPush
- name: Build, tag, and push image to Amazon ECR
    id: build-image
    env:
        DOCKER_BUILDKIT: 1
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        IMAGE_TAG: ${{ github.sha }}
    run: |
        docker build -t $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG .
        docker push $ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG
        echo "::set-output name=image::$ECR_REGISTRY/${{ env.ECR_REPOSITORY }}:$IMAGE_TAG"

# タスク定義に、先ほどPushしたイメージを反映
- name: Render Amazon ECS task definition
    id: render-web-container
    uses: aws-actions/amazon-ecs-render-task-definition@v1
    with:
        task-definition: task-definition.json
        container-name: app
        image: ${{ steps.build-image.outputs.image }}

# CodeDeployを使用してECSにデプロイ
- name: Deploy to Amazon ECS
    id: deploy
    uses: aws-actions/amazon-ecs-deploy-task-definition@v1
    with:
        task-definition: ${{ steps.render-web-container.outputs.task-definition }}
        service: ${{ env.ECS_SERVICE_NAME }}
        cluster: ${{ env.ECS_CLUSTER_NAME }} 
        wait-for-service-stability: false
        codedeploy-appspec: appspec.yaml
        codedeploy-application: ${{ env.CODEDEPLOY_APPLICATION }}
        codedeploy-deployment-group: ${{ env.CODEDEPLOY_DEPLOYMENT_GROUP }}

aws-actions/amazon-ecs-deploy-task-definitionは、Blue/Greenデプロイにも対応しています。事前にCodeDeployアプリケーションやデプロイメントグループを作成しておくだけで、簡単にBlue/Greenデプロイができるようになります。

このCodeDeployを使用したBlue/Greenデプロイが、私たちに最高にクールな体験を与えてくれているのですが、それはまた別の機会に。

残課題

いろいろと改善できたものの、まだまだ課題は山積みです。

GitHub Actionsで使用するIAMユーザ

GitHub ActionsのワークフローからAWSリソースへアクセスするため、事前にIAMユーザを作成してアクセスキーをGitHubのSecretsに登録しておく必要があります。

IAMユーザには必要な権限のみを付与しているものの、現在はアクセスキーのローテーションをしていないのでセキュリティ的には微妙です。
万が一に備えて、ローテーションの自動化等を検討しておくべきかと思われます。

Pull Requestへのコメントでデプロイ

開発中はもっと簡単にデプロイしたいものです。
そこで、テスト環境などはPull Requestにコメントすることでデプロイされるよう、GitHub Actionsのワークフローを設定しています。

on:
    # コメントが作成・編集されたときに発火
    issue_comment:
        types: [created, edited]

jobs:
    deploy:
        # Pull Request内のコメントが対象、且つ、コメントの先頭に「/test」と入力された場合のみ
        if: contains(github.event.comment.html_url, '/pull/') && startsWith(github.event.comment.body, '/test')

/testとコメントすればテスト環境へデプロイされちゃいます。すごくお手軽。
ただし、どのPull Requestからもデプロイできてしまうという問題も。チーム開発時は事前にルールを決めておくのが良さそうです。

CodePipelineは使わないの?

GitHubだけで完結したかった、ので使っていません。
また、テスト環境へのデプロイを手軽にやりたかったということもあり、GitHub Actionsのほうが適していると判断しました。

おわりに

特に目新しいことはしておらず、公式ドキュメントなどに記載されていることを愚直に実践しただけに過ぎません。
ですが、普通のことを普通に実行するだけで多くの恩恵を得られたのも事実です。カイゼンのポイントはそこかしこに眠っているはず。

クラウドへの移行は大変な作業ですが、クラウドのメリットを活かした構築をすることで、生産性の向上に大きく寄与できると思っています。
この記事が、クラウド移行を検討されている方にとって少しでも参考になれば幸いです。

MySQLにおける、複合Indexを貼る際の良い順番とは

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

MySQLにおいてIndexは、Selectクエリのパフォーマンス向上のために必要不可欠な要素です。 1Indexに対して1カラムを付けるということであればいいですが、複数カラムを付ける場合、付ける順番によってパフォーマンスが変わってきます。

ここでは、どのような順番で付けるのが良いかについて1つの案を提示します。

Indexとカーディナリティ

Indexを貼る際には、カーディナリティを考える必要があります。 例えば、以下のようなArticle(記事)テーブルがあるとします。

id code title category publish_date active
1 aaa 子育て記事A child 2021-01-01 00:00:00 1
2 bbb 子育て記事B child 2021-01-02 00:00:00 0
3 ccc 子育て記事C child 2021-01-03 00:00:00 0
4 aaa くらし記事A lifestyle 2021-01-02 00:00:00 1
5 bbb くらし記事B lifestyle 2021-01-05 00:00:00 1
6 aaa ビューティ記事A beauty 2021-01-03 00:00:00 0
7 bbb ビューティ記事B beauty 2021-01-04 00:00:00 1

また、以下の条件だとします。

  • id が主キーでAuto Increment
  • code (記事コード)と category (記事のカテゴリ)で複合ユニークなデータ
  • active は0/1のどちらかが入る

active カラムは0と1のどちらかしか無いため、よほど値に偏りがあったりしないとそこまでレコードを絞り込むことが出来ません。 そのような状態を、「カーディナリティが低い」と言います。

一方で id は主キーであり、すなわちユニークなデータなので、 id を指定すれば大きく絞り込むことが可能です。 そのような状態を、「カーディナリティが高い」と言います。

以上のことから、一般的にIndexはカーディナリティが高いカラムに貼るほうが効果が高いため、そのようなカラムに貼るべきです。

複合Indexとカーディナリティ

1Indexに1カラムの場合はもちろんですが、1Indexに複数カラムが付く場合も、カーディナリティが高い順番でIndexを付けるべきだと言われています。

たとえば今回の場合、 code (記事コード)と category (記事のカテゴリ)で複合Indexを作るとしましょう。 上記の例ではそこまで種類に違いはありませんが、これから記事数が増えていくにつれ、カテゴリ数はそこまで増えず、記事コード数は増えていくことが予想されます。 であれば、カーディナリティで言えば記事コードが高く、カテゴリが低いということになるので、順番としては

  1. code
  2. category

という順番で付けるのが妥当そうな気もします。

ですが実は、そう単純なものではありません。

複合Indexの場合、必ずしもカーディナリティ順でなくても良い

今回のテーブルの場合、 codecategory は複合ユニークです。 すなわち、 code だけでは一意にレコードを絞り込むことが出来ないため、 code だけを条件としてSelectクエリが投げられることはあまりないと考えられます。

一方で、記事をカテゴリ別に取得するというケースは十分に考えられます。 すなわち、 category でSelectすることは考えられるケースです。

そのような時、

  1. code
  2. category

の順番であればこのIndexが当たらない可能性があるため、場合によっては別途Indexを用意する必要がありますが、

  1. category
  2. code

の順番であればIndexが当たるため、新たにIndexを用意する必要がありません。

Indexは、作りすぎると逆にパフォーマンスが落ちてしまう原因にもなりえます。 今回はかなりシンプルなケースだったので category のIndexくらい作れば良いかもしれませんが、これがカラム数の多いテーブルになった場合などは問題になる可能性が考えられます。

もちろん、今回で言う active のレベルでカーディナリティが低いカラムを1番目に入れ始めるとまた話は別だったり、要求されるパフォーマンス次第かもしれませんが、 category くらいであればカーディナリティにそう目くじらを立てるほど厳しくする必要はないでしょう。 それよりも、不要なIndexの作成を防ぐ方を考えても問題ないのではないでしょうか。

まとめ

基本的に、複合カラムのIndexはカーディナリティが高い順につけるのが良いと言われていますが、上記のように必ずしもそれに従っていれば最適であるというわけではありません。 Indexに付く複数のカラムが複合ユニークかどうか、アプリケーションで実際にどのように使われる想定なのか、それらによって柔軟に順番を決めていけるとよりアプリケーションのパフォーマンスを向上させていくことができるでしょう。

「第5回 AI・人工知能 EXPO 春」に行ってきた

こんにちは、エンジニアの藤田です。

少し前になるのですが「第5回 AI・人工知能 EXPO 春」という展示会がビッグサイトで開催されてたので行ってきました。

今回の展示会は同会場、同時刻に「第2回 ブロックチェーン EXPO 春」、「第1回 量子コンピューティング EXPO 春」が開催されていて内容としては盛りだくさんな展示会となっていました。

行く目的は2つ。

  1. AIの流行りの把握とビジネス展開の調査

  2. 量子コンピューターの商用利用の現在地を知る

1についてはこの界隈は時の流れが早いので1年くらいで結構内容が変わってたりするので、断続的にウオッチしないといけないですね。 2についてはあまり情報がないのでとにかくなにかきっかけ作れればくらいの感じで望みました。とくにビジネス展開してる企業の話を聞きたく。あと動作原理を勉強中ということもあり個人的な興味強く。

まずAI系プロダクトについてざっくりまとめ。

エッジAI

増えてる。ラズパイ導入なども市民権を得てきているのか、コスト抑えつつクオリティ担保が可能になってきてる。

動画を扱うプロダクト

カメラでのリアルタイム監視系が数年前よりかなり増えている。やはり自然な流れとして動画系に移行しているのだなと。

ノーコード系

既存のソリューションをノーコードに振ることによって顧客ターゲットシフトや差別化の方向性が垣間見れた。

SaaS系とSI・受託に二極化

SaaS系はプロダクトがしっかりしていて、客単価高く、データ主体なので一度使うとチャーンしないといった王道にうまく乗れている企業が強いというのは他ジャンルのプロダクトと同じ。一方、やはりというか受託メインでやる企業が多いが、それではスケールしないという危機感あり。ただSI除いた受託系スタートアップのほうがユニークで前衛感あって、説明員の本気度も高く、話聞く分には情報量・質ともに得るものが多い(必然的に小さいブースを回ることが多くなる)

総じてこの分野は歴史がある分、目新しさはないが、毎年確実に底上げされてきてるといった感じ。 ファーウェイが一番大きなブース出してるあたり時代の流れを感じます(複数プロダクトの紹介で、内容も安定感あり)

次にブロックチェーンまとめ。 こちらは、正直あまり真剣にみませんでした。ブース区画も小さくてそんなに展示物が多くなかったこともあるかもです。 LINEさんがブース出してました。2年くらい前に発表されてたLINK(LINEの仮想通貨)でLINE経済圏作る構想があったことを思い出し、説明していただいた方に聞いてみたら、もうすでにLINEアプリ内で展開中とのこと、LINEをあまり積極的に使ってるほうじゃないのでこのあたりの情報に疎かったと反省。ただ、LINEのユーザー規模をもってしても独自通貨の流通は厳しいらしく、これからだとおっしゃってました(現時点で独立したアプリとなっていてLINEにがっつり組み込まれてる感がないので、おそらく仮想通貨プロダクトのLINE内での位置付けにも関係してるのかもなと思ったり) いづれにしてもLINEは壮大な実験場。

最後に量子コンピューティングまとめ。 こちらもブース区画はそれほど大きくなくて、展示物も目を引くような目玉プロダクトがないように見えた。 少なくとも日本ではまだまだビジネスとして成立させるのが難しい段階なんだなという実感。利用コストも相当なものだそうで、資本力のある企業中心にPoCを回してる。

ハードウエアレベルの量子コンピューターエミュレーターってのがいくつかあって、そういうジャンルがあるのだなと勉強になった。 私のメイン目的はソフトウエア中心にビジネス展開されてるBlueqatさんの話を聞くこと。量子コンピューター関連の独立系企業としては草分け的な存在と勝手に思っている。量子活用というと、古典コンピューターでは組み合わせ爆発による計算困難な創薬や配送経路問題などの解決ってイメージが強いけど、実際は最適化を目的としたさまざまなソリューションを展開していてたいへん興味深かった 。情報検索やレコメンデーション、生成系なども量子の範囲だとのこと。既存の機械学習アルゴリズムを量子に置きかえるという感じ。今より数段使える量子コンピューターが出てきてコストも下がってきたら、既存の知的リソースが流用可能ということで一気に導入が進むんじゃないかと想像する。それがいつになるのかって話ですが、このあと日を改めて伺った話だと日本は相当遅れてるみたい。確かに海外の情報探ってるともうあと3年くらいで来るんじゃないかと思うようになってたので、その肌感は合ってたかもしれない。好奇心とかの段階はすでに終わっていて、エンジニアとしての個人としても、日本の行く末に対しても潮目を逃すのではないかという漠然とした危機感と焦りが残る。

最後に。

こういった展示会には時々足を運んでます。まあ、一回で得られる情報なんてたかが知れてるとはいえ、一度に数百社のプロダクトが展示され俯瞰できるのは魅力で、その時の流行りがなんとなく見えるのと、スタートアップや社内の前衛的プロダクトで真剣勝負している方の生の声を聞くことができる貴重な機会だからです。一社一社アポとって話聞くなどかかなり無理なのでほんと貴重な機会です。コロナ禍でオンライン開催が増えているので直接会話できる機会が減るのはちょっと残念ですけどね。今回は感想程度のことしか書けてないけど、こういう地道な情報収集から得られた知見を体系化していって自社の価値創造に生かせていければなと強く思いますし、それが大きなミッションでもあります。

GithubのPullRequestのコメントにラベルを書くようにして生産性を少し改善する

エキサイト株式会社メディア開発の佐々木です。

メディア開発では、github.comを使ってソースコード管理を行っています。人数やプロジェクトが多くなってくるとPullRequestのコメントの書き方がバラバラになってくるので、生産性を確保する為に下記のようなラベルっぽいものを書くことを推奨する方針としています。

[MUST]… (Must)

必ず直してほしいところをコメントで記載します。どう直してほしいのか具体例をつけて書くことを推奨します。メンテしづらくなったり、パフォーマンスが著しく悪かったり、色々な観点がありますが、その観点がコミュニケーションでは大事なので、その点を書くようにします。

(例)
[MUST]
この実装はパフォーマンスがよくないとおもうので、 `xxxx` を使った書き方になおしてほしいです

GithubのSuggestion機能

明確な修正提案は、GithubのSuggestion機能を使ってもOK。

f:id:earu:20210425223421p:plain

複数行にまたがる場合は、cmd + 行数で範囲選択ができるので、それをやってからコメントすると作ることができます。

[IMO] (In my opinion)

自分ならこう直すみたいな提案型のコメントになります。具体的にメリット・デメリットも提示しながらコメントできると、チームの実装力が向上するかとおもいます。

(例)
[IMO]
この部分は、こういう書き方もできるとおもいます

[NITS] (nits pick)

細かい指摘等に対するコメントです。直しても直さなくてもいい程度の修正ですが、なるべく直すような方針にしています。

(例)
[NITS]
このコメントは不要な気がします

[ASK] (ask)

実装内容がわからないときとかに使います。

(例)
[ASK]
ここの実装の中身がわからないです

[GOOD] (good)

実装がよい場合にこちらを書いたり、絵文字でアクションすると良いコミュニケーションになので使用します。

(例)
[GOOD]
この書き方いいですね!

[HELP]

プルリクを発行する人が、いい解決法が見つからないときに、これを書いておきます。わからないところや解決したいことを書いておくとレビュアーが知見や解決方法の提案を書きやすくなります。

(例)
[HELP]
ここのいい解決方法が思いつかないです。やりたいことは xxx で、ちょっと冗長になってしまっているのを解消したいです。

対応できなかった指摘

指摘をもらったのに、対応できなかったものに関しては、忘れないようにgithub issueをたててもらうようにしています。コメントからissueをたてられるので、便利です。

f:id:earu:20210425231730p:plain

おわりに

冒頭にも書いたとおり、人数やプロジェクトも増えてくると指摘の仕方やコメントの仕方に関しても、みんなバラバラになりがちで、生産性があがりません。これを是正しようとしてフォーマット等を厳しくすると、これもまた指摘するのが面倒になったり、守られてないプロジェクトなどがでてきて、生産性に寄与しません。現在の部署では最低限のゆるめのフォーマットを使用してある程度はワークしているとおもいます。ガチガチにした方がいいとか いう意見もありますが、生産現場では、生産性の高さが勝負になってくると思います。これからもメンバーのスキルや考え方を見ながら、バランスを意識して開発効率をあげていければと思います。