準備する
前の記事のタスクを基礎として取り上げますが、ソリューションコードが画面に収まるように解決しようとします。 少なくとも40インチの5番目のフォント。 結局、21世紀には、メガバイトのxml-configsと数十の抽象的なファクトリーなしで簡単なタスクを解決できるはずです。
リンクをたくない人のために、顧客データベースにアクセスするための最も単純なRESTfulサービスを実装することを明確にします。 必要な機能は、データベース内のオブジェクトの作成と削除、およびさまざまなフィールドでソートできるすべてのクライアントのリストのページネーションです。
私たちが家を建てるレンガとして、私たちは次のものを取ります:
- Scalaはレンガではなく、基盤であり、
- Unfilteredは、HTTPリクエストを処理するための優れたライブラリです。
- Squeryl-データベースクエリ用のライブラリ、
- ジャクソンは、もともとJava向けに記述されたJSONを操作するためのライブラリですが、Scala型に対応しているため、
- Scalazは、↦、↦、∃などのコードでさまざまな面白い文字を記述できるライブラリであり、同時に、応用ファンクタ、モノイド、セミグループ、およびクライスリー矢印などの便利な抽象化を実装します。 確かに、私はまだ後者を使用する必要はありませんでしたが、これはおそらく、機能的啓発の必要な程度にまだ到達していないという事実によるものです。
記事の過程で、Scalaに慣れていない人でもコードを理解できるように十分な説明をしようとしますが、何がうまくいくかは約束しません。
戦いに
データモデル
まず、データモデルを決定する必要があります。 Squerylを使用すると、通常のクラスの形式でモデルを指定できます。また、書きすぎないように、JSONでの後続のシリアル化に同じクラスを使用します。
@JsonIgnoreProperties(Array("_isPersisted")) case class Customer(id: String, firstName: String, lastName: String, email: Option[String], birthday: Option[Date]) extends KeyedEntity[String]
タイプ
Option[_]
フィールドは、データベースのNULL入力可能列に対応しています。 このようなフィールドは、値がある場合は
Some(value)
、値がない場合は
None
2種類の値をとることができます。
Option
使用すると、
NullPointerException
の可能性を最小限に抑えることができます。これは、関数型プログラミング言語(特に
null
概念がまったくない言語)で一般的な方法です。
@JsonIgnoreProperties
は、JSONシリアル化から特定のフィールドを除外します。 この場合、Squerylが追加した
_isPersisted
フィールドを除外する必要がありました。
データベーススキーマの初期化
JDBCを使用した経験がある人は、最初に行うことはデータベースドライバークラスを初期化することであることを知っています。 この慣行から逸脱しないようにしましょう。
Class.forName("org.h2.Driver") SessionFactory.concreteFactory = Some(() => Session.create(DriverManager.getConnection("jdbc:h2:test", "sa", ""), new H2Adapter))
1行目では、JDBCドライバーをロードし、2行目では、使用する接続ファクトリーをSquerylライブラリーに指示します。 データベースとして、軽量で高速なH2を使用します。
スキームの転換点が来ました:
object DB extends Schema { val customer = table[Customer] } transaction { allCatch opt DB.create }
まず、データベースに
Customer
クラスに対応するテーブルが1つ含まれていることを示し、次にDDLコマンドを実行してこのテーブルを作成します。 実際には、通常、自動テーブル作成の使用には問題がありますが、簡単なデモンストレーションには非常に便利です。 データベースにテーブルが既に存在する場合、
DB.create
例外をスローします
allCatch opt
おかげで、無視できます。
JSONのシリアル化と逆シリアル化
まず、JSONパーサーを初期化して、Scalaで受け入れられるデータ型を使用できるようにします。
val mapper = new ObjectMapper().withModule(DefaultScalaModule)
次に、JSON文字列をオブジェクトに変換するための2つの関数を定義します。
def parseCustomerJson(json: String): Option[Customer] = allCatch opt mapper.readValue(json, classOf[Customer]) def readCustomer(req: HttpRequest[_], id: => String): Option[Customer] = parseCustomerJson(Body.string(req)) map (_.copy(id = id))
parseCustomerJson
関数は実際にはJSONを解析しています。
allCatch opt
使用することにより、解析プロセス中に発生し
allCatch opt
例外がキャッチされ、結果として
None
が取得されます。 2番目の関数
readCustomer
は、HTTPリクエストの処理に直接関連しています。リクエストの本文を読み取り、タイプ
Customer
オブジェクトに変換し、
id
フィールドを指定された値に設定します。
両方の関数で戻り値の型を指定する必要はなかったことに注意する価値があります:コンパイラーはプログラマーの助けなしに型を推測するのに十分なデータを持っていましたが、明示的に指定された型は時々コードの人間の理解を容易にします。
逆のプロセス
Customer
オブジェクト(または
List[Customer]
リスト)をHTTP応答の本文に変えることも難しくありません。
case class ResponseJson(o: Any) extends ComposeResponse( ContentType("application/json") ~> ResponseString(mapper.writeValueAsString(o)))
将来的には、単に
ResponseJson
型のオブジェクトを返すだけで、フィルタリングされていないフレームワークは、それを正しいHTTP応答に変換します。
もう1つの小さなタッチは、新しい顧客識別子の生成です。 常に最も便利な方法ではありませんが、最も簡単な方法はUUIDを使用することです:
def nextId = UUID.randomUUID().toString
HTTPリクエスト処理
準備作業のほとんどが完了したので、Webサービスの実装に直接進むことができます。 Unfilteredライブラリの詳細は説明しませんが、最も簡単な使用方法は次のとおりです。
val service = cycle.Planify { case /* */ => /* , */ }
私たちのサービスには、
/customer
と
/customer/[id]
2つのエントリポイントがあります。 2番目のものから始めましょう。
case req@Path(Seg("customer" :: id :: Nil)) => req match { case GET(_) => transaction { DB.customer.lookup(id) cata(ResponseJson, NotFound) } case PUT(_) => transaction { readCustomer(req, id) ∘ DB.customer.update cata(_ => Ok, BadRequest) } case DELETE(_) => transaction { DB.customer.delete(id); NoContent } case _ => Pass }
1行目では、このコードが
/customer/[id]
という形式のURLのみを処理し、渡された識別子をid変数にバインドすることを示しています(不変変数を呼び出すことができる場合)。 次の行では、リクエストのタイプに応じて動作を調整します。 たとえば、PUTメソッドの処理を段階的に調べてみましょう。
-
transaction { ... }
:ハンドラーの本体の期間中、トランザクションを開く必要があることを示します。 -
readCustomer(req, id)
:リクエスト本文を読み取り、Option[Customer]
を返す事前に作成されたメソッドを使用します -
∘
:このシンボルは特別な注意に値します。実際、これはマップ操作の同義語であり、オプションコンテンツに何らかの機能を適用できます。 -
DB.customer.update
:適用したいまさにその機能は、データベース内のエンティティを更新することです。 -
cata(_ => Ok, BadRequest)
:Option
に値がある場合はOk
返し、リクエストを解析できず、クライアントの代わりにNone
がある場合はBadRequest
返します。
GETおよびDELETE要求は同様に処理されます。
/customer
にリクエストを提供するハンドラーの後半では、2つの補助機能が必要です。
val field: PartialFunction[String, Customer => TypedExpressionNode[_]] = { case "id" => _.id case "firstName" => _.firstName case "lastName" => _.lastName case "email" => _.email case "birthday" => _.birthday } val ordering: PartialFunction[String, TypedExpressionNode[_] => OrderByExpression] = { case "asc" => _.asc case "desc" => _.desc }
これらの関数は、リクエストの一部
order by
を作成
order by
使用されます。おそらく、Squerylの腸で調べてみると、簡単に書くことができますが、このオプションも機能しました。 ハンドラーコード自体:
case req@Path(Seg("customer" :: Nil)) => req match { case POST(_) => transaction { readCustomer(req, nextId) ∘ DB.customer.insert ∘ ResponseJson cata(_ ~> Created, BadRequest) } case GET(_) & Params(params) => transaction { import Params._ val orderBy = (params.get("orderby") ∗ first orElse Some("id")) ∗ field.lift val order = (params.get("order") ∗ first orElse Some("asc")) ∗ ordering.lift val pageNum = params.get("pagenum") ∗ (first ~> int) val pageSize = params.get("pagesize") ∗ (first ~> int) val offset = ^(pageNum, pageSize)(_ * _) val query = from(DB.customer) { q => select(q) orderBy ^(orderBy, order)(_ andThen _ apply q).toList } val pagedQuery = ^(offset, pageSize)(query.page) getOrElse query ResponseJson(pagedQuery.toList) } case _ => Pass }
POSTリクエストに関連する部分には新しいものは何も含まれていませんが、リクエストパラメータを処理する必要があり、2つの不明瞭な文字
∗
と
^
ます。 最初の(慎重に、通常のアスタリスク
*
と混同しないでください)は
flatMap
同義語で
flatMap
、使用する関数も
Option
返すという点で
map
とは異なります。 したがって、複数の操作を連続して実行できます。各操作は、値を正常に返すか、エラーの場合は
None
を返します。 2番目の演算子はもう少し複雑で、使用するすべての変数が
None
と等しくない場合にのみ何らかの操作を実行できます。 これにより、列と方向の両方が指定されている場合にのみソートし、ページ番号とそのサイズの両方が指定されている場合にのみ結果をページに分割できます。
それだけです、残っているのはサーバーを起動することだけです
Http(8080).plan(service).run()
カールを拾って、すべてが機能することを確認できます。
おわりに
私の意見では、結果のWebサービスコードはコンパクトで読みやすく、これは非常に重要な特性です。 当然、理想的ではありません。たとえば、おそらく
scala.Either
または
scalaz.Validation
を使用してエラーを処理する価値がありましたが、Unicode演算子の使用を好まない人もいるかもしれません。 さらに、外部の単純さの背後に、非常に複雑な操作が隠されている場合があり、すべてが「内部」でどのように機能するかを理解するには、脳回に負担をかける必要があります。 それでも、この記事が誰かにScalaを詳しく見てもらうことを願っています。たとえこの言語を仕事に適用しなくても、きっと何か新しいことを学べることでしょう。
予想どおり、このコードはGitHubに投稿されており、アセンブリ用のimport-sおよびsbt-scriptの存在のみが記事に記載されているものと異なります。
記事の冒頭で、モナドやその他の悪霊がWebサービスに存在することを約束しました。 したがって、
flatMap
(別名
∗
)は単項バインドであり、
^
演算子は適用ファンクターに直接関連しています。
そして最後に、ハリコフまたはサラトフにいて、ScalaとAkkaを使用して興味深いものを開発したい場合は、書いてください-有能な専門家を探しています。