さて、裏で何が起きているのかを意識しなくていいというのは多くの場合便利な反面問題が起きた際の検知または解決が遅れるということでもある。データベースアクセスというのは大体の場合物理ファイルへのアクセスがあり、クエリの実行にはソケットが使われるので通信が発生する。これらはパフォーマンスを大きく損なう操作でもあり、一般的(誰?)は可能な限り避けるべきとされている(要出典)。ここでは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の所属する部署を得る
- 上記の部署に所属する全てのユーザを得る
- 上記で得られたユーザ毎にプロダクトを取得し、変換する
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が書けると隠しすぎてて辛いという時があるという話。
No comments:
Post a Comment