【Spring Boot×JpaRepository×MySQL】Hibernate Toolsで生成したVIEW由来のEntityクラスの使い方

Spring BootとJpaRepositoryを使ってEclipseのHibernate Toolsで生成したVIEW由来のEntityクラスの使い方です。
使用するデータベースはMySQLです。
テーブル由来のEntityクラスと違って注意すべき点がいくつかあるので、そのことについてまとめました。

## 環境
- OS：Windows10
- IDE：Eclipse2020-03
- Java：8
- MySQL：5.7
- Spring Boot：2.3.1
- Hibernate ORM：5.4

## VIEW由来のEntityクラス
### 基本的な内容
Hibernate Toolsで生成したVIEW由来のEntityクラスを使うには、

1. 主キーにあたるカラムに@Idを追記する
2. クラス名の上に@Immutableを追記する

必要があります。

主キーにあたるカラムがshop_idであるshop_informationsビュー由来のEntityクラスであるShopInformation.javaの場合は以下の通り。

```java:ShopInformation.java

package jp.co.hibernate_exchange_with_mysql.domain.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

import org.hibernate.annotations.Immutable;


/**
 * The persistent class for the shop_informations database table.
 *
 */
@Entity
@Immutable // 追記
@Table(name="shop_informations")
@NamedQuery(name="ShopInformation.findAll", query="SELECT s FROM ShopInformation s")
public class ShopInformation implements Serializable {
	private static final long serialVersionUID = 1L;

	private String address;

	@Column(name="category_name")
	private String categoryName;

	@Id // 追記
	@Column(name="shop_id", updatable = false, nullable = false) // 追記（任意）
	private Integer shopId;

	@Column(name="shop_name")
	private String shopName;

	private String tel;

	@Column(name="zip_code")
	private String zipCode;

	public ShopInformation() {
	}

	public String getAddress() {
		return this.address;
	}

	public void setAddress(String address) {
		this.address = address;
	}

	public String getCategoryName() {
		return this.categoryName;
	}

	public void setCategoryName(String categoryName) {
		this.categoryName = categoryName;
	}

	public Integer getShopId() {
		return this.shopId;
	}

	public void setShopId(Integer shopId) {
		this.shopId = shopId;
	}

	public String getShopName() {
		return this.shopName;
	}

	public void setShopName(String shopName) {
		this.shopName = shopName;
	}

	public String getTel() {
		return this.tel;
	}

	public void setTel(String tel) {
		this.tel = tel;
	}

	public String getZipCode() {
		return this.zipCode;
	}

	public void setZipCode(String zipCode) {
		this.zipCode = zipCode;
	}

}
```
それぞれの項目について、解説していきます。
#### 1. 主キーにあたるカラムに@Idを追記する
MySQLの場合、VIEWに主キーを定義できないため、Hibernate ORMでEntityクラスを生成した時点では@Idは付いていません（テーブルの場合は付与されている）。そのため、手動で追記してあげる必要があります。
この@Idを追記しないと、起動時に以下のようなエラーが出て落ちてしまいます。

```
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'shopInformationRepository' defined in jp.co.hibernate_exchange_with_mysql.domain.repository.ShopInformationRepository defined in @EnableJpaRepositories declared on JpaRepositoriesRegistrar.EnableJpaRepositoriesConfiguration: Cannot resolve reference to bean 'jpaMappingContext' while setting bean property 'mappingContext'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaMappingContext': Invocation of init method failed; nested exception is org.hibernate.AnnotationException: No identifier specified for entity: jp.co.hibernate_exchange_with_mysql.domain.model.ShopInformation
・・・（以下省略）

```

#### 2. クラス名の上に@Immutableを追記する
こちらは記載しなくても動きます。
しかし、VIEW上のデータは直接操作はできない（read only）ため、@Immutableをつけて変更不可とします。


なお、JpaRepositoryを継承したRepositoryインターフェースは以下の通り。

```java:ShopInformationRepository.java

package jp.co.hibernate_exchange_with_mysql.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import jp.co.hibernate_exchange_with_mysql.domain.model.ShopInformation;

@Repository
public interface ShopInformationRepository extends JpaRepository<ShopInformation, Integer>{
}

```
単一主キーのテーブルと同じく、extends JpaRepository<エンティティクラス, ID>のIDの部分には、主キーに当たるカラムのデータ型を書きます。




### 主キーにあたるカラムが複数存在する場合
VIEWに主キーにあたるカラムが複数存在する（複合主キーにあたる）場合は、上述の内容の他に、別途PKクラスを定義して、Entityクラス上で明示する必要があります。

例えば、主キーにあたるカラムがshop_id, item_id ,user_idのshop_itemsビュー由来のEntityクラスであるShopItem.javaの場合は以下の通り。

