概要
Java Persistence API 2.1のおさらいをHibernateのUser Guideを利用して行いました。おさらいのポイントは次の2点になります。
- OneToMany,ManyToOneなどのアノテーションを使った関連の定義
- 関連の方向性について
- カスケードの指定について
- EAGER,LAZYなどのFetch戦略
環境
動作検証は下記の環境で行いました。
- Windows 10 Professional
- Java 9.0.4
- Spring Boot 2.0.0
- Spring Data Jpa 2.0.5
- Hibernate ORM 5.2.14
- MySQL 5.7.19
参考
- [JSR 338: JavaTM Persistence 2.2] (https://jcp.org/en/jsr/detail?id=338)
- [Java(TM) EE 7 Specification APIs] (https://docs.oracle.com/javaee/7/api/javax/persistence/package-summary.html)
- [Hibernate ORM 5.2.14.Final User Guide] (http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html)
- [Spring Data JPA - Reference Documentation] (https://docs.spring.io/spring-data/jpa/docs/2.0.4.RELEASE/reference/html/)
関連と方向性とカスケードについて
Hibernate ORM 5.2.14.Final User Guideの[2.7. Associations] (http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#associations)から関連について覚えておきたい点をまとめました。
関連(association)について
- 1対1 (OneToOne)
- 1対多 (OneToMany)
- 多対1 (ManyToOne)
- 多対多 (ManyToMany)
方向性(directionality)について
- 単方向 (Unidirectional)
- 双方向 (Bidirectional)
双方向の関連では、それぞれを所有者側(owning side)と逆側(inverse side) or (the mappedBy)と言います。
ManyToOneの例
単方向の関連
(子)Phoneから(親)Personへの単方向の関連です。
Entity
- Personエンティティ
- 親側 (parent side)
- Phoneエンティティ
- 子側 (child side)
- 外部キーを持つ
- ManyToOneアノテーション
@Entity(name = "Person")
public class Person {
@Id
@GeneratedValue
private Long id;
}
@Entity(name = "Phone")
public class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne
@JoinColumn(name = "person_id") // 付けなくてもOK
private Person person;
}
DDL
エンティティクラスから下記のDDLが発行されます。
create table person (
id bigint not null,
primary key (id)
) engine=InnoDB
create table phone (
id bigint not null,
`number` varchar(255),
person_id bigint,
primary key (id)
) engine=InnoDB
alter table phone
add constraint FKkk6uij3j6wikpnqlj9dymobs9
foreign key (person_id)
references person (id)
外部キーの名前を指定する
JoinColumnアノテーションのforeignKey属性にForeignKeyアノテーションで任意の名前を指定することができます。
@ManyToOne
@JoinColumn(name = "person_id", foreignKey = @ForeignKey(name="fk_person_id"))
private Person person;
単方向のカスケード
この例では(子)Phoneから(親)Personへの関連なので、(親)Personを削除して関連する(子)Phoneをカスケード削除することはできません。
以下のようなコードでPersonを削除しようとすると
Person person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
SQL発行時にpersonテーブルへ外部キーによる参照があるのでエラーが発生します。
o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 1451, SQLState: 23000
o.h.engine.jdbc.spi.SqlExceptionHelper : Cannot delete or update a parent row: a foreign key constraint fails (`demo_db`.`phone`, CONSTRAINT `FKkk6uij3j6wikpnqlj9dymobs9` FOREIGN KEY (`person_id`) REFERENCES `person` (`id`))
正確にはカスケード削除ではありませんが、以下のようにするとPersonエンティティとPhoneエンティティを同時に削除することができます。
これは(子)Phoneがすべて削除されたら関連する(親)Personも削除するというカスケードになります。
ManyToOneアノテーションのcascade属性にALLまたはREMOVEを指定し、
(なお、Phoneをすべて削除してもPersonは残したい場合は指定しません。)
@Entity(name = "Phone")
public class Phone {
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "person_id")
private Person person;
}
Phoneをすべてremoveします。Personを参照するPhoneがすべて無くなるとPersonも削除されます。
ただし、この方法では複数回update/delete文が発行されるので非効率です。
Person person = entityManager.find(Person.class, 1L);
List<Phone> phones = entityManager.createQuery("SELECT p FROM Phone p WHERE p.person = :person", Phone.class)
.setParameter("person", person)
.getResultList();
phones.forEach(entityManager::remove);
OneToManyの例
単方向の関連
(親)Personから(子)Phoneへの単方向の関連です。
単方向のOneToManyでは、2つのテーブルの関連を結合テーブルで表現します。
Entity
- Personエンティティ
- 親側 (parent side)
- 外部キーを持たない
- OneToManyアノテーション
- Phoneエンティティ
- 子側 (child side)
@Entity(name = "Person")
public class Person {
@Id
private Long id;
@OneToMany(cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
}
@Entity(name = "Phone")
public class Phone {
@Id
private Long id;
@Column(name = "`number`")
private String number;
}
DDL
エンティティクラスから下記のDDLが発行されます。
この例ではpersonテーブルとphoneテーブルの関連にperson_phonesという結合テーブルが自動的に作成されます。
create table person (
id bigint not null,
primary key (id)
) engine=InnoDB
create table person_phones (
person_id bigint not null,
phones_id bigint not null
) engine=InnoDB
create table phone (
id bigint not null,
`number` varchar(255),
primary key (id)
) engine=InnoDB
alter table person_phones
add constraint UK_haq8fex9okoi3kpaxmqe1kpcl unique (phones_id)
alter table person_phones
add constraint FKincp66whfw4olpi3osmd8mylw
foreign key (phones_id)
references phone (id)
alter table person_phones
add constraint FKo03nn6lgnt28li9oxmhu65esh
foreign key (person_id)
references person (id)
結合テーブルのカスタマイズ
JoinTableアノテーションで結合テーブル名やカラム名をカスタマイズすることができます。
@OneToMany(cascade = CascadeType.ALL)
@JoinTable(name = "person_phones_joining"
, joinColumns = @JoinColumn(name = "_person_id")
, inverseJoinColumns = @JoinColumn(name = "_phone_id"))
private List<Phone> phones = new ArrayList<>();
DDL
create table person_phones_joining (
_person_id bigint not null,
_phone_id bigint not null
) engine=InnoDB
alter table person_phones_joining
add constraint UK_4u9o6oob90baot6wpa3gxono5 unique (_phone_id)
alter table person_phones_joining
add constraint FK1cxpkmp6xjkl7sad7h73sufq2
foreign key (_phone_id)
references phone (id)
alter table person_phones_joining
add constraint FKrgw9hiqn3v32jhxmlwkpinj8q
foreign key (_person_id)
references person (id)
単方向のカスケード
(親)Personから(子)Phoneへの単方向の関連です。
(親)Personと関連する(子)Phoneをカスケード削除する場合、以下のようにPersonを削除することで関連するPhone(と結合テーブル)も同時に削除されます。
ただし、この方法でもPhoneを1件ずつ削除するdelete文が発行されます。
Person person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
双方向の関連
(親)Personと(子)Phoneの双方向の関連です。
子側に親への外部キーを持ちます。
Entity
- Personエンティティ
- 親側 (parent side) or 逆側 (inverse side) or (the mappedBy)
- OneToManyアノテーション
- Phoneエンティティ
- 子側 (child side) or 所有者側 (owning side only)
- 外部キーを持つ
- ManyToOneアノテーション
@Entity(name = "Person")
public class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
public void addPhone(Phone phone) {
phones.add(phone);
phone.setPerson(this);
}
public void removePhone(Phone phone) {
phones.remove(phone);
phone.setPerson(null);
}
}
@Entity(name = "Phone")
public class Phone {
@Id
private Long id;
@Column(name = "`number`", unique = true)
private String number;
@ManyToOne
private Person person;
}
DDL
create table person (
id bigint not null,
primary key (id)
) engine=InnoDB
create table phone (
id bigint not null,
`number` varchar(255),
person_id bigint,
primary key (id)
) engine=InnoDB
alter table phone
add constraint FKkk6uij3j6wikpnqlj9dymobs9
foreign key (person_id)
references person (id)
双方向のカスケード
(親)Personと(子)Phoneの双方向に関連がある場合です。
単方向の場合と同様に、(親)Personと関連する(子)Phoneをカスケード削除する場合、以下のようにPersonを削除することで関連するPhoneも同時に削除されます。
ただし、この方法でもPhoneを1件ずつ削除するdelete文が発行されます。
Person person = entityManager.find(Person.class, 1L);
entityManager.remove(person);
orphanRemoval属性の用途
OneToManyアノテーションのorphanRemovalという属性(デフォルト値はfalse)で、関連が切り離されたエンティティを削除するかどうかを指定できます。
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Phone> phones = new ArrayList<>();
orphanRemoval=trueの時、以下のようなコードで(親)Personから切り離された(子)Phoneは削除されます。
逆にfalseのときは削除されず、Phoneの外部キーがnullで更新されます。(nullが許可されていれば)
Person person = entityManager.find(Person.class, 1L);
Phone phone = person.getPhones().get(0);
person.removePhone(phone);
entityManager.flush();
OneToOneの例
単方向の関連
(子)Phoneから(親)PhoneDetailsへの単方向の関連です。
このテーブルの性質から考えるとPhoneを子側としているのは不自然ですが、この例では外部キーをPhoneに持たせているためです。
OneToOne関連ではどちらを親とするかはテーブル設計により、プロダクトではこのような設計は行われないと思いますが1パターンとして例示しています。
MySQL リファレンスの外部キー制約の仕様より
外部キー関係には、中央のデータ値を保持している親テーブルと、その元の親を指す同一の値を持っている子テーブルが含まれます。FOREIGN KEY 句は、子テーブルで指定されます。
Entity
- Phoneエンティティ
- 子側 (child side)
- 外部キーを持つ
- OneToOneアノテーション
- PhoneDetailsエンティティ
- 親側 (parent side)
@Entity(name = "Phone")
public class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "details_id")
private PhoneDetails details;
}
@Entity(name = "PhoneDetails")
public class PhoneDetails {
@Id
@GeneratedValue
private Long id;
private String provider;
private String technology;
}
DDL
create table phone (
id bigint not null,
`number` varchar(255),
details_id bigint,
primary key (id)
) engine=InnoDB
create table phone_details (
id bigint not null,
provider varchar(255),
technology varchar(255),
primary key (id)
) engine=InnoDB
alter table phone
add constraint FKso51a1txs2exaklk85pa5g4b5
foreign key (details_id)
references phone_details (id)
単方向のカスケード
(子)Phoneから(親)PhoneDetailsへの単方向の関連です。外部キー(関連の所有者)は子側にあります。
(子)Phoneと関連する(親)PhoneDetailsをカスケード削除する場合、以下のようにPhoneを削除することで関連するPhoneDetailsも同時に削除されます。
(なお、OneToOneアノテーションのcascade属性を指定しないとPhoneだけ削除されます。)
Phone phone = entityManager.find(Phone.class, 2L);
entityManager.remove(phone);
entityManager.flush();
ManyToOneの単方向カスケードで説明したとおり、(子)Phoneからの参照がある状態で(親)PhoneDetailsを削除することはできません。
双方向の関連
(親)Phoneと(子)PhoneDetailsの双方向の関連です。
Entity
- Phoneエンティティ
- 親側 (parent side) or 逆側 (inverse side) or (the mappedBy)
- OneToOneアノテーション
- PhoneDetailsエンティティ
- 子側 (child side) or 所有者側 (owning side only)
- 外部キーを持つ
- OneToOneアノテーション
@Entity(name = "Phone")
public class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@OneToOne(mappedBy = "phone", cascade = CascadeType.ALL)
private PhoneDetails details;
public void addDetails(PhoneDetails details) {
this.details = details;
details.setPhone(this);
}
public void removeDetails() {
if (this.details != null) {
this.details.setPhone(null);
this.details = null;
}
}
}
@Entity(name = "PhoneDetails")
public class PhoneDetails {
@Id
@GeneratedValue
private Long id;
@OneToOne
@JoinColumn(name = "phone_id")
private Phone phone;
private String provider;
private String technology;
}
DDL
create table phone (
id bigint not null,
`number` varchar(255),
primary key (id)
) engine=InnoDB
create table phone_details (
id bigint not null,
provider varchar(255),
technology varchar(255),
phone_id bigint,
primary key (id)
) engine=InnoDB
alter table phone_details
add constraint FK4vs8ep213cifmt8opragp1gkw
foreign key (phone_id)
references phone (id)
双方向のカスケード
(親)Personを削除することで関連する(子)PhoneDetailsもカスケード削除されます。
Phone phone = entityManager.find(Phone.class, 1L);
entityManager.remove(phone);
entityManager.flush();
(子)PhoneDetailsだけ削除するにはorphanRemoval属性をtrueにして
@Entity(name = "Phone")
public class Phone {
@OneToOne(mappedBy = "phone", cascade = CascadeType.ALL, orphanRemoval = true)
private PhoneDetails details;
}
PhoneからPhoneDetailsへの関連を切り離すだけです。
Phone phone = entityManager.find(Phone.class, 1L);
phone.removeDetails();
entityManager.flush();
下記のコードのように(子)PhoneDetailsを直接削除しようとする場合、(子)PhoneDetailsのOneToOneのFetchTypeがEAGERのときは削除できません。(なお、LAZYの場合は削除できます。)
PhoneDetails phoneDetails = entityManager.find(PhoneDetails.class, 2L);
entityManager.remove(phoneDetails);
ただし、(子)PhoneDetailsのOneToOneのcascade属性にALLまたはREMOVEを設定すると(子)PhoneDetailsと(親)Phoneが削除されます。
@Entity(name = "PhoneDetails")
public class PhoneDetails {
@OneToOne(cascade = CascadeType.REMOVE)
@JoinColumn(name = "phone_id")
private Phone phone;
}
EAGERのときに削除できないのが仕様なのかは今のところ調べきれていません。
おそらくPhoneDetailsを読み込んだ時に関連する(親)Phoneも読み込まれているのが関係していると思います。
関連のカスケードと外部キー制約について
基本的なポイントとして、関連アノテーション(OneToManyやOneToOne)のcascade属性の指定と、DBの外部キー制約に設定する参照アクションは意味が異なります。
例えばcascade属性にCascadeType.REMOVEを指定すると外部キー制約に"ON DELETE CASCADE"が適用されるように思うかもしれませんが、Hibernateがエンティティクラスから生成するDDLの外部キー制約に参照アクションを設定しません。
以下の例ではOneToManyのcascade属性にCascadeType.ALLを指定していますが、このエンティティクラスから生成されるDDLの外部キー制約に参照アクションは設定されません。
@Entity(name = "Person")
public class Person {
@Id
private Long id;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Phone> phones = new ArrayList<>();
}
@Entity(name = "Phone")
public class Phone {
@Id
private Long id;
@Column(name = "`number`", unique = true)
private String number;
@ManyToOne
private Person person;
}
追加する外部キー制約に参照アクションが明示されていないのでデフォルトの"on update restrict","on delete restrict"が適用されます。
create table person (
id bigint not null,
primary key (id)
) engine=InnoDB
create table phone (
id bigint not null,
`number` varchar(255),
person_id bigint,
primary key (id)
) engine=InnoDB
alter table phone
add constraint FKkk6uij3j6wikpnqlj9dymobs9
foreign key (person_id)
references person (id)
CascadeTypeについて
関連アノテーションのcascade属性で、親エンティティに対する操作を関連するエンティティへ伝播させるかどうかを指定します。
デフォルトではどの関連アノテーションも操作を伝播させません。
- ALL
- PERSIST,MERGE,REMOVE,REFRESH,DETACHのすべてを有効にする
- PERSIST
- persist操作をカスケードする
- MERGE
- merge操作をカスケードする
- REMOVE
- remove操作をカスケードする
- REFRESH
- refresh操作をカスケードする
- DETACH
- detach操作をカスケードする
以下のような(親)Phoneと(子)PhoneDetailsのエンティティを
@Entity(name = "Phone")
public class Phone {
@Id
@GeneratedValue
private Long id;
@OneToOne(mappedBy = "phone")
private PhoneDetails details;
}
@Entity(name = "PhoneDetails")
public class PhoneDetails {
@Id
@GeneratedValue
private Long id;
@OneToOne
private Phone phone;
}
同時に登録する場合、(親)Phoneの関連にカスケード設定がないのでエラーになります。
PhoneDetails phoneDetails = new PhoneDetails();
Phone phone = new Phone();
phon.setDetails(phoneDetails);
phoneDetails.setPhone(phone);
entityManager.persist(phone);
java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing : com.example.demofeaturelayer.ext.entity.Phone.details -> com.example.demofeaturelayer.ext.entity.PhoneDetails
同時に更新するには(親)PhoneのOneToOne関連のcascade属性にALLかPERSISTを設定します。
@OneToOne(mappedBy = "phone", cascade = CascadeType.PERSIST)
OnDeleteAction
なお、Hibernate固有のOnDeleteアノテーションを使えばDDLに参照アクションで指定することは可能です。(なお、OnUpdateは無いようです)
ただし、Hibernateがエンティティをカスケード削除するときに、DBの外部キー制約のON DELETE CASCADEを利用していません。あくまでも子のエンティティを削除し、親のエンティティを削除という順番で削除を行います。
- NO_ACTION (default)
- CASCADE
@ManyToOne
@JoinColumn(name = "person_id", foreignKey = @ForeignKey(name="fk_person_id"))
@OnDelete(action= OnDeleteAction.CASCADE)
private Person person;
alter table phone
add constraint fk_person_id
foreign key (person_id)
references person (id)
on delete cascade
MySQL 5.7のInnoDBの外部キー制約の参照アクションには次のものがあります。
参照アクション
- ON UPDATE
- ON DELETE
オプション
MySQL 5.7のリファレンスより引用しました。
- CASCADE
- 親テーブルの行を削除または更新し、子テーブル内の一致する行を自動的に削除または更新します。
- SET NULL
- 親テーブルの行を削除または更新し、子テーブル内の 1 つまたは複数の外部キーカラムを NULL に設定します。
- RESTRICT
- 親テーブルに対する削除または更新操作を拒否します。
- デフォルトのオプション
- NO ACTION
- 標準SQLのキーワード。MySQLでは、RESTRICTと同等です。
フェッチ戦略
Hibernate ORM 5.2.14.Final User Guideの[11. Fetching] (http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#fetching)を参考にしました。
FetchTypeとN+1問題
関連アノテーション(OneToManyやManyToOneなど)のfetch属性に指定するFetchTypeとN+1問題について確認しました。
デモ環境
シンプルに2つのエンティティの関連を利用しています。
+--------+ +-------+
| |---[OneToMany]--->| |
| Person | (Cascade) | Phone |
| | | |
| |<---[ManyToOne]---+ |
+--------+ +-------+
Entity
@Entity(name = "Person")
public class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
public void addPhone(Phone phone) {
phones.add(phone);
phone.setPerson(this);
}
public void removePhone(Phone phone) {
phones.remove(phone);
phone.setPerson(null);
}
}
@Entity(name = "Phone")
public class Phone {
@Id
@GeneratedValue
private Long id;
@Column(name = "`number`")
private String number;
@ManyToOne(fetch = FetchType.EAGER) // default EAGER
@JoinColumn(name = "person_id")
private Person person;
}
テストデータ
> select person.id as person_id
, person.name
, phone.id as phone_id
, phone.number
from person left outer join phone on person.id = phone.person_id
order by
person.id
, phone.id;
+-----------+------+----------+----------------+
| person_id | name | phone_id | number |
+-----------+------+----------+----------------+
| 1 | robb | 2 | p1-123-456-789 |
| 1 | robb | 3 | p1-456-789-123 |
| 1 | robb | 4 | p1-789-123-456 |
| 5 | bran | 6 | p2-123-456-789 |
| 7 | jon | 8 | p3-123-456-789 |
| 7 | jon | 9 | p3-456-789-123 |
+-----------+------+----------+----------------+
6 rows in set (0.00 sec)
デモコード
EntityManagerを利用してPerson,Phoneを1件取得または全件取得したときに発行されるSQLを確認します。
1件取得時のコード
Person -> Phone (OneToMany)
Person person = entityManager.createQuery("SELECT p FROM Person p WHERE p.id = :id", Person.class)
.setParameter("id", id)
.getSingleResult();
// エンティティにアクセスするダミーコード
System.out.println("person id : " + person.getId() + " : " + person.getName());
person.getPhones().forEach(phone -> {
System.out.println("phone id : " + phone.getId() + " : " + phone.getNumber());
});
Phone -> Person (ManyToOne)
Phone phone = entityManager.createQuery("SELECT p FROM Phone p WHERE p.id = :id", Phone.class)
.setParameter("id", id)
.getSingleResult();
// エンティティにアクセスするダミーコード
System.out.println("phone id : " + phone.getId() + " : " + phone.getNumber());
System.out.println("person id : " + phone.getPerson().getId() + " : " + phone.getPerson().getName());
全件取得時のコード
Person -> Phone (OneToMany)
List<Person> personList = entityManager.createQuery("SELECT p FROM Person p", Person.class)
.getResultList();
// エンティティにアクセスするダミーコード
personList.forEach(person -> {
System.out.println("person id : " + person.getId() + " : " + person.getName());
person.getPhones().forEach(phone -> {
System.out.println("phone id : " + phone.getId() + " : " + phone.getNumber());
});
});
Phone -> Person (ManyToOne)
List<Phone> phoneList = entityManager.createQuery("SELECT p FROM Phone p", Phone.class)
.getResultList();
// エンティティにアクセスするダミーコード
phoneList.forEach(phone -> {
System.out.println("phone id : " + phone.getId() + " : " + phone.getNumber());
System.out.println("person id : " + phone.getPerson().getId() + " : " + phone.getPerson().getName());
});
PersonとPhoneをカスケード削除するコード
Person person = entityManager.find(Person.class, id);
entityManager.remove(person);
PersonからPhoneを1件削除するコード
Person person = entityManager.find(Person.class, id);
Phone phone = person.getPhones().get(0);
person.removePhone(phone);
Phoneを直接削除するコード
Phone phone = entityManager.find(Phone.class, id);
entityManager.remove(phone);
動作確認
Person -> Phoneを検索 (LAZYなOneToMany)
関連
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
@ManyToOne(fetch = FetchType.LAZY) // default EAGER
@JoinColumn(name = "person_id")
private Person person;
Person 1件取得時に発行されるSQL
Personと関連するPhoneは別々のSQLで検索されます。Phoneとの関連はFetchType.LAZYなのでPhoneにアクセスしたタイミングで2番目のSQL文が発行されます。
なので、アクセスしなければ2番目のSQLは発行されません。
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
where person0_.id=1
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
+--------+----------+
1 row in set (0.00 sec)
/* Phoneにアクセスしたタイミングで発行される */
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=1
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
Person 全件取得時に発行されるSQL
これがN+1問題です。
Personを検索するSQLが1回 + Personに関連するPhoneを検索するSQLがN回実行されます。
Personは3件あるので、この例のデモコードでは合計3 + 1回のSQLが発行されます。
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
| 5 | bran |
| 7 | jon |
+--------+----------+
3 rows in set (0.00 sec)
/* 以下はPhoneにアクセスしたタイミングで発行される */
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=1
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=5
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 5 | 6 | 6 | p2-123-456-789 | 5 |
+----------------+----------+----------+----------------+----------------+
1 row in set (0.00 sec)
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=7
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 7 | 8 | 8 | p3-123-456-789 | 7 |
| 7 | 9 | 9 | p3-456-789-123 | 7 |
+----------------+----------+----------+----------------+----------------+
2 rows in set (0.00 sec)
Phone -> Personを検索 (LAZYなManyToOne)
関連
@ManyToOne(fetch = FetchType.LAZY) // default EAGER
@JoinColumn(name = "person_id")
private Person person;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
Phone 1件取得時に発行されるSQL
Phoneと関連するPersonは別々に検索されます。Personとの関連はFetchType.LAZYなのでPersonにアクセスしたタイミングで2番目のSQL文が発行されます。
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
where phone0_.id=2
+--------+----------------+--------------+
| id1_8_ | number2_8_ | person_i3_8_ |
+--------+----------------+--------------+
| 2 | p1-123-456-789 | 1 |
+--------+----------------+--------------+
1 row in set (0.00 sec)
/* Personにアクセスしたタイミングで発行される */
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=1
+----------+------------+
| id1_7_0_ | name2_7_0_ |
+----------+------------+
| 1 | robb |
+----------+------------+
1 row in set (0.00 sec)
Phone 全件取得時に発行されるSQL
この例のデモコードではSQLの実行回数は合計4回です。その内容はPhoneを検索するSQLが1回、Phoneが紐づくPersonを検索するSQLが3回ですが、SQLの発行が3回で済んでいるのは一次キャッシュが効いているためです。
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
+--------+----------------+--------------+
| id1_8_ | number2_8_ | person_i3_8_ |
+--------+----------------+--------------+
| 2 | p1-123-456-789 | 1 |
| 3 | p1-456-789-123 | 1 | /*← id=1のPersonは1次キャッシュにあるのでSQLは発行されない*/
| 4 | p1-789-123-456 | 1 | /*← 同上*/
| 6 | p2-123-456-789 | 5 |
| 8 | p3-123-456-789 | 7 |
| 9 | p3-456-789-123 | 7 | /*← id=7のPersonは1次キャッシュにあるのでSQLは発行されない*/
+--------+----------------+--------------+
6 rows in set (0.00 sec)
/* 以下はPersonにアクセスしたタイミングで発行される */
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=1
+----------+------------+
| id1_7_0_ | name2_7_0_ |
+----------+------------+
| 1 | robb |
+----------+------------+
1 row in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=5
+----------+------------+
| id1_7_0_ | name2_7_0_ |
+----------+------------+
| 5 | bran |
+----------+------------+
1 row in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=7
+----------+------------+
| id1_7_0_ | name2_7_0_ |
+----------+------------+
| 7 | jon |
+----------+------------+
1 row in set (0.00 sec)
PersonとPhoneをカスケード削除 (LAZYなOneToMany)
Person 1件削除時に発行されるSQL
デモコードのEntityManager.removeでカスケード削除すると、検索時のN+1問題と同じような問題が起きます。
下記のようにPersonに関連するPhoneの削除はidを指定して1件ずつ行われます。
select person0_.id as id1_4_0_, person0_.name as name2_4_0_
from person person0_
where person0_.id=1
select phones0_.person_id as person_i3_5_0_, phones0_.id as id1_5_0_, phones0_.id as id1_5_1_, phones0_.`number` as number2_5_1_, phones0_.person_id as person_i3_5_1_
from phone phones0_
where phones0_.person_id=1
delete from phone where id=2
delete from phone where id=3
delete from phone where id=4
delete from person where id=1
削除処理を最適化するにはJPQLでdelete文を実行します。
entityManager.createQuery("DELETE FROM Person p WHERE p.id = :id")
.setParameter("id", id)
.executeUpdate();
こうすると発行されるSQLは下記の1つだけになります。ただしこの方法が有効なのはphoneテーブルの外部キーに"ON DELETE CASCADE"アクションが付いている場合です。つまりperson削除時にphoneをカスケード削除しているのはhibernateではなくDBということになります。
delete from person where id=1
注意する点としてエンティティクラスの実装からテーブルを生成している場合、デフォルトでは外部キーに"ON DELETE CASCADE"アクションは付かない点です。
以下のようにHibernate固有のOnDeleteアノテーションを付けることで外部キーに"ON DELETE CASCADE"アクションが付いたテーブルが生成されます。
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // default LAZY
@OnDelete(action= OnDeleteAction.CASCADE)
private List<Phone> phones = new ArrayList<>();
PersonからPhoneを1件削除 (LAZYなOneToMany)
Phone 1件削除時に発行されるSQL
select person0_.id as id1_4_0_, person0_.name as name2_4_0_
from person person0_
where person0_.id=1
select phones0_.person_id as person_i3_5_0_, phones0_.id as id1_5_0_, phones0_.id as id1_5_1_, phones0_.`number` as number2_5_1_, phones0_.person_id as person_i3_5_1_
from phone phones0_
where phones0_.person_id=1
delete from phone where id=2
Phoneを直接削除
Phone 1件削除時に発行されるSQL
select phone0_.id as id1_5_0_, phone0_.`number` as number2_5_0_, phone0_.person_id as person_i3_5_0_
from phone phone0_
where phone0_.id=2
delete from phone where id=2
Person -> Phoneを検索 (EAGERなOneToMany)
関連
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.EAGER) // default LAZY
private List<Phone> phones = new ArrayList<>();
@ManyToOne(fetch = FetchType.EAGER) // default EAGER
@JoinColumn(name = "person_id")
private Person person;
Person 1件取得時に発行されるSQL
FetchType.LAZYのときと同じSQLが発行されます。(*下記に補足あり)
Phoneとの関連がFetchType.EAGERであってもPersonとPhoneを検索するSQLは別々に発行されます。
EAGERではPhoneへのアクセスがあるかないかに関わらず2番目のSQLが発行されます。
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
where person0_.id=1
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
+--------+----------+
1 row in set (0.00 sec)
/* 以下はPhoneへのアクセスの有無に関わらず発行される */
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=1
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
補足
上記の結果は検索にJPQLを使用したものですが、EAGERのときでEntityManager.findを使った検索の場合、発行されるSQLが変わります。
Person person = entityManager.find(Person.class, id);
select person0_.id as id1_4_0_, person0_.name as name2_4_0_, phones1_.person_id as person_i3_5_1_, phones1_.id as id1_5_1_, phones1_.id as id1_5_2_, phones1_.`number` as number2_5_2_, phones1_.person_id as person_i3_5_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=1
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_4_0_ | name2_4_0_ | person_i3_5_1_ | id1_5_1_ | id1_5_2_ | number2_5_2_ | person_i3_5_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 1 | robb | 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | robb | 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | robb | 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------+------------+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
Person 全件取得時に発行されるSQL
FetchType.LAZYのときと同じSQLが発行されます。
1件取得時とおなじでPhoneへのアクセスの有無に関わらず2番目以降のSQLが発行されます。
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
| 5 | bran |
| 7 | jon |
+--------+----------+
3 rows in set (0.00 sec)
/* 以下はPhoneへのアクセスの有無に関わらず発行される */
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=7
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 7 | 8 | 8 | p3-123-456-789 | 7 |
| 7 | 9 | 9 | p3-456-789-123 | 7 |
+----------------+----------+----------+----------------+----------------+
2 rows in set (0.00 sec)
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=5
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 5 | 6 | 6 | p2-123-456-789 | 5 |
+----------------+----------+----------+----------------+----------------+
1 row in set (0.00 sec)
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=1
+----------------+----------+----------+----------------+----------------+
| person_i3_8_0_ | id1_8_0_ | id1_8_1_ | number2_8_1_ | person_i3_8_1_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
Phone -> Personを検索 (EAGERなManyToOne)
関連
@ManyToOne(fetch = FetchType.EAGER) // default EAGER
@JoinColumn(name = "person_id")
private Person person;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.EAGER) // default LAZY
private List<Phone> phones = new ArrayList<>();
Phone 1件取得時に発行されるSQL
FetchType.LAZYのときとは異なるSQLが発行されます。
Personを検索するSQLでPhoneとの結合にleft outer joinが使われています。これはPersonエンティティのPhoneへの関連がEAGERとなっているためです。LAZYに変えるとjoinは行われません。
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
where phone0_.id=2
+--------+----------------+--------------+
| id1_8_ | number2_8_ | person_i3_8_ |
+--------+----------------+--------------+
| 2 | p1-123-456-789 | 1 |
+--------+----------------+--------------+
1 row in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_, phones1_.person_id as person_i3_8_1_, phones1_.id as id1_8_1_, phones1_.id as id1_8_2_, phones1_.`number` as number2_8_2_, phones1_.person_id as person_i3_8_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=1
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_7_0_ | name2_7_0_ | person_i3_8_1_ | id1_8_1_ | id1_8_2_ | number2_8_2_ | person_i3_8_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 1 | robb | 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | robb | 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | robb | 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------+------------+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
PersonエンティティのPhoneへの関連をLAZYにした場合
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
where phone0_.id=2
select person0_.id as id1_7_0_, person0_.name as name2_7_0_ from person person0_
where person0_.id=1
Phone 全件取得時に発行されるSQL
1件取得時と同様にPersonを検索するSQLでPhoneとの結合にleft outer joinが使われています。同じようにEAGERをLAZYに変えることでjoinは行われなくなります。
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
+--------+----------------+--------------+
| id1_8_ | number2_8_ | person_i3_8_ |
+--------+----------------+--------------+
| 2 | p1-123-456-789 | 1 |
| 3 | p1-456-789-123 | 1 |
| 4 | p1-789-123-456 | 1 |
| 6 | p2-123-456-789 | 5 |
| 8 | p3-123-456-789 | 7 |
| 9 | p3-456-789-123 | 7 |
+--------+----------------+--------------+
6 rows in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_, phones1_.person_id as person_i3_8_1_, phones1_.id as id1_8_1_, phones1_.id as id1_8_2_, phones1_.`number` as number2_8_2_, phones1_.person_id as person_i3_8_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=1
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_7_0_ | name2_7_0_ | person_i3_8_1_ | id1_8_1_ | id1_8_2_ | number2_8_2_ | person_i3_8_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 1 | robb | 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | robb | 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | robb | 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------+------------+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_, phones1_.person_id as person_i3_8_1_, phones1_.id as id1_8_1_, phones1_.id as id1_8_2_, phones1_.`number` as number2_8_2_, phones1_.person_id as person_i3_8_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=5
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_7_0_ | name2_7_0_ | person_i3_8_1_ | id1_8_1_ | id1_8_2_ | number2_8_2_ | person_i3_8_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 5 | bran | 5 | 6 | 6 | p2-123-456-789 | 5 |
+----------+------------+----------------+----------+----------+----------------+----------------+
1 row in set (0.00 sec)
select person0_.id as id1_7_0_, person0_.name as name2_7_0_, phones1_.person_id as person_i3_8_1_, phones1_.id as id1_8_1_, phones1_.id as id1_8_2_, phones1_.`number` as number2_8_2_, phones1_.person_id as person_i3_8_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=7
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_7_0_ | name2_7_0_ | person_i3_8_1_ | id1_8_1_ | id1_8_2_ | number2_8_2_ | person_i3_8_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 7 | jon | 7 | 8 | 8 | p3-123-456-789 | 7 |
| 7 | jon | 7 | 9 | 9 | p3-456-789-123 | 7 |
+----------+------------+----------------+----------+----------+----------------+----------------+
2 rows in set (0.00 sec)
PersonエンティティのPhoneへの関連をLAZYにした場合
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true,
fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
select phone0_.id as id1_8_, phone0_.`number` as number2_8_, phone0_.person_id as person_i3_8_
from phone phone0_
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=1
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=5
select person0_.id as id1_7_0_, person0_.name as name2_7_0_
from person person0_
where person0_.id=7
PersonとPhoneをカスケード削除 (EAGERなOneToMany)
Phone 1件削除時に発行されるSQL
select person0_.id as id1_4_0_, person0_.name as name2_4_0_, phones1_.person_id as person_i3_5_1_, phones1_.id as id1_5_1_, phones1_.id as id1_5_2_, phones1_.`number` as number2_5_2_, phones1_.person_id as person_i3_5_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=1
delete from phone where id=2
delete from phone where id=3
delete from phone where id=4
delete from person where id=1
PersonからPhoneを1件削除 (EAGERなOneToMany)
Phone 1件削除時に発行されるSQL
select person0_.id as id1_4_0_, person0_.name as name2_4_0_, phones1_.person_id as person_i3_5_1_, phones1_.id as id1_5_1_, phones1_.id as id1_5_2_, phones1_.`number` as number2_5_2_, phones1_.person_id as person_i3_5_2_ from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id where person0_.id=1
delete from phone where id=2
Phoneを直接削除
Phone 1件削除時に発行されるSQL
FetchTypeがEAGERのときは削除できません。
下記のようにdelete文の代わりにselect文が発行されます。
LAZYの場合は削除できます。
select phone0_.id as id1_5_0_, phone0_.`number` as number2_5_0_, phone0_.person_id as person_i3_5_0_, person1_.id as id1_4_1_, person1_.name as name2_4_1_
from phone phone0_ left outer join person person1_ on phone0_.person_id=person1_.id
where phone0_.id=2
+----------+----------------+----------------+----------+------------+
| id1_5_0_ | number2_5_0_ | person_i3_5_0_ | id1_4_1_ | name2_4_1_ |
+----------+----------------+----------------+----------+------------+
| 2 | p1-123-456-789 | 1 | 1 | robb |
+----------+----------------+----------------+----------+------------+
1 row in set (0.00 sec)
select phones0_.person_id as person_i3_5_0_, phones0_.id as id1_5_0_, phones0_.id as id1_5_1_, phones0_.`number` as number2_5_1_, phones0_.person_id as person_i3_5_1_
from phone phones0_
where phones0_.person_id=1
+----------------+----------+----------+----------------+----------------+
| person_i3_5_0_ | id1_5_0_ | id1_5_1_ | number2_5_1_ | person_i3_5_1_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
N+1問題の対応
以下の方法で確認しました。
- Join Fetch
- Fetchアノテーション
- BatchSizeアノテーション
- EntityGraphアノテーション
Join Fetch
N+1問題の解決でよく紹介されている方法で、JPQLで記述するselect文にjoin fetchを使用します。
Person -> Phoneを (LAZYなOneToMany)
List<Person> personList = entityManager.createQuery("SELECT DISTINCT p FROM Person p JOIN FETCH p.phones", Person.class)
.getResultList();
Person 全件取得時に発行されるSQL
このデモでは関連するエンティティがjoinされSQLの発行は1回で済みます。
select distinct person0_.id as id1_7_0_, phones1_.id as id1_8_1_, person0_.name as name2_7_0_, phones1_.`number` as number2_8_1_, phones1_.person_id as person_i3_8_1_, phones1_.person_id as person_i3_8_0__, phones1_.id as id1_8_0__
from person person0_ inner join phone phones1_ on person0_.id=phones1_.person_id
+----------+----------+------------+----------------+----------------+-----------------+-----------+
| id1_7_0_ | id1_8_1_ | name2_7_0_ | number2_8_1_ | person_i3_8_1_ | person_i3_8_0__ | id1_8_0__ |
+----------+----------+------------+----------------+----------------+-----------------+-----------+
| 1 | 2 | robb | p1-123-456-789 | 1 | 1 | 2 |
| 1 | 3 | robb | p1-456-789-123 | 1 | 1 | 3 |
| 1 | 4 | robb | p1-789-123-456 | 1 | 1 | 4 |
| 5 | 6 | bran | p2-123-456-789 | 5 | 5 | 6 |
| 7 | 8 | jon | p3-123-456-789 | 7 | 7 | 8 |
| 7 | 9 | jon | p3-456-789-123 | 7 | 7 | 9 |
+----------+----------+------------+----------------+----------------+-----------------+-----------+
6 rows in set (0.00 sec)
Phone -> Personを検索 (LAZYなManyToOne)
List<Phone> phoneList = entityManager.createQuery("SELECT p FROM Phone p JOIN FETCH p.person", Phone.class)
.getResultList();
Phone 全件取得時に発行されるSQL
こちらのデモもSQLの発行は1回で済んでいますが、fetchしていないエンティティにアクセスすると、その分だけ別途SQLが発行されます。
select phone0_.id as id1_8_0_, person1_.id as id1_7_1_, phone0_.`number` as number2_8_0_, phone0_.person_id as person_i3_8_0_, person1_.name as name2_7_1_
from phone phone0_ inner join person person1_ on phone0_.person_id=person1_.id
+----------+----------+----------------+----------------+------------+
| id1_8_0_ | id1_7_1_ | number2_8_0_ | person_i3_8_0_ | name2_7_1_ |
+----------+----------+----------------+----------------+------------+
| 2 | 1 | p1-123-456-789 | 1 | robb |
| 3 | 1 | p1-456-789-123 | 1 | robb |
| 4 | 1 | p1-789-123-456 | 1 | robb |
| 6 | 5 | p2-123-456-789 | 5 | bran |
| 8 | 7 | p3-123-456-789 | 7 | jon |
| 9 | 7 | p3-456-789-123 | 7 | jon |
+----------+----------+----------------+----------------+------------+
6 rows in set (0.00 sec)
たとえば、以下のコードのようにPersonに紐づくPhoneエンティティにアクセスすると、
List<Phone> phoneList = entityManager.createQuery("SELECT p FROM Phone p JOIN FETCH p.person", Phone.class).getResultList();
phoneList.forEach(phone -> {
// ↓このgetPhonesで新たにSQLが発行される
phone.getPerson().getPhones().forEach(p -> {
System.out.println("other phone id : " + p.getId() + " : " + p.getNumber());
});
}
新たに3回SQLが発行されます。
select phone0_.id as id1_8_0_, person1_.id as id1_7_1_, phone0_.`number` as number2_8_0_, phone0_.person_id as person_i3_8_0_, person1_.name as name2_7_1_
from phone phone0_ inner join person person1_ on phone0_.person_id=person1_.id
/* 以下は追加で発行されるSQL */
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=7
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=5
select phones0_.person_id as person_i3_8_0_, phones0_.id as id1_8_0_, phones0_.id as id1_8_1_, phones0_.`number` as number2_8_1_, phones0_.person_id as person_i3_8_1_
from phone phones0_
where phones0_.person_id=1
Fetchアノテーション
Hibernate固有のFetchアノテーションを指定する方法です。
FetchMode.SUBSELECTを指定すると関連するエンティティの取得にサブクエリを使うようになります。
なお、FetchMode.JOINを指定すればJPQLのjoin fetchと同じ結果になると思うのですが、私の試した環境では期待通りに動きませんでした。(joinされない)
Person -> Phoneを検索 (LAZYなOneToMany)
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // default LAZY
@Fetch(FetchMode.SUBSELECT)
private List<Phone> phones = new ArrayList<>();
Person 全件取得時に発行されるSQL
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
| 5 | bran |
| 7 | jon |
+--------+----------+
3 rows in set (0.00 sec)
select phones0_.person_id as person_i3_8_1_, phones0_.id as id1_8_1_, phones0_.id as id1_8_0_, phones0_.`number` as number2_8_0_, phones0_.person_id as person_i3_8_0_
from phone phones0_ where phones0_.person_id in (select person0_.id from person person0_)
+----------------+----------+----------+----------------+----------------+
| person_i3_8_1_ | id1_8_1_ | id1_8_0_ | number2_8_0_ | person_i3_8_0_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
| 5 | 6 | 6 | p2-123-456-789 | 5 |
| 7 | 8 | 8 | p3-123-456-789 | 7 |
| 7 | 9 | 9 | p3-456-789-123 | 7 |
+----------------+----------+----------+----------------+----------------+
6 rows in set (0.00 sec)
Phone -> Personを検索 (LAZYなManyToOne)
FetchMode.SUBSELECTのJavaDocに記載されている通り、コレクションに対して有効なためManyToOneの関連では指定できません。
Available for collections only. When accessing a non-initialized collection, this fetch mode will trigger loading all elements of all collections of the same role for all owners associated with the persistence context using a single secondary select.
BatchSizeアノテーション
Hibernate固有のBatchSizeアノテーションを指定する方法です。
in句を使ったselect文を実行します。in句に指定する値の数はsize属性で指定します。この例では2を指定しているのでin句に最大2つのIDを使用します。
Person -> Phoneを検索 (LAZYなOneToMany)
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
@BatchSize(size = 2)
private List<Phone> phones = new ArrayList<>();
Person 全件取得時に発行されるSQL
select person0_.id as id1_7_, person0_.name as name2_7_
from person person0_
+--------+----------+
| id1_7_ | name2_7_ |
+--------+----------+
| 1 | robb |
| 5 | bran |
| 7 | jon |
+--------+----------+
3 rows in set (0.00 sec)
select phones0_.person_id as person_i3_8_1_, phones0_.id as id1_8_1_, phones0_.id as id1_8_0_, phones0_.`number` as number2_8_0_, phones0_.person_id as person_i3_8_0_
from phone phones0_
where phones0_.person_id in (1, 5)
+----------------+----------+----------+----------------+----------------+
| person_i3_8_1_ | id1_8_1_ | id1_8_0_ | number2_8_0_ | person_i3_8_0_ |
+----------------+----------+----------+----------------+----------------+
| 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | 4 | 4 | p1-789-123-456 | 1 |
| 5 | 6 | 6 | p2-123-456-789 | 5 |
+----------------+----------+----------+----------------+----------------+
4 rows in set (0.00 sec)
select phones0_.person_id as person_i3_8_1_, phones0_.id as id1_8_1_, phones0_.id as id1_8_0_, phones0_.`number` as number2_8_0_, phones0_.person_id as person_i3_8_0_
from phone phones0_
where phones0_.person_id=7
+----------------+----------+----------+----------------+----------------+
| person_i3_8_1_ | id1_8_1_ | id1_8_0_ | number2_8_0_ | person_i3_8_0_ |
+----------------+----------+----------+----------------+----------------+
| 7 | 8 | 8 | p3-123-456-789 | 7 |
| 7 | 9 | 9 | p3-456-789-123 | 7 |
+----------------+----------+----------+----------------+----------------+
2 rows in set (0.00 sec)
Phone -> Personを検索 (LAZYなManyToOne)
ManyToOneの関連では指定しても意味が無いようで、指定してもしなくてもSQLの実行内容に違いはありませんでした。
default_batch_fetch_size
BatchSizeアノテーションの代わりにdefault_batch_fetch_sizeプロパティで指定することも可能です。
(アノテーションはクエリを選ぶことができますが、プロパティはすべてのクエリに影響がでるので注意が必要です。)
spring:
jpa:
properties:
hibernate:
default_batch_fetch_size: 4
hibernate.default_batch_fetch_size (e.g. 4,8, or 16)
Default size for Hibernate Batch fetching of associations (lazily fetched associations can be fetched in batches to prevent N+1 query problems).
EntityGraphアノテーション
JPA 2.1より導入されたEntityGraphアノテーションを使う方法です。
EntityGraphではクエリ毎にFetchTypeを選択することができます。エンティティクラスの関連は常にLAZYにしておき、必要に応じてFetchType.EAGERでクエリを実行するということが可能です。
Entity
@Entity(name = "Person")
@NamedEntityGraph(name = "Person.phones",
attributeNodes = @NamedAttributeNode("phones")
)
public class Person {
@Id
@GeneratedValue
private Long id;
@Column(name = "name")
private String name;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY) // default LAZY
private List<Phone> phones = new ArrayList<>();
}
Repository
EntityGraphType
- FETCH
- NamedAttributeNodeで指定した属性はFetchType.EAGERとして扱う。
- 指定のない属性はエンティティクラスで指定するFetchTypeに準ずる。
- LOAD
- NamedAttributeNodeで指定した属性はFetchType.EAGERとして扱う。
- 指定のない属性はFetchType.LAZYとして扱う。
public interface PersonRepository extends JpaRepository<Person, Long> {
@EntityGraph(value = "Person.phones", type = EntityGraph.EntityGraphType.FETCH)
@Override
Optional<Person> findById(Long id);
@EntityGraph(value = "Person.phones", type = EntityGraph.EntityGraphType.FETCH)
@Override
List<Person> findAll();
}
Person -> Phoneを検索 (LAZYなOneToMany)
エンティティクラスの関連ではLAZYとなっていますが、フェッチ時はEAGERとして扱われます。
Person 1件取得時に発行されるSQL
Person person = personRepository.findById(1L).get();
select person0_.id as id1_4_0_, person0_.name as name2_4_0_, phones1_.person_id as person_i3_5_1_, phones1_.id as id1_5_1_, phones1_.id as id1_5_2_, phones1_.`number` as number2_5_2_, phones1_.person_id as person_i3_5_2_
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
where person0_.id=1
+----------+------------+----------------+----------+----------+----------------+----------------+
| id1_4_0_ | name2_4_0_ | person_i3_5_1_ | id1_5_1_ | id1_5_2_ | number2_5_2_ | person_i3_5_2_ |
+----------+------------+----------------+----------+----------+----------------+----------------+
| 1 | robb | 1 | 2 | 2 | p1-123-456-789 | 1 |
| 1 | robb | 1 | 3 | 3 | p1-456-789-123 | 1 |
| 1 | robb | 1 | 4 | 4 | p1-789-123-456 | 1 |
+----------+------------+----------------+----------+----------+----------------+----------------+
3 rows in set (0.00 sec)
Person 全件取得時に発行されるSQL
1件取得時と同様にEAGERとして扱われます。
List<Person> personList = personRepository.findAll();
select person0_.id as id1_4_0_, phones1_.id as id1_5_1_, person0_.name as name2_4_0_, phones1_.`number` as number2_5_1_, phones1_.person_id as person_i3_5_1_, phones1_.person_id as person_i3_5_0__, phones1_.id as id1_5_0__
from person person0_ left outer join phone phones1_ on person0_.id=phones1_.person_id
+----------+----------+------------+----------------+----------------+-----------------+-----------+
| id1_4_0_ | id1_5_1_ | name2_4_0_ | number2_5_1_ | person_i3_5_1_ | person_i3_5_0__ | id1_5_0__ |
+----------+----------+------------+----------------+----------------+-----------------+-----------+
| 1 | 2 | robb | p1-123-456-789 | 1 | 1 | 2 |
| 1 | 3 | robb | p1-456-789-123 | 1 | 1 | 3 |
| 1 | 4 | robb | p1-789-123-456 | 1 | 1 | 4 |
| 5 | 6 | bran | p2-123-456-789 | 5 | 5 | 6 |
| 7 | 8 | jon | p3-123-456-789 | 7 | 7 | 8 |
| 7 | 9 | jon | p3-456-789-123 | 7 | 7 | 9 |
+----------+----------+------------+----------------+----------------+-----------------+-----------+
6 rows in set (0.00 sec)
Join Fetchを使わない方が効率がよい場合
join fetchはN+1問題の効果的な解決方法ですが、場合によってはjoin fetchを使用しない方がよいという状況も存在します。
例えば、Nの部分のクエリに1次キャッシュが有効な場合などが考えられます。
EAGERについて
Hibernate ORM 5.2.14.Final User Guideの[25.6. Fetching]
(http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#best-practices-fetching)で触れられていますが、FetchType.EAGERは"bad choice"ということです。
EAGER fetching is almost always a bad choice.
少し複雑な関連のデモ
別記事にしました。
[少し複雑な関連のデモ(JPA 2.1のおさらいメモの補足)] (https://qiita.com/rubytomato@github/items/6e891513178de078a83e)
キャッシュ
Hibernate ORM 5.2.14.Final User Guideの[13. Caching] (http://docs.jboss.org/hibernate/orm/5.2/userguide/html_single/Hibernate_User_Guide.html#caching)を参考にしました。
キャッシュの種類
Hibernateのキャッシュには1次キャッシュ(First Level Cache/L1C)と2次キャッシュ(Second Level Cache/L2C)があります。
- First Level Cache (L1C)
- デフォルトで有効
- 永続コンテキスト(EntityManager or Session)で管理
- 永続コンテキストの終了時にキャッシュは破棄
- Second Level Cache (L2C)
- デフォルトで無効
- 有効にするには別途キャッシングプロバイダが必要
- 同じセッションファクトリの永続コンテキストで共有される
First Level Cache
永続コンテキスト(PersistenceContext)にロードされたエンティティはキャッシュとしても利用されます。
永続コンテキスト上に存在するエンティティを検索した場合、SQLは発行されません。
キャッシュされたエンティティはEntityManager.clearで永続コンテキスト上からクリアすることができます。
Second Level Cacheの設定方法
Hibernateが利用できるキャッシングプロバイダには以下のものがあります。
- JCache
- Ehcache
- Infinispan
この例ではEhcacheを利用します。
依存関係の追加
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>5.2.14.Final</version>
</dependency>
コンフィグレーション
spring:
jpa:
properties:
hibernate:
cache:
use_second_level_cache: true
use_query_cache: true
region:
factory_class: org.hibernate.cache.ehcache.EhCacheRegionFactory
javax:
persistence:
sharedCache:
mode: ENABLE_SELECTIVE
sharedCache.mode
選択できるモードは次の4つです。
- ENABLE_SELECTIVE (Default and recommended value)
- Cacheableアノテーションが付いているエンティティをキャッシュする
- DISABLE_SELECTIVE
- キャッシュ不可のアノテーションが付いていないエンティティをキャッシュする
- キャッシュ不可は@Cacheable(false)でマークします
- ALL
- アノテーションの有無に関わらずエンティティをキャッシュする
- NONE
- キャッシュしない(2次キャッシュを無効とする)
エンティティキャッシュ
キャッシュしたいエンティティにCacheableアノテーションを付けます。
@Entity(name = "Person")
@Cacheable(true)
public class Person {
// ...省略
}
クエリキャッシュ
JPQLの結果をキャッシュしたい場合はクエリにヒントを指定します。
Person person = entityManager.createQuery("SELECT p FROM Person p WHERE p.id = :id", Person.class)
.setParameter("id", id)
.setHint("org.hibernate.cacheable", true)
.setHint("org.hibernate.cacheMode", "NORMAL")
.getSingleResult();
CacheModeについて
- NORMAL
- エンティティをキャッシュへ読み書きする。
- IGNORE
- エンティティをキャッシュへ読み書きしない。
- GET
- エンティティをキャッシュから読み取るが、書き込みは行わない。
- PUT
- エンティティをキャッシュへ書き込むが、読み取りは行わない。
- REFRESH
- エンティティをキャッシュへ書き込むが、読み取りは行わない。
- キャッシュのリフレッシュを強制するために"hibernate.cache.use_minimal_puts"パラメータの設定は無視される。
ログ
ログの他に後述する統計情報にも2次キャッシュの情報が出力されます。
logging:
level:
org.hibernate.cache: DEBUG
出力例
Checking cached query results in region: org.hibernate.cache.internal.StandardQueryCache
key: sql: select person0_.id as id1_4_, person0_.name as name2_4_ from person person0_ where person0_.id=?; parameters: ; named parameters: {id=1}; transformer: org.hibernate.transform.CacheableResultTransformer@110f2
Checking query spaces are up-to-date: [person]
key: person
Element for key person is null
Returning cached query results
補足
JPA annotation
association
要注意なのはFetchTypeのデフォルトがアノテーションによって異なる点です。
OneToOneとManyToOneはデフォルトがEAGERになっています。
OneToOne
attribute | type | default |
---|---|---|
targetEntity | Class | void.class |
cascade | CascadeType[] | {} |
fetch | FetchType | EAGER |
optional | boolean | true |
mappedBy | String | "" |
orphanRemoval | boolean | false |
OneToMany
attribute | type | default |
---|---|---|
targetEntity | Class | void.class |
cascade | CascadeType[] | {} |
fetch | FetchType | LAZY |
mappedBy | String | "" |
orphanRemoval | boolean | false |
ManyToOne
attribute | type | default |
---|---|---|
targetEntity | Class | void.class |
cascade | CascadeType[] | {} |
fetch | FetchType | EAGER |
optional | boolean | true |
ManyToMany
attribute | type | default |
---|---|---|
targetEntity | Class | void.class |
cascade | CascadeType[] | {} |
fetch | FetchType | LAZY |
mappedBy | String | "" |
統計情報をログに出力する
Hibernateのコンフィグレーションに統計情報を出力するためのgenerate_statisticsパラメータがあります。
このパラメータを有効にすることでSQLの実行回数や実行にかかった時間、2次キャッシュの利用の有無などがわかります。
spring:
jpa:
properties:
hibernate:
generate_statistics: true
このパラメータを有効にするとログに以下のようなメトリクスが出力されます。
"spent preparing xxx JDBC statements"や"spent executing xxx JDBC statements"のxxxがSQLの実行回数ですが、思っていたより多い数値が出力されていれば、想定していないSQL文が発行されているの可能性があります。
Session Metrics {
1001050 nanoseconds spent acquiring 1 JDBC connections;
0 nanoseconds spent releasing 0 JDBC connections;
38230273 nanoseconds spent preparing 4 JDBC statements;
9335433 nanoseconds spent executing 4 JDBC statements;
0 nanoseconds spent executing 0 JDBC batches;
0 nanoseconds spent performing 0 L2C puts;
0 nanoseconds spent performing 0 L2C hits;
0 nanoseconds spent performing 0 L2C misses;
0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
75228 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}
JpaRepositoryのメソッド呼び出しをログに出力する
[Spring Data JPA - Reference Documentation] (https://docs.spring.io/spring-data/jpa/docs/2.0.4.RELEASE/reference/html/#_common)のAppendix E: Frequently asked questionsで紹介されている方法です。
下記のコンフィグレーションを実装します。
@Bean
public CustomizableTraceInterceptor interceptor() {
CustomizableTraceInterceptor interceptor = new CustomizableTraceInterceptor();
interceptor.setEnterMessage("Entering $[methodName]($[arguments]).");
interceptor.setExitMessage("Leaving $[methodName](..) with return value $[returnValue], took $[invocationTime]ms.");
return interceptor;
}
@Bean
public Advisor traceAdvisor() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression("execution(public * org.springframework.data.jpa.repository.JpaRepository+.*(..))");
return new DefaultPointcutAdvisor(pointcut, interceptor());
}
ログ出力の設定をapplication.ymlに記述します。
logging:
level:
org.springframework.aop.interceptor.CustomizableTraceInterceptor: TRACE
JpaRepositoryのメソッド呼び出しが行われると、呼び出し時の引数と戻り値がTRACEログに出力されます。
下記はfindByIdメソッドを呼び出した時のログです。
TRACE 6568 --- [nio-9000-exec-1] o.s.a.i.CustomizableTraceInterceptor : Entering findById(126).
// 省略
TRACE 6568 --- [nio-9000-exec-1] o.s.a.i.CustomizableTraceInterceptor : Leaving findById(..) with return value Optional[com.example.demofeaturelayer.user.entity.User@1ae9a800], took 67ms.
参考になったstack overflowの記事
古めの記事が多いですがとても参考になった記事です。
- [JPA eager fetch does not join] (https://stackoverflow.com/questions/463349/jpa-eager-fetch-does-not-join) (2009/01/20)
- [In a bidirectional JPA OneToMany/ManyToOne association, what is meant by “the inverse side of the association”?] (https://stackoverflow.com/questions/2584521/in-a-bidirectional-jpa-onetomany-manytoone-association-what-is-meant-by-the-in) (2010/04/06)
- [What is the “owning side” in an ORM mapping?] (https://stackoverflow.com/questions/2749689/what-is-the-owning-side-in-an-orm-mapping) (2010/05/01)
- [Difference between FetchType LAZY and EAGER in Java Persistence API?] (https://stackoverflow.com/questions/2990799/difference-between-fetchtype-lazy-and-eager-in-java-persistence-api) (2010/06/07)
- [JPA 2.0 orphanRemoval=true VS on delete Cascade] (https://stackoverflow.com/questions/4329577/jpa-2-0-orphanremoval-true-vs-on-delete-cascade) (2010/12/01)
- [Hibernate cannot simultaneously fetch multiple bags] (https://stackoverflow.com/questions/4334970/hibernate-cannot-simultaneously-fetch-multiple-bags) (2010/12/02)
- [Hibernate ORMHHH-1718 | Have multiple bag fetches revert to subselect fetching for all but one of the bags] (https://hibernate.atlassian.net/browse/HHH-1718)
- [JPA JoinColumn vs mappedBy] (https://stackoverflow.com/questions/11938253/jpa-joincolumn-vs-mappedby) (2012/08/13)