2017-03-10

JSONユーティリティ

最近の動向として猫も杓子もJSONとなっている感じがあるが、SagittariusではJSONのサポートが薄い(Githubに拙作のJSON Toolsを置いているが本体に入れようかなぁ)ので多少手厚くしようかなぁと思いSchemeオブジェクトに変換するライブラリを書いたりしてみた。まぁ、将来的にJWTとか実装しようと思うと生のデータは扱いづらいがDSLクエリーでは効率が悪いという話もあった。

以下のように使える
(import (rnrs) (text json object-builder))

;; JSON string
(define json-string "{
  \"Image\": {
    \"Width\":  800,
    \"Height\": 600,
    \"Title\":  \"View from 15th Floor\",
    \"Thumbnail\": {
      \"Url\":    \"http://www.example.com/image/481989943\",
      \"Height\": 125,
      \"Width\":  100
  },
    \"Animated\" : false,
    \"IDs\": [116, 943, 234, 38793]
  }
}")

;; records represent JSON object
(define-record-type image-holder
  (fields image))
(define-record-type image
  (fields width height title thumbnail animated ids))
(define-record-type thumbnail
  (fields url height width))

;; JSON -> Scheme object definition
(define builder (json-object-builder
                 (make-image-holder
                  ("Image"
                   (make-image
                    "Width"
                    "Height"
                    "Title"
                    ("Thumbnail"
                     (make-thumbnail
                      "Url"
                      "Height"
                      "Width"))
                    "Animated"
                    ("IDs" (@ list)))))))

;; Scheme object -> JSON definition
(define serializer (json-object-serializer
                    (("Image" image-holder-image
                      (("Width" image-width)
                       ("Height" image-height)
                       ("Title" image-title)
                       ("Thumbnail" image-thumbnail
                        (("Url" thumbnail-url)
                         ("Height" thumbnail-height)
                         ("Width" thumbnail-width)))
                       ("Animated" image-animated)
                       ("IDs" image-ids (->)))))))

(let ((image (json-string->object json-string builder)))
  ;; do some useful thing with image

  ;; ok I want JSON string
  (object->json-string image serializer))
;; Formatted for convenience
;; -> {
;;      "Image": {
;;        "Width": 800,
;;        "Height": 600,
;;        "Title": "View from 15th Floor",
;;        "Thumbnail": {
;;          "Url": "http://www.example.com/image/481989943",
;;          "Height": 125, "Width": 100
;;        },
;;        "Animated": false,
;;        "IDs": [116, 943, 234, 38793]
;;      }
;;    }
リストとかベクタとかも扱えたりするので、まぁ要りそうな機能はざっくりあるかなぁという感じ。

CLOSを使ってJavaのアノテーションよろしくゴテゴテした感じにしてもよかったんだけど、最近そういう書き方を避けてるのと、既存のレコード等も再利用可能にするためにこんな感じのデザイン。SXMLに対してのオブジェクト構築と似た感じになってるのはそっちもそんな感じのデザインで作ってあるから。

使い勝手は(当面のJSONが必要な場面がこれなので)JWTを実装しつつ見ていくことになるが、そこまで大きな変更はないだろうなぁと思ったり。

2017-03-02

Hibernateでクエリー爆発した話

JavaでORMと言えば真っ先に思い浮かぶのがHibernateであろう。データベースをほぼJava Beansのように扱える便利なライブラリである。ともすれば、裏で何が起きているのか全く感知しなくてもいいので、SQLが嫌いな人にはなかなかに受けが良いようである(要出典)

さて、裏で何が起きているのかを意識しなくていいというのは多くの場合便利な反面問題が起きた際の検知または解決が遅れるということでもある。データベースアクセスというのは大体の場合物理ファイルへのアクセスがあり、クエリの実行にはソケットが使われるので通信が発生する。これらはパフォーマンスを大きく損なう操作でもあり、一般的(誰?)は可能な限り避けるべきとされている(要出典)。ここではHibernateによって隠されたこれらの操作が引き起こすパフォーマンス劣化の体験談を今後の戒めとして記録しておく。

