僕が代表をしている株式会社KOSKAでは製造業の原価管理をIoTで自動化するGenkanというサービスを提供しております。
そんな弊社では半年前、バックエンドをRoRからScalaに移行したのですが、これが素晴らしく効果が高かったので以下の記事を書きました。
スタートアップである弊社が全員ほぼ未経験でRoRをScalaに移行した理由、その効果と苦労点
しかし、最後に書いたのですが、苦労する点もとても多いです。
弊社CPOが苦労する点を抽象的な部分に関しては以下の記事で書いてくれてはいます。
0からScalaを本番導入して感じたこと・考えたこと - Qiita
ただ、実際にコードを書き始めた時に引っかかりやすい点をできるだけ詳しくあげておくことで、導入しようと考えた人がなるべく簡単に導入できるという状況を作りたかったので、書きました。
それではスタートです。
RubyやPHP、Pythonなどの言語で開発を行なっているスタートアップがScalaを採用してから半年間でおそらくかなり引っかかってしまうであろう点と、それをどう解決したかを書いておく。
今からScalaを採用しようとする人には有用だと思うので、参考にどうぞ。
目次
Play Frameworkを把握する
sbtを把握する。
マイグレーションシステム(evolution)を把握する
Slick(ORM的なやつ)を把握する
DDDを把握する
認証ライブラリを把握する
モナドや関数型を把握する
モナド変換を把握する
デプロイする
Play Frameworkを把握する
Scalaを始めようと思った時、よく使われているフレームワークが何かとみてみるとフレームワークではなくAkka HTTPというHTTPモジュールが出てくる。採用率は1位なのだが、これはルーティング等だけ存在する最低限のミニマムフレームワークで、Ruby on Rails等に慣れてきてしまった人がこれを採用するのはかなり抵抗がある。
そういう人たちにはPlay Framework1択であるのだが、Playは現在バージョン2.8であるのにも関わらず公式ドキュメントが日本語化されているのが2.4程度であり、日本語の方が使い物にならない。
あと公式のドキュメントのチュートリアルが最低限すぎてそのあと何探せばいいかがわからない。
というわけで何を見ればいいのかというと
https://www.playframework.com/documentation/2.8.x/ScalaHome
と
https://www.playframework.com/documentation/2.8.x/ScalaAdvanced
を上から下までみるといい。
トピック別に分かれているので全部読むのは少し大変だが、ここに書かれているものはほぼ全てPlay使っていれば使う知識であるため、読んでおいたほうがいい。
とりあえず使いたい人は
HTTP programming
Working with Json
Accessing an SQL database
Dependency Injection
Testing your application
でも読んどけば最低限のJSONサーバーくらいはできると思う。
特にPlayはDIシステムが優秀なので、PHP&Laravel開発などでDI使ったことあるって人でないならDependency Injectionの章はちゃんと読まないと多分使えない。
sbtを把握する
これは日本語で素晴らしいドキュメントがある。
少しタスク定義、依存関係定義に癖があるので、3日くらいは把握にかかるかも
sbt Reference Manual — sbt Reference Manual
一番よくわからなくなるのがタスク定義の仕方と、コマンド系ファイルの作り方なきがするのでそこだけ軽く説明する。
例えばシードをコマンドとして行いたいとする。
その場合、まずapp/seedというディレクトリを作ってそこにSeedというパッケージオブジェクトを作る。
そこのメイン関数に処理を色々書いておき、build.sbtに以下を追加する。
lazy val `genkanserver` = (project in file(".")).enablePlugins(PlayScala)//これはデフォルトで存在する。 lazy val seed = (project in file("./app/seed")).dependsOn(`genkanserver`)//これ追加。
この場合
sbt seed/runで実行できる。
また、
sbt seedとかやりたい時はどうすればいいかというとrootオブジェクトで設定できる。
import Dependencies._ import scala.sys.process.Process lazy val `genkanserver` = (project in file("."))//これが元からあるルートオブジェクト .enablePlugins(PlayScala) .settings( exec_seed := {//これを追加 Process( s"docker-compose exec -T web sbt seed/run" ).!//これはsbtからbashを叩くライブラリ。ここは別になんでもいいがscalaコードを書ける。 })
これで
sbt exec_seedでbashからdocker-compose exec -T web sbt seed/runとかした時と同じ意味がある。コマンドは特に意味がないので気にしなくて良い。
マイグレーションシステムを把握する
Playを使うならEvolutionがいいだろう。公式でも推奨されている。
ただ、PHPやRubyなどを使ってた人はここも少しばかり混乱するというか、まずEvolutionはDSLを使わずに普通にSQLでDDLを書く。
さらに公式でも書いてあるが1.sqlなどのファイルで行うため、Railsとかでよく言われるバージョン管理問題が起こるように最初は見える。
ただ、そこはEvolutionの方が優秀で、EvolutionはDDLをDB内に記録しており、SQLで書かれたDDLと現在のデータベースに差分が生まれると自動で差分を解消してくれるようになっている。なので意外と問題はない。というか慣れるとこっちの方が楽。
それさえ把握していればそこまで難しくはない。
もしかしたらこの問題はRailsに慣れ親しんだ人以外は壁とは思わないかもしれないが、RailsからScala移行を考える人のために一応書いておいた。
Slickを把握する
上記2つと比べて、壁と感じる割合が高いと思われるのがORMである。
Rails, PHP, Pythonなどで開発しているとORMはまず当たり前に使うと思うが、ScalaにはいわゆるORMというものはない。
その意味ではもし最初に挫折するとしたらこのDBアクセスライブラリであるSlickが一番挫折ポイントだと思う。
本当にクセが強いライブラリで、例えば1対多の関係を持つユーザーとメモ見たいな関係をRubyなどのORMで書けば
User.findById(id= 1).memos
とかでかける。
Slickで書くと
val action = Users join(Memo) on (_.id === _.user_id) val resultFuture = db.run(action).map( _.groupby(_.1) .map( userAndMemos = > { (userAndMemos._1, userAndMemos._2.map(_.2)) } ) resultFuture.map{ users => println(users.head._2) }
何をしているかというと、JOIN文などを生成しDBから取ってきたあと自分でgroupbyとかしてuserと関連するmemoにまとめる処理を書いているのだ。つまりDBアクセスしたあとORMがオブジェクト指向っぽくまとめている部分は自分で書くことになる。
生成と実行が分けられるとか慣れてくるとすごく便利なのだが、最初見た時「え、こんなこと自分でやるのかよ・・・」ってなった。Javaから来たエンジニアやScalaユーザーからすると当たり前じゃん何いってんだって思うかもしれないがそう感じる人は意外と多いと思う。この辺の乖離がScalaが採用しづらい理由でもある気がしている。
ただ、Rails等のユーザーに言っておきたいのは本当に慣れるとこっちの方が良くなってくる。というか意識しないとN+1問題などが起こる仕組みより、こっちで自分で書いた方がパフォーマンスとかほんと安全だし慣れるとそんなに難しくない。
これはScalaが配列操作などがとても強く、モナドによって集合を扱うことが非常に楽だからというのも要因の一つなのだが、半年くらい経つとScalaにおいてはこっちの方が絶対いいわってなるので頑張って使ってみてほしい。
ちなみに他にもDB系のライブラリはあり、ScalikeJDBCは日本人が作者でとてもクオリティも高い&スターも多いのでこっちの方がいいという意見もあると思う。(あと作者の瀬良さんがすごくいい人。Scala実践入門は現時点で最強の入門書だと思います。)
個人的には慣れるとSlickは結構好きになったのと、コミュニティ的にはSlickはまだまだ活発なのでとりあえず始めるならこっちでいいんじゃないかなぁと思ってたりする。
というわけでSlickをPlayでいじるハンズオンを作ったのでQiitaで公開します。これをやれば大抵困らなくなるのと、意外と情報がない0からdockerでPlay環境を構築するところも把握できます。
DDDを把握する
これもSlickと同様に難易度が高い。これに関しては有名な本が軒並み抽象的すぎて意味がわからないのが原因だと思う。
ScalaやるならDDDをやるのは本当にオススメなのだが、肝心なDDDをどうやって覚えるかが難しい問題である。
あと、DDDって意外とちゃんとやれている人が少なくて、(弊社でも微妙にDDDから外れている実装をしているところはある。今後改善していきたい。)Qiitaとか見てても結構間違ったこと言ってたりする記事があるので注意。
いかにこの順番通り理解していったらいいんじゃないかというのを上げておく。
松岡さんのツイッターとブログを読む
この人がDDD入門するなら一番強いと思う。
僕はDDDに関してはこの人とかとじゅんさんという人のツイッターやブログなどでほとんどの知識を覚えたのだが、最初の最初に読むなら松岡さんのこの記事が一番いい。
なぜDDD初心者はググり出してすぐに心がくじけてしまうのか - little hands’ lab
そのあとに松岡さんのブログを下から見ていけばいいと思います。
ドメイン駆動設計(DDD) カテゴリーの記事一覧 - little hands’ lab
DDD on Scalaのスライドとコードを読む
ある程度DDDを理解したら次は以下の資料と
https://speakerdeck.com/crossroad0201/scala-on-ddd
このレポジトリ を読むべき。
https://github.com/crossroad0201/ddd-on-scala
これはDDD on ScalaというScalaを使ってどのようにDDDを書くのかというのが全部書かれたレポジトリで、最初のうちはこれを指針に進んでいけばいい。
このレポジトリを見れば、DDDをやることでいかにサービス層が自然言語っぽく書けてすごいのかってのがよくわかるし、それをどう実装するべきなのかがわかる。
スライドに考え方は全部書いてあるので全部読みましょう。
「IDDD本から理解するドメイン駆動設計」連載を全部読んでおく(もしくはわからなくなったら読んでおく)
https://codezine.jp/article/corner/655
この連載は実践DDDという本の内容を使ってDDDを解説する記事なのだが、困った時に読むと結構ピンポイントで知りたかったところが書いてある。会員登録すれば読めるので会員登録して絶対に読んでおくべき。
特に10~12章の話はDDDで開発しようと思ったら絶対に引っかかるし、読んでおいた方がいい。
たまに「これ定義的にあってる?」みたいな箇所がないわけでもないが、基本的には助けになる。
チームで議論をする& 有識者を探して色々聞く
DDDは本当にその時々によって正解が変わる。
なのでチームで議論してどのようにしていくかをしつこいくらいやる必要がある。議論が嫌いなチームには多分結構難しい。
なのでこれどうするんだろって疑問が少しでも生まれたら議論することが大事。
あと、おそらくDDDやったことないチームがドメインモデルを作ると100%データ構造を再現しようとして間違ったドメインモデルを作る。
例えばあるモデルの関連モデルが空かどうかをチェックするメソッドを作りたいとする。
普通に考えるとモデルを作成する時に関連モデルの配列を持たせればできると考える。
この時点でORM等の考え方に侵されている。でも大概の人間がこうなると思う。
そもそも空であるかどうかをチェックするだけなら「空であるかどうか」という変数を1つ持ち、その変数をリターンするだけでいい。わざわざそのために配列を持つ必要なんてない。ドメインモデルを生み出す時にすでにチェックしておき、boolean値の変数を一つ持たせておけばいいのである。(もちろん僕たちもこの間違いは散々やりましたが、その時アドバイザーをしてくれていたScalaで有名な某b社の方に考え方を指摘されて初めて考え方が間違っていることに気づきました。)
こういったようにユースケースをよく考えて、そのドメイン知識を表現するのがドメインモデルであり、そこに慣れるまではすでにDDDのプロフェッショナルな人に聞いた方がいい。
松岡さんやかとじゅんさんは質問箱等を公開しているし、松岡さんはハンズオンや勉強会などを積極的に開いているのでそこに行ってもいいと思うがとにかく色々聞いていくことが重要である。
認証ライブラリを把握する
これも結構大変。
Scala にはplay-authとか色々あるのだが、結構カスタムが大変だったりする。
結局最終的に弊社が採用したのがSilhouetteというライブラリで、こいつは把握すればかなり優秀だった。
ただし把握するまでにCPOがソースコード読んだり色々やって1ヶ月くらいかけて完全に把握したので、認証系はできることならcognitoやfirebaseなどの認証サービスで行いたい。
モナドや関数型を把握する
これも最初は鬼門。
本来であればScalaはこのようなモナドや関数型をやらなくてもいい。だから最初はやらないという選択肢を取るのもいいと思う。
ただ、やってくうちに絶対にその書き方を使いたくなる。
特にddd on Scalaとか見ていればどのくらいそれが強力かはよくわかるし、とりあえずOptionとEitherとFutureがどっかには出てくるのでそれを使っているうちにapplicative functorなどが使いたくなり最終的に関数型の沼にはまっていく。
ただはまった側の意見からすると、はまっとくと得しかなかったし、最初難しかったが途中からありとあらゆる言語でモナド使いたくなった。
そういう方はcatsというライブラリを使うことになる。
CPOの記事でもいっているのだが、これはドキュメントなどが結構優秀なので、ぜひ見ていくといい。
また、catsを使う時に使えるqiita記事もいくつかあるので読んでおいた方がいい
Scalaのcatsについて知っておきたい9つのtips(和訳記事) - Qiita
ちなみに昔はscalazというのが関数型プログラミングのライブラリだったが、今はこんな理由でcatsを採用しておけばいい。
あと、scalazの記事になってしまうのだが、
この記事も結構使える。関数型を色々やろうとするとかなり大変なので、ここに出ている型や関数をとりあえず使うだけでもいいと思う。
Scalazにあるものは全てcatsにもあるので、ここでみてcatsだとどれなのか把握しても良い。
モナド変換を理解する
ScalaにはOptionやEither、Futureなどの文脈を保持する仕組みが豊富にある。
これのおかげで正常ルートに集中したプログラミングが多かったりするのだが、問題が起こる場合がある。
例えば、UserとBookモデルがあるシステムでDBにあるbookIdとuserId(本来はurlパラメータでもいいがめんどくさいので今回はjson内に置く)をJsonでポストして、ユーザーに登録する機能を作るとする。
手順としては
bodyに jsonがあるかをチェックする。なかった場合エラー
1. jsonのうちuser idとbook idを表す項目があるかチェック。なかった場合エラー
二つともあった場合、まずそのuserIdを持つユーザーモデルをDBから取得、なかった場合エラー
4. さらにそのbookIdを持つbookモデルをDBから取得、なかった場合エラー
ユーザーモデルにbookモデルのIDを紐づける。
1. ユーザーモデルを永続化、エラーがあったらエラーを返す。
永続化成功したらレスポンスを返す。
以下のようなレポジトリ たちがあったとして
trait UserRepository{ def getById(userId: Long): Future[Option[User]] } trait BookRepository{ def getById(bookId: Long): Future[Option[Book]] }
val userFuture: Future[User] = mayBeJson.map{ case Some(json) => { (json\”userId” , json\”bookId” ) match{ case (Some(userId),Some(bookId))=> { UserRepository.getById(userId).flatMap{ mayBeUser=> mayBeUser match{ case Some(user)=> BookReposioty.getById(bookId).flatMap{ mayBeBook=> mayBeBook match{ case Some(book) => UserReository.save(user.assign(book)) case None => Future.failed(new Exception(“そのIDのbookは存在しません。”)) } case None=> Future.failed(new Exception(“そのIDのuserは存在しません。”)) } } } } case (None,_)=> Future.fail(new Exception(“userIdが存在しません。”)) case (_,None)=> Future.fail(new Exception(“bookIdが存在しません。”)) case (None,None)=> Future.fail(new Exception(“userIdとbookIdが存在しません。”)) } } case None => Future.fail(new Exception(“jsonが存在しません。”)) }
多分こんな感じ。動作確認全くしてないので正確なところは怪しいけどこれを行うとおそらくFuture[User]が返る。
要するにめっちゃめんどくさい
というわけでこれを解決するライブラリが存在する。
モナド変換子やEFF、zioなどがそれであるが、弊社ではEFFを採用している。
これらに慣れていく必要があるのも結構挫折のポイントだったりする。
慣れるとeffすげーってなるが。
effに関しては
- Scala + CleanArchitecture に Eff を組み込んでみた – PSYENCE:MEDIA
- モナドが分からなくても使える Extensible Effects - Qiita
- eff
等を見るといい気がする。
とりあえずeffのドキュメントのチュートリアルは何度か読み直しつつ書いていくといい。なんとなくやり方がわかってくる。
ただ実行時にrunAsyncにはscedulerがいるとか、runEitherに型を指定する方法とかがあまり載ってないのでわからなかったらDM等で聞いて下されば答えます。
デプロイ
これは意外と簡単なのだが、情報が少ない。
ちなみにHerokuはplay対応してる。さすが。
Play Frameworkに関しては、以下だけ知っていれば結構いける。
DB接続情報などをどう管理するか
アプリケーション側でDB情報を環境変数から読むようにする。
本番は環境変数でDB接続情報を読み込みたい。
PlayではTypesafe Configを使ってサーバーのコンフィグを設定しているため、
sbt run -Dconfig.file=./conf/application.prod.confとかオプションを書くとconfファイルを入れ替えられる。
そのためまずapplication.prod.confを作り、
application.conf
slick.dbs.default { dataSourceClass = org.postgresql.ds.PGSimpleDataSource profile="slick.jdbc.PostgresProfile$" db { driver=org.postgresql.Driver url="jdbc:postgresql://開発環境localhost" databaseName = "開発環境DB名" user= "開発環境ユーザー名" password="開発環境Password" }
となっている環境を
application.prod.conf
slick.dbs.default { dataSourceClass = org.postgresql.ds.PGSimpleDataSource profile="slick.jdbc.PostgresProfile$" db { driver=org.postgresql.Driver url=${?"DB_URL"} databaseName= ${?"DB_NAME"} user= ${?"DB_USER"} password=${?"DB_PASS"}
と書き換える。この場合環境変数が設定されていないと何も設定がされないが、デプロイをするときに繋がっていないか確認できるのでむしろ環境変数が設定されているかどうかを検知するためにもあえてNoneがある状態にしておく。
ちなみに意外と引っかかるので書いておくと、もしNoneの時にデフォルトのconfigを入れたい場合、
application.prod.conf
slick.dbs.default { dataSourceClass = org.postgresql.ds.PGSimpleDataSource profile="slick.jdbc.PostgresProfile$" db { driver=org.postgresql.Driver url="デフォルトurl" url=${?"DB_URL"} databaseName= "デフォルトDB名" databaseName= ${?"DB_NAME"} user= "デフォルトuser名" user= ${?"DB_USER"} password="デフォルトpassword" password=${?"DB_PASS"}
というように環境変数を読み込む上に同じ変数名でデフォルト値を書いておく。公式もこんな感じにやってるので多分正しい。
こうすると環境変数があればデフォルト値は上書きされ、なければデフォルト値になる。
サーバーをバイナリ化
まずPlayサーバーはRailsと違ってrun productionとかするのではなく、コンパイルしてからバイナリで起動する。
そのためまず最初にバイナリ化することから始める。
ここに書いてある通り sbt distというコマンドでバイナリ化することが可能。
/target/universal以下にバイナリのzipができてるので、これを使ってdockerを作る。
以下が解凍したディレクトリ。binにバイナリが入っているのはもちろんだが、confディレクトリも一緒にzip化されていることがわかる。
ただ、dockerfileとかも一緒にzipに入れてくれればdockerを使ったデプロイなどがその中で全部できる。全部まとめたzipを作れないか?となる。
ここに書いてあるが、playではプロジェクトディレクトリにdistというディレクトリがあるとdist時に一緒にzip化してくれるそう。神。副次的にディレクトリのルートをdokcer-compose-proとかで汚さなくても済むようになった。
こんな感じでYour package is ready in ~とかで存在するパスを教えてくれる。デフォルトはtarget/universal以下
$ sbt dist [info] Loading global plugins from /Users/koska/.sbt/0.13/plugins [info] Updating {file:/Users/koska/.sbt/0.13/plugins/}global-plugins... [info] Resolving org.fusesource.jansi#jansi;1.4 ... [info] Done updating. [info] Loading project definition from /Users/koska/devlopment/GKAssistantServer/project [warn] Found version conflict(s) in library dependencies; some are suspected to be binary incompatible: [warn] [warn] * org.webjars:webjars-locator-core:0.36 is selected over 0.32 [warn] +- com.typesafe.sbt:sbt-web:1.4.4 (scalaVersion=2.10, sbtVersion=0.13) (depends on 0.32) [warn] +- com.typesafe:npm_2.10:1.2.1 (depends on 0.32) [warn] [warn] Run 'evicted' to see detailed eviction warnings [info] Set current project to GenkanServer (in build file:/Users/koska/devlopment/GKAssistantServer/) [info] Wrote /Users/koska/devlopment/GKAssistantServer/target/scala-2.12/genkanserver_2.12-1.0.pom [warn] [1] The maintainer is empty [warn] Add this to your build.sbt [warn] maintainer := "your.name@company.org" [success] All package validations passed [info] [info] Your package is ready in /Users/koska/devlopment/GKAssistantServer/target/universal/genkanserver-1.0.zip [info] [success] Total time: 3 s, completed 2019/09/04 4:36:18
以下のようにzip化されたファイルがあるところまで行き、解凍。distの中に入れたGemfileとかはルートディレクトリに配置されている。
$ cd target/universlal $ ls genkanserver-1.0.zip scripts stage $ unzip genkanserver-1.0.zip Archive: genkanserver-1.0.zip inflating: genkanserver-1.0/docker-compose-pro.yml inflating: genkanserver-1.0/DockerfileProduction inflating: genkanserver-1.0/Gemfile inflating: genkanserver-1.0/Gemfile.lock inflating: genkanserver-1.0/DockerfileDb inflating: genkanserver-1.0/lib/genkanserver.genkanserver-1.0-sans-externalized.jar ... inflating: genkanserver-1.0/share/doc/api/domain/services/DeviceService.html inflating: genkanserver-1.0/share/doc/api/router/Routes.html inflating: genkanserver-1.0/share/doc/api/router/index.html inflating: genkanserver-1.0/share/doc/api/router/RoutesPrefix$.html inflating: genkanserver-1.0/README.md $ ls genkanserver-1.0 genkanserver-1.0.zip scripts stage $ cd genkanserver-1.0 DockerfileDb Gemfile README.md conf lib DockerfileProduction Gemfile.lock bin docker-compose-pro.yml share
ここから起動するなら
genkanserver-1.0ディレクトリ内で
bin/genkanserver -Dconfig.file=conf/application.prod.conf
とやればapplication.prod.confをコンフィグとして起動する。
最後に
これらをある程度把握するだけで意外とscalaは導入できたりする。
だけとはいってるが結構な量である。弊社はCPOとCEO2人でおおよそ2ヶ月は使い方を徐々に学んでいった感じであった。そこから完璧に問題なく開発するまで多分半年くらいかかっていると思う。
Scala関連の情報は本当に少なく、引っかかった時にかなり苦労する。
その時はscalajp/public - Gitterで聞いてみるといい。
最初僕も質問するのが怖かったが、本当にみんなちゃんと答えてくれている。(ていうか僕がScala初心者すぎて戸惑ってた時に質問しまくってたのが少し遡ると見えるが、Scala界の有名人がちゃんと答えてくれている。素晴らしいコミュニティだ。)
'JLPT' 카테고리의 다른 글
Vue.jsを100時間勉強して分かったこと (0) | 2021.06.27 |
---|---|
【PHP8.0】PHPに名前付き引数が実装される (0) | 2021.06.27 |
ニューラルネットワーク研究開発職の募集 (0) | 2021.06.26 |
Node.jsのフレームワーク「Express」とは【初心者向け】 (0) | 2021.06.26 |
CSSのアニメーションがIEで動作しない (0) | 2021.06.26 |
댓글