Studyplus Engineering Blog

スタディプラスの開発者が発信するブログ

MySQLの0000-00-00 00:00:00という負債とridgepoleの限界

こんにちは、CTOの島田です。 今回は、StudyplusのDBのmigrationで発生した問題とその解決ステップを説明したいと思います。

前提

まずは前提。

  • Aurora MySQL 5.7
  • Rails 5.1.6 (対応当時。今は5.2.2)

schema.rbでの運用

Studyplus本体のmigrationは、色々な経緯によって2018年5月まで、いわゆるRailsのmigration の作法とは異なる方法で運用されてました。

schema.rb でスキーマの状態を管理してはいたのですが通常とはやや異なる管理がされていました。 以下、改変して一部抜粋。

ActiveRecord::Schema.define do
  execute %q(
    CREATE TABLE `samples` (
      `id` bigint(20) NOT NULL AUTO_INCREMENT,
      `content_type` varchar(256) DEFAULT NULL,
      `last_modified_at` datetime DEFAULT NULL,
      `issued_at` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
      PRIMARY KEY (`id`),
      UNIQUE KEY `content_type ` (`content_type `)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  )
...
end

このように execute で直接DDLが記述されていました。 開発環境を構築する場合や自動テストを実行するにはこれで良いですが、カラムやインデックスの追加、削除などのALTER TABLE に対してはこれでは対応できません。
そういった変更を各環境に適用するためにはDDLを直接DBで実行するという属人的なオペレーションが必要になっていました。

ridgepoleの導入

そこでschema.rbでの運用による解決として、ridgepole の導入を決めました。
これで煩雑かつ属人的でリスクの高いスキーマ変更作業から解き放たれるかと思ったのですが、別の問題が勃発しました。

github.com

"0000-00-00 00:00:00" 問題

ridgepoleを導入してSchemafileを生成し、以下のようにdry-runを実行するとなぜか差分が発生してしまいました。

以下、改変して一部抜粋。

$ ridgepole -c ./config/ridgepole.yml -f ./db/Schemafile --apply --dry-run
Apply `./db/Schemafile` (dry-run)
change_column("samples", "issued_at", :datetime, {:unsigned=>false, :comment=>nil})
...

# ALTER TABLE `samples ` CHANGE `issued_at` `issued_at` datetime NOT NULL
...

このようなカラムが、なんと60テーブルに及び90カラム以上!

問題の原因は?

差分として change_column... となってしまうカラムには共通点がありました。
それは、全て DATETIME でDEFAULT '0000-00-00 00:00:00'でした。

よくよくSchemafileを確認してみると、確かに

create_table "samples", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" do |t|
...
  t.datetime "issued_at", null: false
...
end

となっていました。

試しに、

t.datetime "issued_at", null: false

t.datetime "issued_at", null: false, default: '0000-00-00 00:00:00'

とすると、その部分のchange_column... は出力されなくなりました。

ここからは推測ですが、ridgepoleではActiveRecordを利用しているため、Schemafileをexportする際に '0000-00-00 00:00:00'を認識することが出来ないのではないかと考えました。

例えば以下のようなデータがあった場合に、

mysql> SELECT  `samples`.* FROM `samples` ORDER BY `samples`.`id` ASC LIMIT 1 \G
*************************** 1. row ***************************
                 id: 1
...
          issued_at: 0000-00-00 00:00:00
1 row in set (0.00 sec)

ActiveRecord では、

[1] pry(main)> sample = Sample.first
...
=> #<Sample:0x000055b595171600
 id: 1,
...
 issued_at: nil>

というように、nilと認識をされてしまう事と関連があるのではないかと思いました。

これはridgepoleやActiveRecordの不具合でなく、0000-00-00 00:00:00を利用している事が問題であり、0000-00-00 00:00:00の撲滅をしてく事を決めました。

0000-00-00 00:00:00がなぜ良くないかについては、そーだいさんの MySQLの0000-00-00 00:00:00は使ってはならない の記事を参照してもらえればと思います。

soudai.hatenablog.com

暫定対応

ただ撲滅をするといっても数も多く、すぐに全てを対応するのは難しい状態でした。
しかし、スキーマ変更でDDLの直接実行をするのを防ぐ事を優先したかったので、やむえず暫定オペレーションを考えることにしました。

それは、$ ridgepole --export -o Schemafile で生成したSchemafileに default: '0000-00-00 00:00:00' を追加するRakeタスクを作るという苦肉の策でした。

Rakeタスクの内容としては、

  1. ridgepole で Schemafile を export(ファイル名: SchemafileImport)
  2. Rakeタスクにてdefault: '0000-00-00 00:00:00'を追加する対象の列に、defaultを追加して、Schemafileを別途出力(ファイル名:SchemafileExport) という処理になります。

そして、スキーマ変更をする順番は、

  1. $ rake database_export:with_default_datetime
  2. ridgepole の実行にはSchemafileExportを利用する
    • $ ridgepole -c ./config/ridgepole.yml -f ./db/SchemafileExport --apply

という手順となっていました。

以下、Rakeタスクを一部改変して掲載

namespace :database_export do
  desc "Execute ridgepole export"
  task :with_default_datetime do
    # ridgepoleで生成するSchemafile
    IMPORT_OUTPUT_PATH = "./db/SchemafileImport".freeze

    # スキーマ変更時に利用するSchemafile
    EXPORT_OUTPUT_PATH = "./db/SchemafileExport".freeze


    OVERWRITE_TARGETS = {
      samples: ["issued_at", "xxxx_at"],
      xxxs: ["xxxx_at"],
       ...
    }.freeze

    @updated_schema = ""

    puts "exporting schema"
    sh "ridgepole -c ./config/ridgepole.yml --export -o #{EXPORT_OUTPUT_PATH}"
    sh "ridgepole -c ./config/ridgepole.yml --export -o #{IMPORT_OUTPUT_PATH}"

    puts "adding default value to EXPORT file"
    File.open(EXPORT_OUTPUT_PATH, "r") do |file|
      file.each_line("") do |lines|
        line_array = lines.split("\n")
        create_table_line = ""
        create_table_line = line_array[2] if line_array[0]&.match(/^\#/) && line_array[2]&.match(/^create_table/) # 1個目のテーブル用
        create_table_line = line_array[0] if line_array[0]&.match(/^create_table/)
        if create_table_line
          create_table_line_array = create_table_line.split(/\s/)
          table_name = create_table_line_array[1]&.match(/[a-zA-Z\_]+/).to_s
          if OVERWRITE_TARGETS.keys.include?(table_name.to_sym)
            line_array.each do |line|
              col_array = line.split(/\s/)
              col = col_array[3]&.match(/[a-zA-Z\_]+/).to_s
              if OVERWRITE_TARGETS[table_name.to_sym].include?(col)
                lines.gsub!(/#{line}/, line + ", default: '0000-00-00 00:00:00'")
              end
            end
            @updated_schema += lines
          else
            @updated_schema += lines
          end
        end
      end
    end
    puts "updating file"
    File.open(EXPORT_OUTPUT_PATH, "w") do |file|
      file.puts(@updated_schema)
    end
    puts "update finished"
  end
end

問題の解消

しばらくは、このRakeタスクでSchemafileを生成する運用と並行して、0000-00-00 00:00:00を廃止するという地道な作業をしていきました。 ただ、幸運にもほとんとの対象が、created_atupdated_atだったので(なぜ0000-00-00 00:00:00だったのか謎だが)

  t.datetime "created_at", null: false, default: '0000-00-00 00:00:00'
  t.datetime "updated_at", null: false, default: '0000-00-00 00:00:00'

  t.datetime "created_at", default: -> { "CURRENT_TIMESTAMP" }, null: false
  t.datetime "updated_at", default: -> { "CURRENT_TIMESTAMP" }, null: false

のように CURRENT_TIMESTAMPにしていくことで対応できました。

解消手順としては、

  1. 対象カラムを利用している箇所をCURRENT_TIMESTAMPにして問題ないか確認、必要に応じて修正
  2. 修正したコードをデプロイ
  3. Rakeタスクを変更
  4. ridgepoleにてALTER TABLEDEFAULTを修正

という事をひたすら繰り返しました。

あるべき姿

そうして、少しづつ進めてどうにか以下のようにする事ができました。

$ ridgepole -c ./config/ridgepole.yml -f ./db/Schemafile --apply --dry-run
Apply `./db/Schemafile` (dry-run)
No change

最後の対応が終わった時のSlackの投稿

まとめ

Studyplusはリリースから、2019年で7年目を迎えます。
最初はRailsのバージョンが3.0.9でしたが、現在は5.2.2となっており、これまで様々なコントリビューターの方が関わってきました。
そのため、サービスの運用を続けていれば必然的に発生する技術的な課題が弊社にも少なからずあります。
しかし、それらを放置する事なく地道に改善を続けて、モダンな仕組みを取り入れています。
もし、そんなスタディプラスのサーバーサイドの開発に興味がある方は、是非 こちら から応募いただければと思います。
(または、私のTwitter にDMをいただくでも構いません)

info.studyplus.co.jp

新規プロジェクトにVueとVuexを採用してみた

こんにちは。Studyplus開発部の田口です。
新規で立ち上がった開発プロジェクトにVueVuexを採用してみたので、今回はその所感を書こうと思います。

Vue/Vuexの採用理由

今回のプロジェクトで開発するのは、Studyplusのアプリ内ブラウザのWebViewページです。
プロジェクトを開始するにあたって、アプリケーション自体は小規模で、画面の遷移がありつつサーバーとの通信を極力避けユーザーの入力情報を保持したいため、SPAで開発することにしました。
開発にあたって、

  • モダンな技術を採用し、社内に知見を貯めたい
  • フロントエンドの専任がチームにいないため、専任でないエンジニアがコードを読むときなるべくコストがかからない
  • (上記に関連して)日本語のドキュメントがあるとよい
  • メインで開発を行う自分が過去に少しだけFluxアーキテクチャを触ったことがある

という理由でVueとVuexの採用を決めました。
以下は日本語の公式サイトです

Vue: https://jp.vuejs.org/index.html
Vuex: https://vuex.vuejs.org/ja/

プロジェクトの立ち上げ

Vueの魅力のひとつに、アプリケーションの開発に役立つビルドやルーティングなどの機能が公式のサポートライブラリとして配布されており、エコシステムが充実していることが挙げられます。
今回利用した状態管理のVuexも、ルーティングで利用したvue-routerも公式が出しているライブラリです。基本的に公式が出しているライブラリが充実しているため、ライブラリの選定に悩まないところは採用して良かったと思った点です。
そして、プロジェクトの雛形を作成するCLIツールでああるVue CLIも、公式が出しているライブラリです。

Vue CLIを利用する上での注意点

Vue CLIはv3がリリースされて名称が変わっています。npm installする際に注意が必要です。

$npm install -g @vue/cli
または
$yarn global add @vue/cli

でインストールします。
パッケージ名の前半の@vueは、npmでライブラリを公開する際の名前空間です。この名前空間を利用することで、vue公式が出しているパッケージであることがすぐに分かったり、ライブラリ名が他のライブラリと被ってしまうことがなくなります。

@vue/cliをインストールする際に、Vue CLI v2以前のvue-cliがすでにインストールされている場合は、古いバージョンのアンインストールが必要です。また、Vue CLI v3のインストールにはNode.jsのv8.9以上のバージョンが必要です。

@vue/cliでプロジェクトの雛形を作成する

プロジェクトの作成はvue create project-nameでできます。すでにプロジェクトディレクトリを作成していた場合は、そのプロジェクトでvue create .を実行します。
コマンドライン上で自分が利用するパッケージを選択していくだけで、そのパッケージが入ったプロジェクトの雛形が作成されます。非常に便利です。

Webpackの設定を追加できる

Vue CLI v3で作成してプロジェクトは、v2とは違ってWebpackの設定が隠蔽されています。
ですが、自分でWebpackの設定を追加したい場合は、vue.config.jsというファイルをルートディレクトリに作成し、そこに追加の設定を書くことでWebpackの設定を独自にカスタマイズできます。 今回のプロジェクトでは独自設定はほとんどしませんでした。詳しくはリファレンスを参照してみてください。

Vuexを利用した状態管理

Vuexを使用した理由

今回はアプリケーションの状態管理をVuexで行うことにしました。
上述の通り、フロントエンド専任のエンジニアがいなかったことと、メインの開発を担当する自分がFluxアーキテクチャを触った経験があったということもありVuexを採用しました。
Nuxtを利用することも考えましたが、小規模なプロジェクトなこともあり今回はVuexのみ扱うことにしました。

Vuexの基本的な考え方

スクリーンショット 2019-01-24 22.34.12.png (30.2 kB)

Flux、 Redux そして The Elm Architectureから影響を受けています。

(https://vuex.vuejs.org/ja/ より引用)

とあるように、Vuexは単一方向のデータの流れが特徴のライブラリです。
基本的な考え方はFluxアーキテクチャと同じです。

Vuexでは、stateの更新をactionではなくmutationで行います。stateの更新の唯一の方法はmutationをcommitすることで、actionがdispatchするのはmutationということになります。ここがRedux等と違うところでしょうか。

actionの使い所は、非同期処理を行いその結果をstoreに反映させたいといった場面です。
React/Reduxを使う場合は、redux-sagaやredux-thunkなどのmiddlewareを導入すること考慮に入れる必要がありますが、Vuexではコアな機能としてデフォルトで利用できるので、ライブラリの選定コストがかからないところがいいですね。

今回は初めてのVuexの利用ということで、基本的に全てのstateの変更はactionを通すようにし、mutationの命名を定数で管理するようにしました。以下のような感じです。

// src/mutation_type.js

export const hogeTypes = {
  // hogeのリクエストが成功する
REQUEST_SUCCESS: "REQUEST_SUCCESS",

// hogeのリクエストが失敗する
REQUEST_FAILURE: "REQUEST_FAILURE"
}
...
// src/store/modules/hoge.js

import { hogeTypes } from "../../mutation_type"

const hogeModule = {
  namespaced: true,
  ...
  mutations: {
    [hogeTypes.REQUEST_SUCCESS](state, payload) {
      // 成功時のstateへの処理
    },
    [hogeTypes.REQUEST_FAILURE](state, payload) {
      // 失敗時のstateへの処理
    }
  },
  actions: {
    async requestHoge({ dispatch }) {
      // リクエスト処理
      try {
        const hoge = await fetchHoge()
        dispatch("doneFetchHoge", hoge)
      } catch (e) {
        dispatch("failureFetchHoge", e)
      }
    },
    doneFetchHoge({ commit }, hoge) {
      // リクエスト成功
      commit("REQUEST_SUCCESS", hoge)
    },
    failureFetchHoge({ commit }, e) {
      commit("REQUEST_FAILRE", e)
    }
  }
}

必ずしも全てのmutationをactionを通じてcommitする必要があるわけではなく、プロジェクトの規模やスピード感に応じて柔軟に変更できるので、今後Vuexを採用する場合はもっと柔軟に利用していきたいですね。

Vue/Vuexを利用してみて

SFCが分かりやすい

今回のプロジェクトはVue/VuexをSFC(Single File Component)で実装していったのですが、非常に書きやすかったです。
Vuexではviews(pages)で一つの画面の構成を表現し、そのviews内でいろいろなcomponentsを呼び出します。
views(pages)とcomponentsの関係やcomponentsの切り分け方など、下記のブログが非常に参考になりました。

aloerina01.github.io

SFCでコンポーネントを書くと、template script styleをすべて一つのファイル内で定義できるので、見通しがいいと感じました。styleはscopedにもできるので、セレクタの命名で機能ごとのプレフィックス等を考える必要もなく、シンプルな命名ができます。
個人的にはSFCでのコンポーネント実装が書きやすくて便利だと感じました。

storeのmodulesが名前空間で分かれている

storeのmodules化によってstoreを機能ごとに分割でき、さらにそのmodulesに名前空間を設定できるのも便利だと思いました。mutationの命名で被ってしまうことがあっても、namespacedなstoreなら問題ないです。

ハマったところ

名前空間で分けたstoreの引数付きgettersの呼び出し

storeにgettersを定義する際、引数付きのgetterを定義できます。
その引数付きのgetterをmapGettersで呼び出そうとした際にどうしても呼び出せなかったところで時間を使ってしまいました。
mapGettersではなく、普通に(this.$store.getters.getData(args)のように)呼び出すことで解決できます。
ただし、namespacedなstoreに定義したgettersの呼び出しは、this.$store.getters["namespace/getData"](args)といった書き方をします。
gettersのプロパティにアクセスする際に.ではなく[ ]を利用します。これはES2015の算出プロパティという構文で、[ ]で括られた部分が評価され、その値がプロパティ名になります。(この算出プロパティ、上述の例のmutationsにも利用されています。)
namespacedなstoreに定義されたgetterは"namespace/getData"というプロパティ名なので、[ ]を使わないとアクセスできないということです。
今回のプロジェクトで初めて知った構文だったのでちょっと戸惑いました。

こうしたほうがよかったと思うところ

TypeScriptの導入

今回のプロジェクトでは、ほぼ初学でVueを使い、プロダクトも小さいためJavaScript(ES2015)で実装しました。
しかし、主にstore(state)の設計をする際に型がほしいと強く思いました。
stateは単なるJavaScriptのObjectです。プロパティ名を決めても、入ってくる値を型で制限できないので、意図した型ではない値が入ってしまうことが考えられます。
単にSPA内でstateに値を追加するときもバグに気づきやすくなるので有用ですが、外部のサーバーと通信してJSONで取得したレスポンスをstateに追加するといったときなどに特に効果があると思います。
今回導入しなかったことの後悔を忘れず、次の機会があったら積極的にTypeScriptを導入しようと思います。

まとめ

Vue/Vuexを初めて利用しましたが、わかりやすい書き方で日本語のドキュメントがあるところが導入障壁を下げてくれているなと感じました。
とっつきやすさというのも重要な要素ですし、エコシステムが充実している、活発なコミュニティ活動がある、など盛り上がっている理由がよくわかります。
今後もSPAの開発プロジェクトで積極的に導入していきたいと思います。

インフラエンジニアがオフィス移転で考えたこと[後編]

インフラエンジニアの菅原です。

弊社のオフィス移転が完了しました。
f:id:ksugahara08:20190111111915j:plain

前編で引越しの計画を立てたものの、事は上手く運ばず、色々学びがありました。

今回はインフラエンジニアがオフィス移転で考えたこと[前編]の続きとして振り返りと学びを共有したいと思います。

プロバイダー契約

振り返り

現状の棚卸しをしたところインターネット回線の2重契約や電話回線の契約に不要なFAXの契約が入っていたりしていたため、契約プランを変更することでコストカットを進めました。

また、前編でも話した通り社内ネットワークの平行期間を設けました。具体的には以下の様にインターネット回線契約を「解約」と「新規」にしました。
「解約」・・・旧オフィスを12月末まで
「新規」・・・新オフィスを12月頭から
平行期間を設けることで余裕を持って動作確認を行うことが出来ました。
しかし、全て上手くいったわけではありませんでした。プロバイダーとの認識のズレで旧オフィスのインターネット回線を「移行」にさせられてしまい、12月頭にインターネットが使えないという事態も起きました。

学び

棚卸しはコストカットに繋がることもありますが、契約の変更はよく検討した方が良いと思います。電話回線契約が必要だったりと、要件を見極めるためにも前任者に連絡が取れるのなら事情を確認してからにした方が良いと思います。

社内ネットワークの移行は平行期間を設けたことで余裕が生まれました。
しかし、インターネットが旧オフィスで使えない期間ができてしまいました。プロバイダーには旧オフィスの解約日を書面等で明確に伝えておくべきだと思います。

AWSの設定変更

振り返り

弊社では社内の固定グローバルIPでアクセス制御をしている部分があります。
移転時には固定IPが変更になってしまうため、各サーバーに設定変更をかける必要がありました。

具体的にはSecurity Group、IAM、nginxの設定箇所を移転に合わせて変更しました。
事前に移転先の固定IPがプロバイダーから発行されるので、引越し前に新固定IPを追加し、引越し後に旧固定IPを削除する形を取りました。

学び

前任者の方が移転のことも鑑みてAnsibleで設定をまとめてあったので非常に楽でした。
構成管理ツールでサーバーの設定を記述しておくことはオフィス引越しでもメリットがあります。

コミュニケーション

振り返り

今回の移転で最もコストが掛かったのは、関係各所とのコミュニケーションでした。話したことと実際のアウトプットが違っていたり、合意した内容に齟齬が発生していました。
ドキュメントを作って満足するのではなく、相手が理解・納得して合意を取る。進捗確認・共有という基本的な部分が当初から出来ていなかったのは反省点だと思います。

学び

闇雲にミーティングを行うのは愚策だと思いますが、話した事や決まった事はメールやドキュメントで書類にして合意を形成するという基本をちゃんと行っておくべきだと思います。基本大事!!

スケジュール

振り返り

実際に作業して頂く先方とスケジュールを確認したところチグハグになっていることがわかりました。
コミュニケーション不良もありましたが、スケジュールが明確になっていない、進捗がわからないという状況になりかけていました。

学び

自分兼共有用のスケジュールやチェックリストを作成して、共有するようにしました。
具体的には以下のようなものを用意しました。

日付 担当 内容 場所 ステータス
11/15 電話回線業者 電話回線工事(立会) 御茶ノ水
11/21 ISP業者 光回線工事(ダークファイバー側)(立会) 御茶ノ水
11/27 ISP業者 光回線工事(1階から4階側)(立会) 御茶ノ水
12/3 社内インフラ 新固定IPをAnsibleで追加する 代々木
12/4 電話回線業者 電話回線本工事(立会) 御茶ノ水
12/4,5 電気工事業者 無線AP配線取付、新規ルーター設置 ネットワーク試験 電話先行配線(立会) 御茶ノ水
12/14 社内インフラ 床下配線回収、複合機の持ち出し 代々木
12/15 電気工事業者 2台目ルーター設定、動作確認、電話設備撤去運搬取付、複合機用LAN増設 御茶ノ水
12/15 複合機業者 複合機の移設、設定 御茶ノ水
12/15 電話回線業者 旧モデム一式返却(13時〜17時立会必要) 代々木
12/15 社内インフラ 床下配線回収 代々木
12/17 複合機業者 2台目複合機の増設、設定 御茶ノ水
12/17 社内インフラ 旧固定IPをAnsibleで削除する 御茶ノ水
12/19 ISP業者 旧モデム一式返却(15時頃) 代々木
12/20 複合機業者 2台目複合機の増設、設定 御茶ノ水

全体から逆算して、いつまでに何が出来ていないといけないか把握することは大切だと改めて思いました。

移転作業

振り返り

旧オフィスでは床下の配線を弊社側で行っていたため原状回復が必要でした。ビル側の要望としても元に戻す様に言われていたため配線を回収する必要がありました。

f:id:ksugahara08:20190111114139j:plain
床下の配線回収

学び

原状回復でどこまで行うかはビルの管理を行っている方に確認したが良いと思います。有志を募って回収作業を行いましたが、回収方法などの計画は事前に練っておいた方が良いと思います。

まとめ

  • プロバイダー契約は棚卸しして、ゼロベースで検討してみても良いと思います。
  • 移転時はインターネット回線の平行期間を設けると色々とメリットがありました。
  • コミュニケーション不足陥らないためにスケジュール確認や進捗確認などまめに行うべきだと思います。エビデンスは文面で残す様にしましょう。
  • 移転作業における自分の作業範囲を確認しましょう。
御茶ノ水新オフィス紹介

新オフィスにはイベントスペースやバーカウンター、自由に使える作業スペースもあります。

f:id:ksugahara08:20190111112818j:plain
イベントスペース
f:id:ksugahara08:20190111112659j:plain
バーカウンター
f:id:ksugahara08:20190108105522j:plain
作業スペース

弊社では新しいオフィスで働く仲間を募集しております。
興味のある方はこちらからご連絡下さい。

Railsで流量の多いデータをページネーションする

はじめまして、for School事業部のサーバーサイドエンジニアの冨山です。
今回はfor SchoolのリニューアルにおいてAPIでリアルタイムなデータをページネーションする上でJinraiというカーソルベースのページネーションライブラリを開発しました。
今回はその開発の経緯や直面した課題についてお話していきたいと思います。

TL:DR

  • ページベースのページネーションだとクライアントへ渡すデータに重複が発生する。
  • 基点となるレコードを示すカーソルをクライアントへ渡すことでOFFSETを意識することなく特定の範囲のデータを取得できる。
  • IDをクライアントに渡したくない場合、カーソルとして渡す値がユニークで無いとクライアントに渡らない情報が発生する。
  • ソートキーの値が重複する場合IDでもソートすることで上の問題が解決する

Jinraiの使い方

class Post < ApplicationRecord
  cursor_per 5 # 一度に取得する件数の指定
end

class PostsController < ApplicationController
  def index
    @posts = Post.cursor(since: params[:since], till: params[:till])
  end
end

posts = Post.cursor # 最新5件のレコード
since = posts.till_cursor # postsの最後のレコードを示すカーソル
Post.cursor(since: since) # postsの最後のレコード以前の5件のレコード

till = posts.since_cursor # postsの最初のレコードを示すカーソル
Post.cursor(till: till) # postsのレコードより最新のレコード

デフォルトではidでソートされますが、id以外のキーでソートしたい場合sort_atオプションを渡すことで実現できます。

Post.cursor(sort_at: :updated_at) #=> updated_atでソートされた上から5件のレコード

開発経緯

Studyplus for Schoolでは生徒の勉強記録の情報をタイムラインに表示するような機能があります。

当初はKaminariを使っていましたがそれだとデータの流量が増えたときにページ間でデータの重複が発生するためカーソルベースのページネーションを作ろうという話になりました。

課題1

最新データから一定件数ずつ取得するタイムラインでは常にデータの先頭に最新のレコードが挿入され続けるためページ型のページネーションだとデータの重複が発生します。
以下にその一例を示しますが、下のような投稿のテーブルがあるとします。

posts

id text updated_at
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
6 post06 2018/12/13 14:08
5 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

Kaminariで上のテーブルから最新のデータを5件づつデータを取得すると、

Post.order(id: :desc).page(1).per(5).pluck(:id) #=> [10, 9, 8, 7, 6]

発行されているSQLは

SELECT posts.* FROM posts ORDER BY posts.id DESC LIMIT 5 OFFSET 0

となります。

ここで新しい投稿があり、データ更新されるとテーブル内の情報は下のように更新されます。

posts

id text created_at
11 post11 2018/12/13 14:20
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
6 post06 2018/12/13 14:08
5 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

このタイミングで先程の方法で2ページ目を取得すると、発行されるSQLは

SELECT posts.* FROM posts ORDER BY posts.id DESC LIMIT 5 OFFSET 5

となるため

Post.order(id: :desc)page(2).per(5).pluck(:id) #=> [6, 5, 4, 3, 2]

は、id=6のデータが重複してしまいます。 ページベースのページネーションで上のようなリアルタイムデータを扱うと重複や削除処理などが入れば表示されないデータが発生します。

解決策

そこで最初と最後のレコードを一意に特定可能な値を渡すことで、そのレコードを基点に取得すれば上のような問題は発生しなくなります。 一般的な方法にIDを渡す方法があり、IDはユニークなのでクライアントへ直前に渡したデータの任意のIDをリクエストに含めて貰えれば重複なく前後のデータを返すことができます。

例えばkaminariでは2ページ目を取得する場合

curl -X GET http://example.com/users?page=2

といったリクエストを受けますが、Jinraiでは以下のような形でリクエストを受けます。

curl -X GET http://example.com/users?since=6

ここで発行されるSQLは以下のようになります。

SELECT  posts.* FROM posts WHERE posts.id < 6 ORDER BY posts.id DESC LIMIT 5

この方法であれば上の新しい投稿がされた場合であっても直前に受け取ったデータの最後のIDをリクエストのsinceパラメータに含めてあげることで、重複なくデータを取り出すことができます。

課題2

上でデータの重複は解決できますが、クライアントにIDを渡したくかったり、IDでなくupdated_atなどの異なる属性でソートされたデータが欲しいといったケースが出てくると思います。

下はpostsをupdated_at > idの順序でソートされたデータです。

posts

id text updated_at
10 post10 2018/12/13 14:12
9 post09 2018/12/13 14:11
8 post08 2018/12/13 14:10
7 post07 2018/12/13 14:09
5 post06 2018/12/13 14:08
6 post05 2018/12/13 14:08
4 post04 2018/12/13 14:06
3 post03 2018/12/13 14:05
2 post02 2018/12/13 14:04
1 post01 2018/12/13 14:03

こちらのテーブルから最初の5件を取得すると

Post.cursor(sort_at: :updated_at).pluck(:id) #=> [10, 9, 8, 7, 5]

となります。次にその次の5件を上と同じSQL取得すると、

SELECT posts.* FROM posts
WHERE posts.updated_at < '2018/12/13 14:08' ORDER BY posts.updated_at DESC LIMIT 5

という形ですが、直前に取得したデータの最後(id=5)のupdated_atが次のレコード(id=6)のupdated_atと同じ値の為id=6のレコードが取得できなくなってしまいます。

解決策

そこでJinraiではid以外の属性でソートするときは以下のようなSQLを発行します。

SELECT posts.* FROM posts
WHERE (posts.updated_at < '2018/12/13 14:08')
OR ((posts.updated_at = '2018/12/13 14:08') AND (posts.id > 6))
ORDER BY posts.updated_at DESC LIMIT 5

WHERE句でソートキーの値が重複する可能性を考慮して、同じだった場合より大きいidのデータを取得するようにしています。これを実装上でArelを使用してコードで以下のように表現しています。

if sort_at != primary_key
  condition_1 = arel_table[sort_at].send(rank, attributes[sort_at])
  condition_2 = arel_table.grouping(arel_table[sort_at].eq(attributes[sort_at])
    .and(arel_table[primary_key].send(rank_for_primary, id)))
  relation = where(condition_1.or(condition_2))
else
  relation = where(arel_table[primary_key].send(rank, id))
end

まとめ

以上のようにjinraiはページベースのものに比べて、全ページ数などが取得できないといったデメリットはありますが今回のようなケースでは過不足なくデータの受け渡しを行えます。
流量が多く最新からデータを取得したいようなケースに扱いやすい形でデータを提供できるような形で開発されています。

ソースは以下のGithubにあり弊社メンバー以外の方の開発参加も歓迎しています。
OSSの開発に関することはGitterにてやり取りされている為興味がある方は是非一度ルームで声をかけてください。
Github(studyplus/jinrai)
Gitter

Ruby biz Grand prix2018、大賞を受賞しました

こんにちは、CTO島田です。 2018年12月13日、Ruby biz Grand prix2018の表彰式が開催され、スタディプラスが大賞を受賞いたしました!
まさか大賞を頂けるとは思っていませんでしたので、とても嬉しいです。

f:id:kana_peko:20181213161821p:plain:w400 f:id:kana_peko:20181213161912p:plain:w400

今回、Ruby biz Grand prix応募にあたり、当社の取り組みやRubyの活用方法を応募用紙にしたためました。
この開発者ブログをご覧いただいている方に当社についてより詳しく知っていただきたく、コンフィデンシャルな部分を除いて部分的にこちらに公開します。

スタディプラスについてご興味をお持ちの方、また次回以降のRuby biz Grand prixへエントリーを検討されている方にとって、学びにしていただけますと幸いです。

募集要項(一部)

1. 応募団体の概要

(略)

2. 商品・サービス

(1) 商品・サービスの名称等

  • 商品・サービス名
    • Studyplus
  • 商品・サービスのキャッチフレーズ
    • 学ぶ喜びをすべての人へ

(2)商品・サービスの概要

  • サービス概要
    • Studyplusは、学習管理サービスです。
      勉強している教材のバーコードを、スマートフォンのカメラで読み取るだけで、勉強している教材としてスマホに登録し、勉強記録をつけることができます(オリジナルの教材も登録できるため、あらゆる学びを記録することができます)。
      私達は、学習の最大の課題は、学習それ自体ではなく、学習の継続であると考え、Studyplusを開発しました。学習はその効果をすぐには体感しづらいため、日々の学習をStudyplusで記録することで、日々成長していることを可視化したり、同じ目標に向かって学習している学習者をつなげることで切磋琢磨していることを感じやすいようにしています。
  • 用途
    • 大学受験、高校受験、資格試験、語学習得、読書記録など、あらゆる学習の記録にご利用できます。教育事業者にもサービスの提供を開始し、塾や学校の先生が、Studyplusを利用して、生徒の学習モチベーションを見守り、生徒のモチベーションが下がっている時に、声をかけたり、メッセージを送るような事例もあります。
  • 顧客ターゲット
    • 学習を継続したいユーザー
    • 現在のユーザーの内訳は、中学生が10%、高校生・浪人生が56%、大学生が15%、社会人が19%。
  • 顧客アプローチ方法
    • 2012年のサービス開始以来、広告は一切行わず、学習者同士の口コミで集客してまいりました。友達同士で競い、励まし合う、というコンセプトから、特に、中学生や高校生は使いやすいようです。アプリの評価がAppstoreでは★4.7、GooglePlayでは★4.6と非常に高く、アプリストアで知っていただくケースも多いです。

(3) 商品・サービスの安全性確保のため講じている方策

(略)

3. 事業の成長性と持続性

(1) 新規性・独創性・優位性のPR

①事業概要、新規性

Studyplusの新規性は、先生が「教える」ことではなく、生徒が「続ける」ことをIT化した点です。
教育のIT化、いわゆるEdtechは、対面の授業を映像などデジタルに置き換えるコンテンツを扱うサービスがほとんどでしたが、Studyplusは学習記録をつけるだけの、コンテンツを扱わない、まったく新しいサービスになっています。また、収益モデルについても、学習コンテンツのサービスは、学習コンテンツを購入してから学習をはじめるサービスがほとんどですが、Studyplusは、無料サービスで、広告収入で運営しており、生徒が勉強すればするほど、広告収益が増える、教育業界では他にない収益モデルになっています。

② 独創性・優位性

私達は、対面の授業を映像などデジタルに置き換えるコンテンツを扱うサービスが多い中、学習の最大の課題は、学習それ自体ではなく、学習の継続であると考え、例えば、節約の継続のために、支出を記録する家計簿アプリが必要であるように、学習の継続のために、学習を記録するアプリが必要だと考え、開発しました。

また、家計簿アプリは、その性質上、ユーザーが一人で利用するクローズドなサービスになりますが、Studyplusはユーザーが一人で利用するのではなく、学習記録を公開し、同じ目標を目指すユーザー同士が切磋琢磨する点が特徴的です。結果的に、ユーザー同士が切磋琢磨するネットワーク効果がユーザーに評価され、学習管理SNSとして競合はおらず、教育カテゴリのアプリとして圧倒的ナンバーワンのサービスになっています。また、収益モデルとしても、これまで学習意欲の高い生徒に広告を出稿したかった塾や大学が、日々勉強記録をつける学習意欲の高いStudyplusのユーザーに広告を出稿できることに価値を感じていただいており、塾や大学の生徒募集広告市場でもオンリーワンのサービスになっています。

(2) 市場性・成長性のPR

  • Studyplusの累計利用者数は2018年6月時点で350万人を突破しました。2016年10月にはユーザー数200万人、2017年12月には300万人突破と順調にユーザー数を伸ばしています。全都道府県に満遍なくユーザーが存在し、年齢層も幅広いです。アクティブユーザー数は年々増加を続けており、2018年8月のアクティブユーザー数は昨年比118%に増加しました。また、ユーザーの利用頻度が高いことが特徴であり、ユーザーあたり一日のセッション数(Studyplusを起動する回数)は平均14.2となっています。
    ユーザー数の成長を支えているのは、実際にアプリを利用したユーザーからの高い評価です。「このアプリを使い始めてから勉強に対する意識が変わりました」「みんなと共に頑張っている!と実感できるアプリです」といったポジティブなコメントを日々頂いています。
  • 顧客ニーズに応えるための創意工夫・事業検証・フィードバック
    • 地方出張ユーザーインタビュー
    • 目的:地域間の学習者の違いや地域格差の理解のため、出張ユーザーインタビューを推奨
    • 内容:Studyplus for School導入校にて、Studyplusのインストールから記録・教材登録までの導入サポート。

(3) 将来性のPR

(中略)

  • 学習管理機能に加えて、学習に関する記事コンテンツを読めるなど欲しい情報を受け取れるサービスへ進化
  • SNSとしての機能をより洗練させ、学習に関するさまざまな情報をシェアしやすいコミュニティとなり、学習する人にとって欠かせないサービスを目指す
  • 学習量に加えて習熟度を網羅した学習ビッグデータを活用した学習者へのレコメンデーション機能

4. Rubyの関わり方(優位性のPR)

アーキテクチャ図

f:id:kana_peko:20181213160638p:plain

(1) Rubyの採用理由
  • 「ビジネス上」「技術上」の背景とその理由(採用理由)
    • ネイティブアプリのサーバーサイドAPI開発をする際に、RubyやRailsの安定性や採用事例、情報の豊富さから一定以上のスピードと柔軟性を持った開発が実現できると感じたため。
  • 実際に使って実感したRubyの強み・メリット
    • 簡潔な実装と、高い生産性。
    • ライブラリ(gem)の豊富さによる汎用的な仕組みの再利用性の高さ。
(2) アーキテクチャの難易性等(加点要素)
  • 非同期処理ではSidekiqのenterprise版を利用して、大規模な処理を適切にスケールして実行する仕組みを導入。
  • 複数のマイクロサービスをRuny on Railsを利用して開発。共通化が必要な箇所に社内Gemを利用して実装の統一化を図っています。
(3)システム外での貢献(Rubyの普及啓発)
  • Ruby技術カンファレンスへの協賛
    • RubyKaigi 2018
    • RubyWorld Conference 2018
  • その他Rubyイベントへの協賛
    • 中高生国際Rubyプログラミングコンテスト2018
    • 第10回 Rails Girls Tokyo
  • Rubyコミッターに、設計・コードレビュー等の技術アドバイスを受けています

5. 事業の社会的な影響度(事業の実効性・インパクト)

NPO法人へのStudyplus for School無償提供、経済格差による教育格差の改善を目指しています。

当社は、2018年10月12日(木)より、教育事業者向け学習管理ツールStudyplus for Schoolを、教育格差をはじめとした社会課題解決に取り組む特定非営利活動法人(NPO法人)へ無償提供するプログラムを開始しました。
NPO法人は年々増加傾向にある一方で、経営資源に乏しく、安定した運営が難しいという実態があります。本プログラムは、NPO法人の慢性的な人材不足を、Studyplus for Schoolの活用によって解消し、社会貢献事業を加速させる一助になることを目的としています。
第一弾として、無料塾を運営する「NPO法人キッズドア」へStudyplus for Schoolを無償提供しています。 info.studyplus.co.jp

6. 独自の特色ある事項、アピールポイント

  • 中学生・高校生向け企業訪問プログラム
    • 目的:中学生・高校生の方に、気づきや学びを得る機会を提供するため、修学旅行や課外学習の一環として、企業訪問プログラムを実施しております。
    • 内容:会社紹介、オフィス見学・社員との交流、「アプリの企画」等のワークショップ 略
  • その他受賞歴
    • 日本e-Learning大賞2016年最優秀賞受賞
    • GooglePlay ベストアプリ受賞(2015/2016 年)
    • Appstore Essentials 選出

補足

受賞者プレゼンテーション

speakerdeck.com

まとめ

まつもとさんが最後の審査委員長講評で「賞は時の運もある」と仰っていましたが、まさにその通りだと感じています。
今回、大賞を受賞できたのはStudyplusというサービスの実績や可能性、Rubyとの関わり方などを総合的に評価していただいた結果であり、タイミングも良かったのだと思っています。

この結果をひとつの糧にして、今後とも教育領域とRubyコミュニティへ貢献をしていく所存です。

透明Activityの罠

初めまして、Studyplus開発部でAndroidアプリの開発をしております中島です。

今回のブログでは、我々 Studyplus Androidチームが踏んでしまった、深い深い落とし穴について少しお話しさせていただければと思います。

透明Activityとは?

ここで 透明Activity と言っているのは、以下の指定を持ったThemeを用いることで背景を透過させたActivityのことです。

(Android SDK内、theme.xmlより抜粋)

<item name="android:windowBackground">@android:color/transparent</item>
<item name="android:colorBackgroundCacheHint">@null</item>
<item name="android:windowIsTranslucent">true</item>

弊社アプリでは、例えば以下のような箇所で使用しています。

  • 初めて使う機能の際に、チュートリアル画像を出して簡単に説明する
  • ホーム画面を開いた際、ユーザーが設定したイベント(模試などが多いでしょうか)までの日にちをカウントダウン表示する

透明Activity の罠とは?

では、この透明Activityにどんな罠があったかということについて、順を追って説明いたします。

Overture<発端>

弊社アプリは、今年の10/10のアップデートをもってTargetSDKを 26 から 27 に更新いたしました。 これ自体は特に問題ありません、むしろ素晴らしいことであると思います。(Androidチームとしては28まで上げたかったのは確かですが、まぁ一歩ずつ)

手元のテストでも特に問題はなくリリースしたのですが、その日の夕方から悪夢が始まりました…

Stampede<殺到>

Fatal Exception: java.lang.RuntimeException: Unable to start activity ComponentInfo{~~Activity}: java.lang.IllegalStateException:
Only fullscreen opaque activities can request orientation

複数のActivityで上記のクラッシュレポートが殺到しました。一晩で 10k を超えるクラッシュが報告され、はっきり言って異常事態でした。

Investigate<調査>

正直この時点で精神を落ち着かせることなど不可能ですが、頭だけでも落ち着かせて調査を進めなくてはいけません… Crashlyticsの確認と、ソースコードの確認で以下のようなことがわかりました。

  • クラッシュは Android 8.0 のみで起こっていること
  • クラッシュしているActivityは全て 透明Activity であること

また、エラーメッセージで検索したところ StackOverFlowの投稿 が見つかりました。 これに加えAOSPのActivity.java内部を8.08.1で比較してみましたところ、8.0で以下の処理が追加され、8.1で削除されていることがわかりました。

Activity.java(8.0)

protected void onCreate(@Nullable Bundle savedInstanceState) {

    ~~~

    if (getApplicationInfo().targetSdkVersion > O && mActivityInfo.isFixedOrientation()) {
        final TypedArray ta = obtainStyledAttributes(com.android.internal.R.styleable.Window);
        final boolean isTranslucentOrFloating = ActivityInfo.isTranslucentOrFloating(ta);
        ta.recycle();

        if (isTranslucentOrFloating) {
            throw new IllegalStateException(
                    "Only fullscreen opaque activities can request orientation");
        }
    }

    ~~~

以下は、この変更がAOSPに追加された時(66fa94d42f30771c3e7b249756aed656d38aed08)のコミットメッセージです。

This changelist enforces that activities targeting O and beyond can only specify an orientation if they are fullscreen. The change ignores the orientation on the server side and throws an exception when the client has an orientation set in onCreate or invokes Activity#setRequestedOrientation.

O以上において、透明Activity などのfullscreenではないActivityにはOrientationの指定を行えないようにすることが目的として明言されています。 こうすることで、直前のActivityのOrientationに依存させることが目的だと思われます。

また、その後に Oには指定を許す といった旨のコミットメッセージと共に コミット(d1ac18c7c9eca1b07120be598dc6859b188baeb3)が追加され、上記のコードの形になったようです。

Allow for SDK 26 Activities to specify orientation when not fullscreen.

Conclusion<結果>

調査の結果、以下の条件を全て満たした場合に、 Activity#onCreateで必ず例外をスローしてクラッシュする ようになっていることが発見されました。

  • アプリの指定する TargetSDKが 27(Android 8.1) 以上
  • 動作端末 が Android 8.0 (SDK 26)
  • ActivityのThemeに <item name="android:windowIsTranslucent">true</item> または <item name="android:windowIsFloating">true</item> と指定している
  • Activityに android:screenOrientationportraitlandscape を指定している

なお弊社アプリは縦画面固定アプリのため、アプリ内の全Activityに対し android:screenOrientation="portrait" が指定がされています。 そのため、TargetSDKを27に上げたことが、罠の最後のトリガーだったのでした…

Fix<対応>

対応としては、Orientationの指定を消せば解決する問題ではあります。 しかし、メッセージでは8.0 以上に限定しているため、ただOrientation指定を消すのみでは 8.0 未満の端末に影響する可能性があります。 以上の理由から、該当Activityにおいて以下のような修正を行ないました。

  • AndroidManifestからOrientation指定を削除
  • 8.0 未満の場合のみ、 Activity#onCreate でOrientation指定を改めて追加
super.onCreate(savedInstanceState);

// super#onCreate の後に改めて Orientation 指定
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}

この対応により事態は収束しましたが、次に私を待っていたのは障害報告書の作成任務であったことは想像にかたくありません。

まとめ

今回は、TargetSDKを上げたことにより顕在化した不具合についてお話しさせていただきました。

ただ、今回の一件も、元はと言えばTargetSDKを最新に近づけようという意志の下に生まれたものと言えます。 StudyplusにはFail Forward(失敗から学ぶことに重点を置き、失敗そのものを批判することはしない)という標語があります。 我々Studyplus Androidチームはこの一件から学んだことを糧にして、今後もモダンなAndroid開発を目指していければと思っております。(Kotlin Coroutinesの導入も始まっています!) もちろん同じ轍は踏まぬよう、十二分に注意いたします。

Studyplusでの開発に興味がおありの方はぜひ ご応募ください

AAC Pagingライブラリ導入の話

お久しぶりです。Androidエンジニアの若宮(id:D_R_1009)です。 今日はAndroid Architecture Components Pagingライブラリ(AAC Paging)の導入例を紹介したいと思います。

アプリを利用してくださっている方はお気づきになられていると思いますが、11月19日よりアプリのデザインを一新しました。 デザインの変更に伴い、アプリ起動時の通信処理や各Fragmentの生成処理の見直しを行いました。安定性が増すことを目標に、見直しに取り組みました。

デザインのアップデートとほぼ同時ではあるのですが、タイムラインのパフォーマンス改善にも取り組みました。 今回は、パフォーマンス改善でAAC Pagingライブラリを導入した際のアレコレを書きたいと思います。

AAC Pagingライブラリとは

https://developer.android.com/topic/libraries/architecture/paging/

AACライブラリの一つとして2018年6月に正式リリースされた、ローカルDBやリモートサーバーから逐次データを読み込む処理をサポートしてくれるライブラリです。 データ読み込みの処理と表示するデータの保持を分けることで、RecyclerViewのスクロール処理がデータの読み込み処理によって遅くならない等の効果が期待できます。

個人的には recyclerview.extensions.ListAdapter がsupportライブラリに入った頃から気になっている存在です。 そのため「ようやく正式版になってくれたか」という気分ですね。

導入に当たって検討した事柄

旧タイムライン処理にはいくつかの課題がありました。一部を抜粋すると下記の通りです。

  • 追加読み込み処理が「最後から5番目」を表示したタイミングになるため、読み込み処理がUI処理に強く依存している
  • リストのデータがRecyclerView.Adapterで保持されているため、Fragmentの破棄時にキャッシュが削除されてしまう
  • ObservableListを利用しているため、通信取得処理のスレッドとUI更新時のスレッドをFragment内で明示的に切り替える必要がある

Pagingライブラリの導入によって次のように解決すると考えました。

  • 追加読み込みのタイミングがPagingライブラリ側でハンドリングされるため、読み込みの通信処理に専念できる
  • リストのデータをメモリやディスク上で保持できるため、Fragmentの破棄時に(不要な)キャッシュ削除が発生しない
  • LiveDataでリストの取得ができるため、UIスレッドとそのほかのスレッドをほぼ意識しないで済む(ViewModel + LiveDataの処理にまとめられる)

導入方法

前述の通り、Pagingライブラリを導入していた時期はアプリのデザインリニューアルの時期とかぶっています。 また導入事例や導入LTなどをあまり見つけることもできなかったため、いくつか安全策をとることとしました

旧タイムライン処理との平行運用

通信時のパースクラスから表示用のFragmentまで、旧タイムラインの処理と切り離して全て新規作成することとしました。 他の新規開発機能とのバッティングを抑え、ライブラリの導入に失敗した場合や想定以上の工数を使ってしまった場合に備えるためです。 結果としてタイムラインを置き換えることはできましたが、タイムラインの一部のみ置き換えることを想定しました。

良かった点

  • 旧タイムラインを置き換え可能な箇所から書き換えることができたため、レビューのコストが想定より低くなった
  • 一部のみの置き換えを念頭に置いて設計した結果、依存度の低いコード記述となった
  • 旧タイムラインは主にJavaで記述されていたが、新タイムラインはKotlinで記述することができ、data classを活用できた

悪かった点

  • JSONのパースクラスから作り直したところ、RecyclerViewのItemレイアウトが使い回せなくなってしまった
  • 旧タイムラインに追加された修正を新タイムラインに反映させていたが、反映漏れが発生した

その他に新しくなった箇所

旧タイムラインのItemをconstraint layoutに置き換えたところ、View間の関係性が漏れていた箇所が多数ありました。 Viewの縦横のサイズ計算に関わる問題となるため、ネストが浅くなるようリファクタリングを行なっています。

また、タイムラインのFragment間でRecyclerViewのViewPoolを共有し、Inflate回数を減らす対応を行いました。 こちらは2018年のDroidKaigiより広く使われている手法なので、弊社アプリもようやく追いついた感覚があります。

感想

導入のためサンプルコードをPCに落としてから、機能としてリリースするまで3週間ほどかかりました。 ほとんどPagingライブラリの導入にかかりきりだったので、長い時間をかけたような気がしています。

導入時に想定していたほど、Pagingライブラリの導入にはコストがかかりませんでした。 これは公式で非常にわかりやすいサンプルプロジェクトが用意されていたことが大きいです。まず公式サンプルに従ってAPIとDBの接続をするのが良さそうです。

逆に想定しきれていなかったのが、レイアウト周りの修正の多さです。 もともとButterKnifeをDataBindingに置き換えている途中だったこともあり、Kotlinとxmlの両方で書き換え処理が多くなりました。 変更に強いデータ構造を作らなければならない、という強い反省になったと感じています。

ひとこと

バリバリAACを使っていきたい! という気持ち溢れるエンジニアがいましたらこちらよりご連絡ください。