電子書籍をiPhoneで聴くようにしたら読書への苦手意識が和らいだ

*1

読書の秋ですね。

書籍を読む行為は、自分のペースで読み進めることができるという点が良い反面、ページ送りをするという行為が必須であるため、「ながら」ができないのがイマイチだなと感じることも多いです。

昨今ではaudible など、耳で本を聴くという体験も広がっているように思います。読み上げられた本を聴くならば、精読をしたいのか、拾い読みをしたいのかに関わらず一定のスピードで読み進めてくれ、ページ送りも不要なので「ながら」読みが可能になります。

しかし、そういったサービスでは別途お金がかかったり、お目当ての本がなかったりというので、少し残念だなと感じていました。


そこで本エントリでは最近僕が重宝している、iOS や iPadOS の読み上げの機能を紹介します。

各OSのアクセシビリティの機能として存在する画面上の文字の読み上げを流用し、Kindle や ブックアプリの電子書籍を機械に読み上げさせ、聴く読書に使おうというものです。

利用方法


準備は簡単です。新規にアプリをインストールする必要はありません。

設定アプリを開き「アクセシビリティ > 読み上げコンテンツ」と進み、以下の画像のようにします。 *2

 

 

  • 「画面の読み上げ」のトグルを有効にする
  • 「読み上げコントローラ」をオンにする

 

すると、画面の端に 「>」のようなマークが現れ、タップすると画像のようなコントロールキーが現れます。

 


その後、少し説明が難しいですが、以下の gif のように手のマークをタップしてから画面上からスワイプすることで、そのページの読み上げがスタートします。

 

 

実は紹介する以前からこの機能は存在していたのですが、僕の旧端末の iPhone SE2 ではスペックの問題なのか、読み上げが止まるなどあまり思うように動いてくれず、今回 iPhone 13 mini と 第3世代の iPad Proで動作が確認できたため、紹介しようと思いました。

 

使ってみて

 

僕は精読が苦手で、理解のために何度も同じ場所を読み返して進行が遅くなってしまい、結果飽きてせっかく買った書籍も途中で離脱してしまうことが多いのが悩みでした。

逆にスピードを上げてとりあえず浅くでも読み終えることを優先する方法を試そうとしたこともありましたが、なかなかよいバランスがわからず結局時間がかかってしまっていました。

 

この読み上げ機能を利用し始めてからは、「ながら」でもとりあえず本を最後まで流すことができ、この本は大体こういうことを言っている、というインデックスを貼ることが可能になりました。聴き流した後で気になる場合は後でもう一度読めばよく、その時は初めて精読するときよりもスムーズに読み進められている感覚がありました。

家事をしながら、日々の買い物をしながら、散歩・ランニングしながら聴くことができ、読書の量が以前より増えたように思います。

 

プログラムコードがたくさん存在するような書籍では利用できませんが、そういう書籍はむしろ写経するので、読み上げの対象にならない、という整理をしています。

 

一方でこの機能はあくまでアクセシビリティのためのものですので、聴く読書としての使い勝手がこなれているとはいえません。

具体的には以下の部分が微妙ポイントです。

 

  • スワイプの操作、再生の操作に多少の慣れがいる
  • 読み上げ中にKindle や ブックアプリを離れたり、画面がロックされるとページの境界で読み上げがストップする
  • 漢字などの読みが不正確な時がある*3
  • 埋め込まれているリンクも読み上げてしまうので、目次や章末の参考文献を聴くのが辛い
  • Kindle の固定レイアウトのものや、PDFの本は読み上げを利用することができない
  • 図表などは読み上げられない(これは読み上げ機能だけの問題ではなく、聴く読書の問題だと思います)


ということで、もちろん 本を聴くという体験としては audible などにお目当ての書籍がある場合はそちらを利用するほうが良いのだろうな、と思います。

 

まとめ

メリデメありますが、読書について僕と同様の課題がある方はぜひ試してみてください。

なんかうまくいかないよ、もっとこういう良い方法があるよ、とかがあれば是非コメントまたはTwitter にて教えてほしいです!

これで読んだ本の感想もブログに書いていきたいです。

*1:もちろん聴くのは電子書籍です

*2:僕は全ての端末をダークモードで利用しているので、背景色・文字色などはお手元の環境と異なっている可能性があります。

*3:漢字の読みは登録できたりします。が、なんとなく想像できる範囲なので、僕はあまりつかっていません

2021年の振り返り

2021年振り返りです。雑多に。

本業

タスクのデリゲーション

去年の振り返りブログにも書いたんですが、昨年マネージャー職になって今年の頭はプレイイングしていた業務のデリゲーションを求められていました。

去年からもペア・モブで作業したり、議事録やメモを多めに残したりしてましたが、なかなか情報を渡しきること、任せきることが難しかったです。

今年途中から僕のチームでモバイルアプリ開発が始まったことにより一気にデリゲーションが進んだように思います。

studist.tech

既存の画面をアプリストアからインストールできるようにする、というように要求が明確であったということと、僕自身が詳しくない領域であったことから、調査なども含めて任せる部分がわかりやすかったのが良かったかなと思いました。

一方でプロジェクトの進め方、スケジューリングについても一気に任せようとしてしまい、メンバーに負荷をかけすぎてしまったのは反省が必要なポイントでした。

プロダクトマネジメント

EMの傍らプロダクトマネジメントのようなことも引き続きやってました。

studist.tech

会社のブログでも言及しましたが、「可能な限り作りすぎない」というのを基本スタンスとして、各所からの機能要望について対処してきました。

不信感を持たれないように、やることになった開発プロジェクトのスケジュールについては意識していました。しかしそれでも「Noというのではなく、対案を出してほしい」と言われたことがありました。対案を出すと結局なにか作ってしまうことになるので避けたかった僕は、backlogを用意して個別のやりたくないことではなく全体像に話を向ける、ようにしました。時間がすぎると優先順位も変わって不要な機能となることも多く、チケットをクローズできると清々しい気持ちになるのでちょうどよい塩梅の対応だったかなと思います。