```java:ShopItem.java

package jp.co.hibernate_exchange_with_mysql.domain.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.NamedQuery;
import javax.persistence.Table;

import org.hibernate.annotations.Immutable;


/**
 * The persistent class for the shop_items database table.
 *
 */
@Entity
@Immutable // 追記
@IdClass(ShopItemPK.class) // 追記（別途PKクラスを定義）
@Table(name="shop_items")
@NamedQuery(name="ShopItem.findAll", query="SELECT s FROM ShopItem s")
public class ShopItem implements Serializable {
	private static final long serialVersionUID = 1L;

	@Column(name="is_favorite")
	private Integer isFavorite;

	@Id // 追記
	@Column(name="item_id", updatable = false , nullable = false) // 追記（任意）
	private String itemId;

	@Column(name="item_name")
	private String itemName;

	private Integer price;

	@Id // 追記
	@Column(name="shop_id", updatable = false , nullable = false) // 追記（任意）
	private Integer shopId;

	@Column(name="shop_name")
	private String shopName;

	@Id // 追記
	@Column(name="user_id", updatable = false , nullable = false) // 追記（任意）
	private Integer userId;

	public ShopItem() {
	}

	public Integer getIsFavorite() {
		return this.isFavorite;
	}

	public void setIsFavorite(Integer isFavorite) {
		this.isFavorite = isFavorite;
	}

	public String getItemId() {
		return this.itemId;
	}

	public void setItemId(String itemId) {
		this.itemId = itemId;
	}

	public String getItemName() {
		return this.itemName;
	}

	public void setItemName(String itemName) {
		this.itemName = itemName;
	}

	public Integer getPrice() {
		return this.price;
	}

	public void setPrice(Integer price) {
		this.price = price;
	}

	public Integer getShopId() {
		return this.shopId;
	}

	public void setShopId(Integer shopId) {
		this.shopId = shopId;
	}

	public String getShopName() {
		return this.shopName;
	}

	public void setShopName(String shopName) {
		this.shopName = shopName;
	}

	public Integer getUserId() {
		return this.userId;
	}

	public void setUserId(Integer userId) {
		this.userId = userId;
	}

}

```

PK（プライマリーキー）クラスは以下の通りとなります。

```java:ShopItemPK.java
package jp.co.hibernate_exchange_with_mysql.domain.model;

import java.io.Serializable;

import javax.persistence.Column;
import javax.persistence.Embeddable;

/**
 * The primary key class for the user_favorites database table.
 *
 */
@Embeddable
public class ShopItemPK implements Serializable {
	//default serial version id, required for serializable classes.
	private static final long serialVersionUID = 1L;

	@Column(name="shop_id")
	private Integer shopId;

	@Column(name="item_id")
	private Integer itemId;

	@Column(name="user_id")
	private Integer userId;

	public ShopItemPK() {
	}
	public Integer getShopId() {
		return this.shopId;
	}
	public void setShopId(Integer shopId) {
		this.shopId = shopId;
	}
	public Integer getItemId() {
		return this.itemId;
	}
	public void setItemId(Integer itemId) {
		this.itemId = itemId;
	}
	public Integer getUserId() {
		return this.userId;
	}
	public void setUserId(Integer userId) {
		this.userId = userId;
	}

	public boolean equals(Object other) {
		if (this == other) {
			return true;
		}
		if (!(other instanceof ShopItemPK)) {
			return false;
		}
		ShopItemPK castOther = (ShopItemPK)other;
		return
			(this.shopId == castOther.shopId)
			&& (this.itemId == castOther.itemId)
			&& (this.userId == castOther.userId);
	}

	public int hashCode() {
		final int prime = 31;
		int hash = 17;
		hash = hash * prime + this.shopId;
		hash = hash * prime + this.itemId;
		hash = hash * prime + this.userId;

		return hash;
	}
}

```
主キーが数値型以外の場合は、hashCode()メソッド内の記述を以下のようにします（.hashCode()を使います。）。

```java:
hash = hash * prime + this.itemId.hashCode(); //itemIdがString型の場合
```


Repositoryインターフェースは以下の通り。

```java:ShopItemRepository.java
package jp.co.hibernate_exchange_with_mysql.domain.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import jp.co.hibernate_exchange_with_mysql.domain.model.ShopItem;
import jp.co.hibernate_exchange_with_mysql.domain.model.ShopItemPK;

@Repository
public interface ShopItemRepository extends JpaRepository<ShopItem, ShopItemPK>{

}

```
extends JpaRepository<エンティティクラス, IDのIDの部分には、PKクラスを書きます（複合主キーを持つテーブルと同様）。

## テーブル定義とVIEW定義
今回作成したEntityクラスのもととなるテーブルとVIEWの定義を以下に記載しておきます。

