ScalaにRESTful Webサービスを実装します

先週のHabréには、JavaでのRESTful Webサービスの実装に関する2つの記事がありました。 さて、遅れずにモナドと応用ファンクターを使用してScalaで独自のバージョンを作成しましょう。 経験豊富なScala開発者はこの記事で新しいものを見つけることはまずないでしょうし、Djangoファンは一般にこの機能をすぐに使えると言うでしょうが、Java開発者にとって興味深いものであり、読みたくなります。

準備する


前の記事のタスクを基礎として取り上げますが、ソリューションコードが画面に収まるように解決しようとします。 少なくとも40インチの5番目のフォント。 結局、21世紀には、メガバイトのxml-configsと数十の抽象的なファクトリーなしで簡単なタスクを解決できるはずです。

リンクをたくない人のために、顧客データベースにアクセスするための最も単純なRESTfulサービスを実装することを明確にします。 必要な機能は、データベース内のオブジェクトの作成と削除、およびさまざまなフィールドでソートできるすべてのクライアントのリストのページネーションです。

私たちが家を建てるレンガとして、私たちは次のものを取ります:

記事の過程で、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メソッドの処理を段階的に調べてみましょう。

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を使用して興味深いものを開発したい場合は、書いてください-有能な専門家を探しています。

Source: https://habr.com/ru/post/J150199/


All Articles