bizメンバーとよりガッツリ関わるようになった

仕事のデリゲーションができてきたことにより、事業のbizメンバーとの1on1を隔週で実施したり、biz定例に参加し始めました。僕は開発部で、事業部のメンバーとはレポートライン上にもいなかったり、定例では開発の自分がはいる必要がない話題も多いですが、これによって体感かなり話やすくなりしました。また、事業部のOKRを考えることにも積極的に参加でき、結果事業KPIの決定にも入っていきやすくなりました。KPIが定まっていけば開発要望についてのやるやらの議論もやりやすくなり、backlogに入れて塩漬け、というようなある種の姑息対応をせずにすむようになりました。

インプットが足りない

ぼくの振る舞いとして、自分の経験からそれっぽく話すことに対し、よいよねと言われることが何回かありました。具体的な話になるのでわかりやすくてよい、ということなのかなと思うんですが、それに甘んじてインプットが少なすぎたな、と感じることが増えてきました。会社の周りの方々は本などからインプットしたことを利用して考えを整理されているのがすごくうまいなと思ったのがきっかけでした。来年は技術についても、マネジメント系についてもインプットにあてる時間を意識的にとっていきたいと思っています。

副業

coralcap.co

今年は友人のhkdnetから紹介してもらった Fanfare さんで副業を行っていました。基本的にRuby on Railsのコードを書いています。 去年の振り返りブログでは、本業で得られない技術的興味を満たすことをモチベーションとして書いていました。 しかし、今は友人であり尊敬するエンジニアであるhkdnetの振る舞いを近くで見られる貴重な機会として、また、本業のサービスの少し前をいくVertical SaaS の CTOのかたの振る舞いを間近で見る機会として、利用できることにありがたみを感じています。

一方で、今年後半にかけて稼働が少なめでアウトプットも比較的すくなくなってしまっており、来年からは書斎も手に入るので気合を入れ直したい気持ちです。

プライベート

デスク周りを整えた

f:id:zuckey_17:20220101000321j:plain:w350

ゴールデンウィークFlexispotさん昇降デスクの脚とKANADEMONO さんの天板を購入し、デスク周りを整えました。コロナになってから自宅で作業することが多く座り続けているのもあまり良くないなと思っていたし、ミーティングが増えていたのでミーティング中は立つか、というので買いました。

リフレッシュできるし眠くなるお昼過ぎのミーティングでも集中できるので、良い買い物でした。

僕は有線のキーボードを使っているため、線を机上にだしたくなくてスライド式のキーボードトレイを購入しました。スタンディング状態ではちょうど良かったんですが、座ったときに使い心地が悪く、使わなくなってしまいました。

今年頭にブログに書いて使い始めたErgodoxは今でもガッツリ使っています。2ヶ月くらいで完全に慣れて、これまでの速度と同じ程度のタイピングができるようになりました。US配列にも同じタイミングで慣れたので、キーボードを劇的に変えても概ね2ヶ月位あればなんとかなるというのがわかってよかったです。

住み替え

f:id:zuckey_17:20220101000728j:plain:w350
今の住まいから近場の新宿中央公園によく散歩に行きました。懐かしむ日が来るんだろう。

今のマンションの部屋を購入してから2年半と経たないんですが、住み替えることになりました。結婚して土日に一人じゃなくなり、副業もガッツリするようになり、仕事をするための部屋が欲しくなったのです。

5月頃に具体的に検討を始め、まずは夢を膨らませようということで住宅展示場にも足を運びました。人生でそういうところに行くことになるとは思ってもなかったのでなかなか面白い体験ができました。

結局前職のサービスである cowcamo を利用させてもらってまた都内の中古マンションを購入することになったんですが、今回は今の住まいの売却の話も並行してすすめたのでかなり疲弊してしましました。詳しくはどこかでまた書こうと思っています。余裕があれば。

旅行

あんまりアクティブに出歩く方ではないんですが、泊まりの国内旅行に2回行きました。ご時世もあるので近場で、秩父と葉山に行きました。

秩父(2月)

f:id:zuckey_17:20220101000524j:plainf:id:zuckey_17:20220101000451j:plainf:id:zuckey_17:20220101000457j:plain

近場の温泉、という感じで、はなのやという温泉旅館に泊まりました。人生で初めて部屋に風呂がついている宿に泊まり、風呂に入っては寝、飯を食っては寝という1日目でした。

2日目に長瀞に行き、こたつ舟でライン下りをしようと思っていたんですが、ご時世と平日ということもありやってなかったのは残念でした。

けれども、宝登山神社の雰囲気がよかったのと、阿左美冷蔵のかき氷は絶品でした。

葉山(7月)

f:id:zuckey_17:20220101000630j:plainf:id:zuckey_17:20220101000613j:plain

近場のリゾート、という感じで、音羽の森というホテルに泊まりました。ベランダからの長めが良かったのと、インフィニティプールというものを初めて知りました。晩ごはんに食べた地元の野菜を使ったフランス料理がおいしかったのと、ベランダでの朝ごはんもご飯もかなり気持ちよかったです。

パン作り

f:id:zuckey_17:20220101001159j:plain:w350

オーブンレンジを買い数回パンを焼きました。僕はシナモンロールが大好きなんですが、かもめ食堂シナモンロールを作りたくてチャレンジしました。発酵などで時間はかかりますが意外と簡単で美味しいので何度か作りました。体重にはリティカルに影響するので抑えめにしたいところですが、在宅の趣味としてはおすすめです。

イクラ

前から何度からやっていたこともあったんですが、YouTube で何人かの実況者を知ることでハマりました。

ハヤシさんの熊本城

www.youtube.com

鶴太郎さんの巨大図書館

www.youtube.com

などに憧れを持ちつつ、家を作っては壊しを繰り返しています。

ドズル社さんとかを見てmodづくりとかも面白そうだなぁと思いながらなにかできないかなぁとか考えています。

しがないラジオ