### テーブル定義
```sql:shops
create table shops (
  shop_id INT not null AUTO_INCREMENT comment '店舗ID'
  , shop_name VARCHAR(100) comment '店舗名'
  , tel VARCHAR(15) comment '電話番号'
  , zip_code VARCHAR(8) comment '郵便番号'
  , address VARCHAR(150) comment '住所'
  , category_id SMALLINT comment 'カテゴリID'
  , created_by VARCHAR(100) comment '新規作成者'
  , created_at DATETIME default now() comment '新規作成日'
  , updated_by VARCHAR(100) comment '最終更新者'
  , updated_at DATETIME default now() comment '最終更新日'
  , is_deleted TINYINT default 0 comment '削除フラグ'
  , constraint shop_PKC primary key (shop_id)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin comment 'ショップ' ;
```

```sql:shop_categories
create table shop_categories (
  category_id SMALLINT not null AUTO_INCREMENT comment 'カテゴリID'
  , category_name VARCHAR(100) comment 'カテゴリ名'
  , created_by VARCHAR(100) comment '新規作成者'
  , created_at DATETIME default now() comment '新規作成日'
  , updated_by VARCHAR(100) comment '最終更新者'
  , updated_at DATETIME default now()comment '最終更新日'
  , is_deleted TINYINT default 0 comment '削除フラグ'
  , constraint shop_category_PKC primary key (category_id)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin comment 'ショップカテゴリ' ;
```

```sql:items
create table items (
  item_id  VARCHAR(43) not null comment '商品ID'
  , item_name VARCHAR(100) comment '商品名'
  , price INT comment '価格'
  , shop_id INT comment 'ショップID'
  , created_by VARCHAR(100) comment '新規作成者'
  , created_at DATETIME default now() comment '新規作成日'
  , updated_by VARCHAR(100) comment '最終更新者'
  , updated_at DATETIME default now() comment '最終更新日'
  , is_deleted TINYINT default 0 comment '削除フラグ'
  , constraint items_PKC primary key (item_id)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin comment '商品' ;
```

```sql:users
create table users (
  user_id INT not null AUTO_INCREMENT comment 'ユーザーID'
  , user_name VARCHAR(100) comment 'ユーザー名'
  , tel VARCHAR(15) comment '電話番号'
  , zip_code VARCHAR(8) comment '郵便番号'
  , address VARCHAR(150) comment '住所'
  , password VARCHAR(20) comment 'パスワード'
  , created_by VARCHAR(100) comment '新規作成者'
  , created_at DATETIME default now() comment '新規作成日'
  , updated_by VARCHAR(100) comment '最終更新者'
  , updated_at DATETIME default now() comment '最終更新日'
  , is_deleted TINYINT default 0 comment '削除フラグ'
  , constraint users_PKC primary key (user_id)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin comment 'ユーザー' ;
```

```sql:user_favorites
create table user_favorites (
  item_id INT not null comment '商品ID'
  , user_id INT not null comment 'ユーザーID'
  , created_by VARCHAR(100) comment '新規作成者'
  , created_at DATETIME default now() comment '新規作成日'
  , updated_by VARCHAR(100) comment '最終更新者'
  , updated_at DATETIME default now() comment '最終更新日'
  , is_deleted TINYINT default 0 comment '削除フラグ'
  , constraint user_favorites_PKC primary key (item_id,user_id)
)ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin comment 'お気に入り' ;
```

### VIEW定義
```sql:shop_informations
create view `shop_informations`
as select
sh.shop_id,
sh.shop_name,
sc.category_name,
sh.zip_code,
sh.address,
sh.tel
from shops sh left outer join shop_categories sc on sh.category_id = sc.category_id and sc.is_deleted = 0
where sh.is_deleted = 0;
```
```sql:shop_items
create view `shop_items`
as select
si.*,
case when exists (select * from user_favorites uf where uf.item_id = si.item_id and uf.user_id = si.user_id and is_deleted = 0) then 1 else 0 end as is_favorite
from
(select
sh.shop_id,
it.item_id,
us.user_id,
sh.shop_name,
it.item_name,
it.price
from shops sh inner join items it on sh.shop_id = it.shop_id
cross join users us
where sh.is_deleted = 0
and it.is_deleted = 0
and us.is_deleted = 0
order by shop_id, item_id, user_id) as si;
```

## 参考
- [Hibernate Tips: How to map a view with Hibernate | Thorben Janssen](https://thorben-janssen.com/hibernate-tips-map-view-hibernate/)
- [HIBERNATE ANNOTATIONS リファレンスガイド | Red Hat Customer Portal](https://access.redhat.com/documentation/ja-jp/red_hat_jboss_web_server/1.0/html-single/hibernate_annotations_reference_guide/index)