問題になったのはだいたいこんな感じのテーブル。
                       +--------------+
   +--------+        |  X_User_Dep  |    +-------------+          +-----------+
   |  User  |          +--------------+           |  X_UD_Prod  |          |  Product  |
   +--------+ 1   1..* |  + ID        | 1    1..* +-------------+          +-----------+
   | + ID   | o------o |  + User_ID   | o-------o |  + UD_ID    | 1..*   1 |  + ID     |
   | + Name |          |  + Dep_ID    |         |  + Prod_ID  | o------o |  + Name   |
   +--------+        +--------------+    +-------------+          +-----------+
                        o 1..*
           |
           |
                 o 1
                       +--------------+
                       |  Department  |
                       +--------------+
                       |  + ID        |
                       |  + Name      |
                       +--------------+
Java側はこんな感じのエンティティ
@Entity
@Table(name = "User")
public class User {
    @Id
    private int id;
    @Column(name="Name")
    private String name;
    @OneToMany(mappedBy = "user")
    private Set<UserDepartment> userDepartments;
}

@Entity
@Table(name = "Department")
public class Department {
    @Id
    private int id;
    @Column(name="Name")
    private String name;
    @OneToMany(mappedBy = "department")
    private Set<UserDepartment> userDepartments;
}

@Entity
@Table(name = "Product")
public class Procdut {
    @Id
    private int id;
    @Column(name="Name")
    private String name;
}

@Entity
@Table(name = "X_User_Dep")
public class UserDepartment {
    @Id
    private int id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "User_ID")
    private User user;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "Dep_ID")
    private Department department;
    @OneToMany(mappedBy = "product")
    private Set<Product> products;
}

@Entity
@Table(name = "X_UD_Prod")
public class UserProduct {
    @Id
    private int id;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "UD_ID")
    private UserDepartment;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "Prod_ID")
    private Product product;
}
ビジネスロジックは以下のようなことを実行する:
  • ユーザAが所属する部署に所属する全てのユーザが保持するプロダクトを得る
  • 適切なオブジェクトに変換し返す
普通に考えれば、まず条件を満たす全てのレコードを取得し変換処理をするだろう。僕が目にしたコードは以下のようなことをしていた。
  • ユーザAの所属する部署を得る
  • 上記の部署に所属する全てのユーザを得る
  • 上記で得られたユーザ毎にプロダクトを取得し、変換する
SQLが直接見えるのであればこの処理がO(n*m)かかるひどいものだとすぐに気づくのだが、Javaのコードを見ると大体以下のように実装されていた。

getAllUserInDepartment(thisUser.getDepartment()).stream()
    .map(UserDepartment::getProducts)
    .map(convert)
    .collect(Collectors.toList);
これの困ったところは、最初のgetAllUserInDepartment以外は目に見えたデータベースアクセスがないということ。実際にはproductsはLAZYに初期化されているので、convertないで他の何らかProductのプロパティにアクセスして初めて取得のクエリが走る。EAGERに取得しろよという話もあるが、このオブジェクトが使われているのはここだけではないので影響範囲に責任持ちたくないという無責任さからやめた。キャッシュという選択肢もあったんだけど、使われているデータベースへのアクセスがJavaアプリケーション以外からもあるので多分キャッシュの整合性が取れないということ(+僕自身Hibernateにそこまで精通していないの)で断念。

どうしたかといえば、エンティティ定義は変えずに、ひたすら下請けのSQLがどんな風になるかを想像しながらDetachedCriteriaを書いて逃げた。正直、SQL直接書かせてください、お願いしますという気分にはなったが…こういう(キャッシュ使えない、エンティティ定義変更できない)時はどうするのがベストプラクティスなのか興味があるが、僕のググり力の低さのせいで解決方法は見つからず。

Hibernate便利なんだけどある程度SQLが書けると隠しすぎてて辛いという時があるという話。