あまり更新できませんでした。

実は2人回を8月に収録し、2人の半年ごとの振り返りをメインにしつつ、ゲスト回は声をかけられたら収録する感じにゆるく続けて行こうか、という話をしたんですが、

Soundcloud ではなくAnchorに移行し、費用も抑えようというところで、手作業での移行がめちゃくちゃ大変ですべての作業が止まっています。

フェードアウトになってしまうのも悲しいので、がんばって移行を完了して何かしらの宣言を投稿しようとは思います。これからの話しはそれからちゃんとアナウンスしたいですね。

おわり

Rails 6.0 ⇒ 6.1アップグレード時にActiveModel::Errorsの仕様変更にハマったメモ

つい先週 Rails 6.0系だったアプリケーションを 6.1にアップグレードした。

6.0系で rails new しているもので、テストカバレッジもそこそこあるので、アップグレード自体は半日もかからず終わったが、ActiveModel::Errorsの仕様の変更によって修正が必要な部分があり、その原因が分かりづらかったのでメモ。

ActiveModel::Errors の変更内容

Rails ガイドの記述: https://railsguides.jp/6_1_release_notes.html#active-model-主な変更

PR: https://github.com/rails/rails/pull/32313

例えば、以下のようなモデルクラスがあるとして:

class Dog
  include ActiveModel::Validations

  validates :name, presence: true
  validates :age, numericality: { only_integer: true }, allow_nil: true

  attr_reader :name, :age

  def initialize(name: nil, age: nil)
    @name = name
    @age = age
  end
end

Rails 6.0系では:

Loading development environment (Rails 6.0.3.4)

> dog = Dog.new(age: 3)
=> #<Dog:0x0000559792f03f50 @age=3, @name=nil>
> dog.valid?
=> false
> dog.errors
=> #<ActiveModel::Errors:0x000055978faa7338
 @base=
  #<Dog:0x0000559792f03f50
   @age=3,
   @errors=#<ActiveModel::Errors:0x000055978faa7338 ...>,
   @name=nil,
   @validation_context=nil>,
 @details={:name=>[{:error=>:blank}]},
 @messages={:name=>["を入力してください"]}>
> dog.errors[:name]
=> ["を入力してください"]
> dog.errors[:age]
=> []

のように、動く。

detailsmessages という属性に見える通り、ハッシュのようにしてエラーの内容を持っている。

各エラーにアクセスするためには、ActiveModel::Errorsインスタンスにたいしてハッシュの値を取得するようにしてアクセスする必要があった。

Rails 6.1系では:

Loading development environment (Rails 6.1.1)
> dog = Dog.new(age: 3)
=> #<Dog:0x00007f1745a30008 @age=3, @name=nil>
> dog.valid?
=> false
> dog.errors
=> #<ActiveModel::Errors:0x00007f17459f2c58
 @base=
  #<Dog:0x00007f1745a30008
   @age=3,
   @errors=#<ActiveModel::Errors:0x00007f17459f2c58 ...>,
   @name=nil,
   @validation_context=nil>,
 @errors=[#<ActiveModel::Error attribute=name, type=blank, options={}>]>
> dog.errors.where(:name)
=> [#<ActiveModel::Error attribute=name, type=blank, options={}>]
> dog.errors.where(:name).first.message
=> "を入力してください"
> dog.errors.where(:age)
=> []
> dog.errors.messages_for(:name)
=> ["を入力してください"]
> dog.errors.messages_for(:age)
=> []

各エラーは、ActiveModel::Errorオブジェクトとして表現され、ActiveModel::Errors は その Errorオブジェクトのリストとなった。

wheremessages_for を利用することによって、各エラーにアクセスすることができる。

もともとのハッシュのようなアクセスもインターフェースとしては6.1の段階では残っているが、PRによるとdeprecatedとなっているので、wheremessages_forなどにおきかえていくのが良いと思う。

変更が必要だった点

6.0系では、[]インターフェースで各エラーにアクセスしたときに、空のエラーが追加されていた。

例えば、

Loading development environment (Rails 6.0.3.4)

> dog = Dog.new(age: 3)
=> #<Dog:0x0000559792f03f50 @age=3, @name=nil>
> dog.valid?
=> false
> dog.errors
=> #<ActiveModel::Errors:0x000055978faa7338
 @base=
  #<Dog:0x0000559792f03f50
   @age=3,
   @errors=#<ActiveModel::Errors:0x000055978faa7338 ...>,
   @name=nil,
   @validation_context=nil>,
 @details={:name=>[{:error=>:blank}]},
 @messages={:name=>["を入力してください"]}>
> dog.errors[:name]
=> ["を入力してください"]
> dog.errors[:age]
=> []
> dog.errors
=> #<ActiveModel::Errors:0x0000561e22f62890
 @base=
  #<Dog:0x0000561e22fa7c88
   @age=3,
   @errors=#<ActiveModel::Errors:0x0000561e22f62890 ...>,
   @name=nil,
   @validation_context=nil>,
 @details={:name=>[{:error=>:blank}]},
 @messages={:name=>["を入力してください"], :age=>[]}>
> dog.errors.to_json
=> "{\"name\":[\"を入力してください\"],\"age\":[]}"

一度dog.errors[:age]にアクセスすると、ageにエラーはないにも関わらず、age keyが存在する形になる。

一方で、6.1系では[]でアクセスしても、error のリストや、errors.to_jsonの結果に変更はない。

Loading development environment (Rails 6.1.1)
> dog = Dog.new(age: 3)
=> #<Dog:0x00007f1745a30008 @age=3, @name=nil>
> dog.valid?
=> false
> dog.errors
=> #<ActiveModel::Errors:0x00007f17459f2c58
 @base=
  #<Dog:0x00007f1745a30008
   @age=3,
   @errors=#<ActiveModel::Errors:0x00007f17459f2c58 ...>,
   @name=nil,
   @validation_context=nil>,
 @errors=[#<ActiveModel::Error attribute=name, type=blank, options={}>]>
> dog.errors[:name]
=> ["を入力してください"]
> dog.errors[:age]
=> []
> dog.errors
=> #<ActiveModel::Errors:0x000055f4b47046e0
 @base=
  #<Dog:0x000055f4b474d570
   @age=3,
   @errors=#<ActiveModel::Errors:0x000055f4b47046e0 ...>,
   @name=nil,
   @validation_context=nil>,
 @errors=[#<ActiveModel::Error attribute=name, type=blank, options={}>]>
> dog.errors.to_json
=> "{\"name\":[\"を入力してください\"]}"

errorsJSONとして APIレスポンスとしてまるっと返してしまっていると、APIのインターフェースが変わってしまう。

開発しているアプリケーションでは、key にたいする空配列が作成される前提のコードになっていたため、変更が求められた。

※その他に、errors[:hoge]でアクセスしていたところをmessages_forwhereに修正するという必要もあったが、その部分では動きは変わらなかったのでたいした問題にはならなかった。

CSVのバリデーションとエラーレスポンス

ここから、僕が具体的に遭遇したケースの話を書く。

ユーザーがアップロードしたCSVを利用して、手で作るのは辛い大量のデータを作成する機能を実装するのはよくある。

そういう機能を実装するときに、フォーマットや各項目のバリデーションをするのに、AcriveModel::Validationsを利用していて、

class BaseCsv
  include ActiveModel::Validations

  attr_accessor :header, :table

  def initialize(csv_string)
    @table = ::CSV.new(csv_string, headers: true).read
    @header = ::CSV.parse(csv_string).first
  end

  #
    # 各種バリデーションロジック
    #

end

というような感じで、headertableに分けてバリデーションしている。

まずheaderで、各列ごとの要素や順番、列の数など、CSVの大まかな形を検証し、仕様通りのフォーマットでアップロードされたCSVかを判断し、 tableの方のロジックで、1つずつの値をバリデーションしていく。

すると、

{
    "header": [],
  "table": [
    "[2 行目] 他の メールアドレス と重複しています。",
    "[4 行目] 名前 は 20 文字以内で指定してください。",
    "[5 行目] 名前 は 191 文字以内で指定してください。",
    "[7 行目] 名前 が空です。入力してください。",
    "[8 行目] メールアドレスのフォーマットが不正です。",
    "[9 行目] 店舗名 が空です。入力してください。"
  ]
}

という形で、keyはちょっと分かりづらい感があるが、フォーマット と 各項目によってエラーを分けることができる。

フォーマットが誤っているときは、各項目が空になったり、要素がおかしかったりして、全行に渡って大量にメッセージが入ってしまうため、フォーマットをきれいに直してもらってから、中身の検証をしてあげよう、ということになっている。*1

名前,メールアドレスCSVがほしいときに、仮に名前列しかなかった場合、

{
    "header": [
        "CSV は 2 列で指定してください。",
        "2 列目は 名前 を指定してください。"
    ],
  "table": [
    "[2 行目] 名前 が空です。入力してください。",
      "[3 行目] 名前 が空です。入力してください。",
        "[4 行目] 名前 が空です。入力してください。",
        ........
        "[2000 行目] メールアドレス が空です。入力してください。"
  ]
}

みたいなことになりかねないので、フォーマットのエラーだけを先に見て、問題なければ中身を見る、ということにしている。

csv.errors[:header].present?

というのを聞けばよいだけだが、こうすると、6.0ではheader keyerrorsに追加され、6.1では追加されなかった。

で、request spec には、header keyに空配列が入っていることを検証しようとしているのに、バージョンアップすればなくなる、といった問題だった。

結果、このプライベートAPIを利用しているフロントの実装は問題なかったので、そのままにし、RSpec を拡充してこの問題は終了した。

おわり

  • ActiveModel::Errors[]のインターフェースでアクセスしたときの挙動が変わっていることに気づくのに少し時間がかかった
  • Rails 6.1移行では、messages_forwhereなどを使ってActiveModel::Error オブジェクトにアクセスしましょう

*1:CSVテンプレートをダウンロードさせてあげることでフォーマットの誤り自体はある程度防ぐことができるとはおもう

【2021年度版】もう一度ErgoDox EZを設定する

2018年にErgoDox EZを購入して、利用開始までの記事を書きました。

blog.zuckey17.org

この記事は長く細く流入があってErgoDoxに関心がある人がある一定いるんだなあということが うかがえるんですが、実はこの記事を書いて1週間後には僕自身ErogoDoxを使っていませんでした。 会社に持っていっていたのですが、家のキーボードが Magic Keyboardだと、結局ErgoDoxに慣れることができず、タイピング速度の遅さのせいで業務がおろそかになりがちでした。

2020年のコロナ禍でWFH*1が続き、これは再度導入する良い機会ではないかと思いたって、年末年始に環境を整えてみました。

2018年の当時と一部設定方法が異なったので、本エントリではその差分を埋めたいと思います。

ただし、現在の最適な購入方法や新製品の情報については詳しくないので、公式サイトなどをあたっていただけると嬉しいです。

*2

ergodox-ez.com

レイアウトを決定

設定ファイルを作成する方法は大まかには前回の記事とかわりません。(参考: キーマップを決定

レイアウト編集Webアプリケーションにアクセス

以下のリンクにアクセスすると、デフォルトで用意されているレイアウトが確認できます。

https://configure.ergodox-ez.com/ergodox-ez/layouts/default/latest/0

f:id:zuckey_17:20210110233931p:plain:w300

そして、自分用にレイアウトを変更するため Modify layoutボタンを押します。

レイアウトを編集、コンパイル

レイアウトを変更し、レイアウトに名前つけ、コンパイルボタンを押します。(名前をつけないとコンパイルボタンが活性化しません。)

f:id:zuckey_17:20210110234235p:plain:w300

※レイヤーなどの概念の理解については、英語になりますが以下の動画が参考になると思います。

www.youtube.com

レイアウトファイルをダウンロード

コンパイルされたレイアウトファイルをダウンロードします。 この状態では固有のURLが振られているので、次から一部を変更するときのためにブックマークするなどしてとっておくと便利です。

f:id:zuckey_17:20210110234527p:plain:w300

レイアウトをキーボードに書き込み

ここが前回から最も変わっている部分だと思います。

前回は、Teensy Loader) というマイコンにコードを書き込むアプリケーションを利用しましたが、今は、ErgoDoxに書き込むためのアプリケーションが用意されていました。

ergodox-ez.com

僕はMac OS 用のインストーラをダウンロードしました。

Teensy Loader は少し分かりづらいインターフェースでしたが、今回は非常にわかりやすいと思います。

起動すると以下のような画面になります。 下の方につながっているキーボードの種類が書かれていれば、正しく認識されています。

f:id:zuckey_17:20210111001349p:plain:w300

Select Fileから、先程ダウンロードした.hex拡張子のレイアウトファイルを選択します。

f:id:zuckey_17:20210111001618p:plain:w300

指示のとおりに、右手側のキーボードの小さな穴の中のリセットボタンを押します。(僕は安全ピンでやりました。) しっかり押せると、ロードされたようなアニメーションの後に以下の用に書き込みが成功された画面になります。

f:id:zuckey_17:20210111001638p:plain:w300

こちらで設定は以上です。自分に最適なレイアウトを探求しましょう。

【おまけ】左右の間のケーブルを交換

購入時についてくるデフォルトのケーブルは少し長く、弛んでしまってあまり見た目がよくありませんでした。 かといって結束バンドなどで縛るには少し短い感じもありました。

その対応のため、Amazonで↓のようなケーブルを買ってスッキリとさせることができました。

www.amazon.co.jp

Before

f:id:zuckey_17:20210110235751p:plain:w400

After

f:id:zuckey_17:20210111000203p:plain:w400

終わり

値段も値段ですし、今回は使いこなしたいですね。(今回は2週間は続いています。)

以上です。参考になれば幸いです。

*1:Work From Home

*2:ちらっと眺めてみましたが、次世代機として「MOONLANDER」 というのが出ているんですね。機能性はさておき、見た目だけで言えばErogDox EZのほうがかっこいいと思ってしまいました。

Rails アプリケーションのER図(SchemaSpy)をCircleCIで定期出力する

業務にてRuby on Railsの Webアプリケーション開発していて、DB設計資料やER図を必要に応じて手で作って説明に利用したりしていました。 システムの運用も始まり、そろそろ手で作ることによる更新が厳しくなってきた感じがするので、自動生成できないかなあと思っていました。

今回は、CIプロセス(CircleCI)の中でSchemaSpyというツールを用いて、ドキュメントを自動生成してみたので、学びを書き残しておきます。

f:id:zuckey_17:20210107070346p:plain:w300

SchemaSpy

schemaspy.org

SchemaSpyというものを見つけました。

テーブル、カラムの構成情報やER図といったドキュメントをHTML/JS/CSSとして静的に出力してくれるツールです。実際にサーバー上で動いているDBを指定するとドキュメントを生成してくれます。

Javaで作られていているツールですが、公式でDockerコンテナイメージを公開してくれているので、簡単にCircleCIなどで実行できます。

手元で動かす

まずは、Rails アプリケーションのバックエンドとなっている MySQLが立ち上がっている状態で、そのDBに対して SchemaSpyの作成コマンドを実行してみました。

docker コマンドが動く環境で、以下のようにたたきます。

(手元の環境は、mac OS 10.15.7 で、MySQL 5.7でした)

$ docker run -v "$PWD/schema:/output" --net="host" schemaspy/schemaspy:snapshot -debug -t mysql -host 127.0.0.1:3306 -db 【DB名】 -u 【ユーザー名】 -p 【パスワード】 -connprops useSSL\\=false -s 【DB名】

すると実行したディレクトリ直下に schemaディレクトリが作成され、その中の index.html をブラウザで開くと作成されたドキュメントを確認することができます。

簡単ですね。

最新のドキュメントをいつでも見たい

ここまで来ると、後はDB構成に変更があってもいつでも最新のドキュメントを用意しておきたい気持ちになります。

最終的には冒頭でも書いたとおり、CircleCIで実行発行してホストすることになりましたが、Railsアプリケーションでやろうとするといくつか問題もありました。その紆余曲折含めて最終的にどう実現したかを紹介します。

コマンドを実行する対象のDBを準備する必要がある

ドキュメント作成時、実際にサーバー上で動いているDBを指定する必要があります。

最新のドキュメントをいつでも見られるようにするための、選択肢としては以下の3つくらいを上げていました。

  • A案)CI環境でドキュメント作成用のDBを一時的に立ち上げる
  • B案)ステージング環境などのDBに定期的にコマンドを実行する
  • C案)スキーマに変更があったとき毎回ローカルのDBに対してコマンドを叩く

C案は自動化とはいえなさそうなので却下して、A案かB案になります。
わざわざこのために実行環境を用意するのも嫌だなという気持ちになって、最終的にA案に落ち着きました。

SchemaSpyでは、稼働しているDBに対して実行することでテーブルごとの行数も見れますが、本番の情報ではない限りその情報はあまり意味もないので、すべてのテーブルが0行になっていてもドキュメントとして致命的な問題ではないということにしました。

f:id:zuckey_17:20210107071854p:plain

今回はCircleCI上で実現しようということで、最終的なコードとしては、以下のようなjobの定義になりました。

(省略)

build-erd:
    docker:
      - image: schemaspy/schemaspy:snapshot
        user: root
      - image: mysql:5.7.12
            <<: *dockerhub_auth
            environment:
          MYSQL_DATABASE: xxxx_test
          MYSQL_USER: root
          MYSQL_ROOT_PASSWORD: password
    working_directory: ~/repo
    steps:
      - checkout
      - run: apk add openssl mariadb-client
            - run: sleep 5
      - run: cat ./db/structure.sql | mysql -h 127.0.0.1 -P 3306 -u root -ppassword xxxx_test
      - run: cd / && schemaspy -t mysql -host 127.0.0.1 -port 3306 -u root -p password -db xxxx_test -s xxxx_test -hq -connprops useSSL\\=false
      - store_artifacts:
          path: /output

(省略)

MySQLのコンテナを傍らで動かしつつ、SchemaSpy のコンテナの中でいくつかの処理を実行することになります。

Railsスキーマフォーマットを変更する

SchemaSpyのコンテナからMySQLコンテナに対して、何らかの方法でSQLを実行し、DBを準備する必要があります。

SchemaSpyを利用されている例をいくつか調べてみると、DDLの形式のSQLファイルを手元でdumpしてリポジトリに含めておく、という運用をされているのが観測できました。
ローカルのDBをもとにダンプしてしまうのは、結局手で運用する部分がのこったり、開発者の考えることが増えると考えました。

Railsのデフォルトだと、db/schema.rb というスキーマファイルを bin/rails db:setup(の中のbin/rails db:schem:load)で読み込んで反映することになります。しかし、これをそのままやろうとすると、SchemaSpyのコンテナにRubyの環境を構築する必要があり、すこしつらそうという印象を持ちました。

他の方法を検討すると、Rails には db/structure.sql という別のスキーマフォーマットがあることに気づきました。

Railsには、config.active_record.schema_formatという設定があり、:sql:rubyを選択でき、それによって、structure.sqlschema.rbの違いが出てきます。

デフォルトは:rubyですが、config/application.rbに以下のように書くことによって、設定を変更することができます。

class Application < Rails::Application
  config.active_record.schema_format = :sql
end

ドキュメント自動生成のためにスキーマフォーマットを変えるのはどうかな、とも思いましたが、調べてみると、デメリットとしては手元でのスキーマファイルのコンフリクトが少し難しくなる程度でした。 structure.sqlでは、むしろメリットとして schema.rbでは表現しきれないDBの機能まで表現できるということで、特にこの変更は問題ないだろう、というふうに考えました。(実際そういった高度なDBの機能は使っていませんが。) *1

Active Record マイグレーション - Railsガイド
Pros and Cons of Using structure.sql in Your Ruby on Rails Application | AppSignal Blog

これによって上記のようなjobの設定で、CircleCI上にSchemaSpyで生成したドキュメントをホストすることができました。

その他

sleep 5 ?

job の設定で、 sleep 5という謎のコマンド実行があります。

f:id:zuckey_17:20210107063806p:plain

上記のようなエラーが出てしまい、MySQLが立ち上がりきっていない段階でコマンドを実行してしまっているのかなと考えて、足しています。

https://circleci.com/docs/ja/2.0/executor-types/

CircleCI公式のドキュメントでもmongoの立ち上がりを待っているような記述もあって、それに習ってこの対応をしていますが、5秒以上かかったらどうするんだ、という気持ちがあるので、あるべきやり方をご存知のかたいらっしゃったら教えてほしいです。

そんなに頻繁にスキーマ変更するの?

pushごとに毎回ドキュメントを生成するのかという話があると思います。確かにそんなに頻繁にスキーマの変更は起こらないので、追加で db/structure.sqlの変更差分があるときのみドキュメント生成を実行するということにしました。

CircleCIにはスケジュール実行の機能があったりするので、そちらを利用しても良いかもしれません。

終わり

これで、Rails の DB関連のドキュメントを自動メンテナンスする基盤が整いました。参考になると嬉しいです。

今のまま、CircleCI上にホストすると、せっかくのドキュメントの導線が埋もれてしまうので、ドキュメントを生成したタイミングでSlackなどにリンクを通知したり、S3などに上げてURL固定で最新のドキュメントを参照できるようにしたりなどもう少し改良の余地があるかなと思っています。

参考

https://engineering.mercari.com/blog/entry/2018-05-25-133818/

*1:6.1系のRails までは、bin/rails db:structure:dump というコマンドを実行することにより、スキーマフォーマットの設定を変えることなく structure.sqlを取得することができますが、6.2よりこの機能は削除され、設定フォーマットのスキーマファイルのみ取得できる、ということになっています。

【Golang】 Airを用いてWebアプリケーション開発中に変更を即座に反映する

github.com

Golang で Webサイトを作ろうと gin を触っていると、ライブリロードの機能がなく、変更が即座に反映されなくて、面倒くさく思っていました。 Air というツールがよく使われていそうだったので、使ってみました。

このエントリはそのときのメモです。

gin アプリケーションの準備

まず、Air と関係のない部分で最小のアプリケーションを用意します。

Golang バージョンは以下の通りで、環境は macOS Catalina (10.15.7) です。

$ go version
go version go1.15.5 darwin/amd64

目指すフォルダ構成は以下です。

.
├── go.mod
├── go.sum
└── main.go

フォルダを作成し、go modules で環境を構築します。

$ mkdir gin-air-example && cd $_
$ go mod init gin-air-example
go: creating new go.mod: module gin-air-example
$ touch main.go

main.go の内容は以下とします。

package main

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "OK",
        })
    })

    r.Run(":3000")
}

localhost:3000 にアクセスしたら、 {"message": "OK"}というJSONを返します。

$ go run main.go
go: finding module for package github.com/gin-gonic/gin
go: found github.com/gin-gonic/gin in github.com/gin-gonic/gin v1.6.3
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
 - using env:   export GIN_MODE=release
 - using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
[GIN-debug] Listening and serving HTTP on :3000
$ curl localhost:3000
{"message":"OK"}

f:id:zuckey_17:20210104004434p:plain

このように、デバッグのログにもアクセスが来たことが表示されます。

main.go を編集する

ここで、main.goを以下のように変更します。

// (省略)

func main() {
    r := gin.Default()
    r.GET("/", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
-          "message": "OK",
+           "message": "Hello World",
        })
    })

    r.Run(":3000")
}

main.go を保存して、再度 localhost:3000にアクセスしても結果は変わりません。

この際、go run main.goを一旦止め、再度同じコマンドを実行すると出力結果は変わります。

$ curl localhost:3000
{"message":"Hello World"}

しかし、普段開発していて毎回 コマンドを叩き直すのはやめたいです。ここで Air というツールを導入します。

Air を導入する

Air を installします。

$ go get -u github.com/cosmtrek/air

.air.toml ファイルを作成して、設定ファイルを公式の例)からコピーし、ペーストします。

# Config file for [Air](https://github.com/cosmtrek/air) in TOML format

# Working directory
# . or absolute path, please note that the directories following must be under root.
root = "."
tmp_dir = "tmp"

[build]
# Just plain old shell command. You could use `make` as well.
cmd = "go build -o ./tmp/main ."
# Binary file yields from `cmd`.
bin = "tmp/main"
# Customize binary.
full_bin = "APP_ENV=dev APP_USER=air ./tmp/main"
# Watch these filename extensions.
include_ext = ["go", "tpl", "tmpl", "html"]
# Ignore these filename extensions or directories.
exclude_dir = ["assets", "tmp", "vendor", "frontend/node_modules"]
# Watch these directories if you specified.
include_dir = []
# Exclude files.
exclude_file = []
# Exclude unchanged files.
exclude_unchanged = true
# This log file places in your tmp_dir.
log = "air.log"
# It's not necessary to trigger build each time file changes if it's too frequent.
delay = 1000 # ms
# Stop running old binary when build errors occur.
stop_on_error = true
# Send Interrupt signal before killing process (windows does not support this feature)
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms

[log]
# Show log time
time = false

[color]
# Customize each part's color. If no color found, use the raw app log.
main = "magenta"
watcher = "cyan"
build = "yellow"
runner = "green"

[misc]
# Delete tmp directory on exit
clean_on_exit = true

そして、gin のプロセスを止め、airコマンドからWebアプリケーションを起動します。

$ air -c .air.toml

すると、main.go への 変更が即座に反映されるようになったのがわかるかと思います。

これで Air が導入できました。

gin アプリケーションの構築以外でやったことは、以下のみなので、簡単です。

  1. Abr の インストール
  2. 設定ファイルの追加

必要に応じて、.air.toml を修正して利用します。

最終的にファイル構成はこうなっています。

.
├── .air.toml
├── go.mod
├── go.sum
└── main.go

Docker で利用

Docker で利用する場合のメモも載せておきます。

Dockerfile を作成

FROM golang:latest

COPY ./ /go/app

WORKDIR /go/app/

RUN go get -u github.com/gin-gonic/gin
RUN go get -u github.com/cosmtrek/air

CMD ["air", "-c", ".air.toml"]**

docker-compose.yml を作成

version: '3'
services:
  app:
    container_name: app
    build:
      context: ./
      dockerfile: ./Dockerfile
    ports:
      - 3000:3000
    tty:
      true
    volumes:
      - ./:/go/app

アプリケーションを立ち上げます。

$ docker-compose up —build 
Building app
Step 1/6 : FROM golang:latest

# 省略

Successfully built 6e9649de9e6b
Successfully tagged gin-air-example_app:latest
Recreating app ... done
Attaching to app
app    | 
app    |   __    _   ___  
app    |  / /\  | | | |_) 
app    | /_/--\ |_| |_| \_ v1.12.1 // live reload for Go apps, with Go1.14.0
app    | 
app    | mkdir /go/app/tmp
app    | watching .
app    | !exclude tmp
app    | building...
app    | running...
app    | [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
app    | 
app    | [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
app    |  - using env:  export GIN_MODE=release
app    |  - using code: gin.SetMode(gin.ReleaseMode)
app    | 
app    | [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
app    | [GIN-debug] Listening and serving HTTP on :3000

2回目移行は --build オプションは不要です。

これで同様にmain.goの変更が即座に反映されるようになります。

最終のファイル構成はこちらです。

.
├── .air.toml
├── Dockerfile
├── docker-compose.yml
├── go.mod
├── go.sum
└── main.go

終わりです。

2020年の振り返り

2020年初めてのブログです。 備忘のため。

f:id:zuckey_17:20201231234023j:plain

スタディストに転職し、EMになった

1月にスタディストという会社に転職しました。

エンジニアになってからお世話になっている人もいらっしゃったり、新規事業のリードエンジニアに、というお話をいただいたりしてそこに興味を持ったので決めました。 実際ガッツリ0→1のフェーズを任せていただけたので、慌ただしく楽しい1年になりました。
オンボーディングが終わった2月から構想段階にガッツリ参加し、11月に正式リリースにこぎつけました。

新規事業のPoC

新規事業を始めるにあたって、PoC(Proof of Concept)というのをやりました。
一言にPoCといってもやり方はいろいろあると思うんですが、僕らは実際にちゃんと動くプロトタイプをガッツリ作って未来のお客様に使っていただきフィードバックをもらうというプロセスを選択しました。
実際、Google SpreadSheet をバックエンドにしたものから、それを全部壊して次に firebase で仕様を変えて作り直したりしました。
to Bのサービスなので未来のお客様に実際に使ってもらって、そのお客様の課題が用意した機能によって本当に解消されるのか、というのを本番で作り込む前に検証できるというのは、自分たちの方向性に自身も持てる良いプロセスだったと思いました。

一方で難しいこともありました。
プロトタイプを実際に業務の中でお客様に使ってもらっているため、しっかり本番バージョンへの移行までをやりきる必要があります。 そのため、PoC段階で出た、いろいろなアイデアを取捨選択して、宣言したタイミングにリリースを仕切ることが求められました。
その際に、データを移行しないということや、プロトタイプにはあるこの機能は本番の初期リリースでは実装されないということなど、biz やお客様としっかり握っておく必要がありました。
to Bだと、商談の期間が長くなることも多いです。 そのため、初回の商談ではプロトタイプについての案内をしていたにも関わらず、途中から本番バージョンの話をしているということもよくあり、お客様の目線ではなにの話をしていて、自分たちはどういう準備をしないといけないのかと言うのを掴みづらくなります。
こういった混乱を防ぐため、まだ作っている最中の リリースしていない段階から、営業には仕様や案内時の注意点を事前に説明しておく必要がありました。

振り返って書いていると、to Bの開発ではよくあることな気もしますが、それにリードとして関われたことによる視野の広がりを感じました。

EMになった

上記のリード部分での他チームとのやり取りや、新規事業のエンジニアの採用についても携わっていたこと、会社の組織的な変更タイミングなど、さまざまな要素があって、入社後初の評価のタイミングでエンジニアリングマネージャーになることになりました。 もともと入社のタイミングからふんわりと話はしていたものの、新規プロダクトの立ち上げでガッツリコードを書いたり、設計したりと、プレイヤー業務が多そうだったでタイミングを見ていました。

現状でいうと、とりあえず自分がやっていた仕事をデリゲーションして、マネージャーがやるべきことをする時間を作り出すということを目標としています。
そのために自分のみに集まりがちな情報を適切にメンバーにも見えるようにして、情報の非対称性をなくしたり、手元でいうとモブプログラミングやモブ設計などを取り入れ暗黙知のチーム内での共有をしたりというアクションを取っています。

まだまだ手探りでやっている部分が多いので、なれてきて身についたことがあれば、何かしらアウトプットをしていきたいなと思ってはいます。

副業をしっかりめにやってみた

本業以外でいうと、年始あたりは個人プロジェクトでいくつかサービスを作ったりしていましたが、夏前くらいに副業をもう一度ちゃんとやってみたいなと思い立って、今年後半は2つの副業に携わりました。
これまで副業の経験は、学生時代の友人の会社を少し手伝ったりしたくらいでしっかりとやっていなかったなという感覚がありました。

マイルストーンベースのプロジェクトでレガシーRoRに機能を追加した

1つめの副業として、クラウドワークスという大手クラウドソーシングサービスを利用し、自分で探して仕事を受けてみました。 Ruby on Rails ベースの開発が自信を持てるのでそれで探すとちょうど 1つ良さそうに見えた募集があり、応募してみるとサクッと決まって、思い立ってから 3日で副業が決まってそれは少しびっくりしました。
その案件は、ざっくりいうと、「動いているRoRサービスに機能を追加したいが、テストもないしすべての実装を把握している人もいないためすぐにはできない。まずは必要な部分のテストを追加して、直近ほしい機能を追加してほしい。」という趣旨でした。

  1. テストを追加
  2. 機能を改修、細やかな修正
  3. ほしかった機能を追加

という3つのマイルストーンをおき、そのマイルストーンごとに決まった金額をいただくという形での契約になりました。

技術的な話でいうと、比較的レガシーな、自分以外が書いたコードを理解してテストを書いたり、使ったこともないライブラリやあつかったこともないドメインについて頭に入れて影響範囲を少なくして安心してリリースできるようにすすめたりという部分が勉強になりました。
特に、本業で0→1のプロジェクトをやっていて自分のなかにある知識でほぼすべての機能を実装していたため、そういう難しさを求めていたタイミングでもあり、ちょうど良かったなという気がします。

副業とモチベーション

2つめの副業は11月くらいから友人の紹介で関わり始めたものなので、あまり書くことはないですが、こちらはフロントエンドの細かな修正がおおく、普段の僕の業務ではあまりやらない部分の実装のため、勉強になるなと思いながら取り組んでいます。
副業は自分のcanを残しつつ、少しだけコンフォートゾーンをはずれた部分の業務をすることで、学習サイクルのドライバーにもなるため、続けていきたいなと思いました。

一方で、振り返ってみると、金銭的なことについて今後もう少しちゃんと考えていかないといけない、という課題が残りました。
1つめの副業は、マイルストーンごとに決まった金額をいただくということだったということもあり、難しい機能の実装に時間がかかってしまうと時給換算するといただける額が少なくなってしまいました。
また、2つめの副業についても当初1つめの副業でかかったベースの時給を提示してしまったため、頑張ったなというわりに額が伴わないんだなというふうになってきています。 副業のモチベーションは、本業では得られない技術的興味を満たせるということで一定保てる反面、頑張る気持ちになるための一定の報酬をちゃんと設定する必要性を感じました。

しがないラジオをちょっとお休みしていた

僕はしがないラジオというPodcastのパーソナリティおよび編集をかれこれ4年弱ほどやっていますが、今年はこれまでと比べて活動を緩やかにしていました。
夏前の結婚により生活リズムがかわった、コロナ禍でゲストと対面で収録できなかった、副業が思ったよりタイトなスケジューリングになってしまった、などほかにもいくつかあります。

これによって、しがないラジオを起因として細くも続いていたアウトプットが一切なくなってしましました。 アウトプットによってフィードバックをいただけたりすることが、楽しみの一部であり、成長の要因だったなとおもうので、来年はしがないラジオに限らず、アウトプットを増やしていけたらよいなとおもっています。

具体的には、2019年に執筆した同人誌『チーム開発一年目の教科書』を追記修正して、第2版としたいなと言う気持ちがあります。
副業の序盤で、調査のドキュメントをしっかり書いたり、コミットの分割やPRの説明などをしっかり書いたりすることにより、スムーズに受け入れてもらえたという手応えがありました。
この同人誌の内容はやはり価値がありそうだな、ということをもう一度実感し、一方で読み返してみるともっとよくできる部分が沢山あるなと思ったためです。

ほかにも、まずはカジュアルにまずはアウトプットする習慣を戻していきたいなと思っています。

来年はもう少ししがないラジオの頻度も上げて行きたいなと思っているので、もし興味あるかたいらっしゃいましたら、初めての方でも、これまですでにでていただいた方でも、ぜひ Twitter DMハッシュタグ #しがないラジオにてお声がけください。

おわり

良いお年を!