リレーション

リレーションとは

リレーション(またはリレーションシップ)とは、RDBMS上のテーブル同士が関係性を持っていることを言います。
Springでは、Entityにリレーションの設定をしておくと、簡単にリレーション先へアクセスすることができます。SQLで言うJOIN(テーブル結合)のイメージです。

リレーションの関係性の種類としては、「1対1」「1対多」「多対多」があります。
それぞれのパターンについて見ていきましょう。

1対1

下記の「会員」と「会員の個人情報」テーブルは 1対1の対応関係です。

「個人情報」テーブルが持つmember_idには、membersテーブルのidの値が入ることを想定しています。
このmember_idを外部キーと言い、membersテーブルを主テーブル、profilesテーブルを従テーブルと呼びます。
外部キーは一般に「主テーブルの単数形_主テーブルの主キー」で命名することが通例です。

1対1の関係なので、会員テーブルのid列の値で個人情報テーブルのmember_id列を検索した場合、結果は一意に定まります。

テーブル、Entity、Repositoryの準備

下記のSQL文を実行して、membersテーブルおよびprofilesテーブルの作成・データの登録を行ってください。

//テーブルの作成
CREATE TABLE members (id INTEGER PRIMARY KEY, name VARCHAR2(15 CHAR), password VARCHAR2(100));
CREATE TABLE profiles (id INTEGER, member_id INTEGER, address VARCHAR(100 CHAR), PRIMARY KEY (ID));

//データの挿入
INSERT INTO members VALUES (1, '田中', 'tanakaPassword-sample');
INSERT INTO members VALUES (2, '山田', 'yamadaPassword-sample');

INSERT INTO profiles VALUES (1, 1, '東京都');
INSERT INTO profiles VALUES (2, 2, '長野県');

下記の内容で各Entityを作成し、それぞれに対応するRepositoryも作成してください。
【com/cmps/spring/entity/Member.java】

package com.cmps.spring.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import lombok.Data;

@Entity
@Data
@Table(name = "members")
public class Member {
	
	// ID
	@Id
	private Integer id;

	// 名前
	@Column(length = 15)
	private String name;

	// パスワード
	@Column(length = 100)
	private String password;

	/**
	 * リレーション Profile 1対1
	 */
	@OneToOne(mappedBy = "member")
	private Profile profile;
}

【com/cmps/spring/entity/Profile.java】

package com.cmps.spring.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "profiles")
public class Profile {
	
	// ID
	@Id
	private Integer id;

	// メンバーID(外部キー)
	@Column
	private Integer memberId;

	// 住所
	@Column(length = 100)
	private String address;

	/**
	 * リレーション Member 1対1
	 */
	@OneToOne(targetEntity = Member.class)
	@JoinColumn(name = "memberId", referencedColumnName = "id", insertable=false, updatable=false)
	private Member member;
}

リレーションの記述を見ていきましょう。
@OneToOne、@JoinColumnアノテーションを付与し、互いのEntityをフィールド変数として持たせています。

@OneToOneアノテーションはその名の通り、1対1のリレーションを意味します。
Member側では@OneToOne(mappedBy = "member") としていますが、これは「リレーションとしての紐づけを相手エンティティ(Profile)のフィールド(member)での設定に依存する(Memberエンティティ側では詳細に設定しない)」、と言う意味になります。
Profile側で指定しているtargetEntity属性はリレーションする相手のEntityクラスを指定します。なおコンピュータが分かるように明示的に記載しているだけで、省略しても問題ありません。
OneToOne (Jakarta EE 8 Specification API) – Javadoc

@JoinColumnアノテーションにはリレーションで紐づけるカラムを設定します。ここでのリレーションの肝がこの記述です。
nameに自身のテーブルのカラム名、referencedColumnNameに相手テーブルのカラム名(変数名)を指定します。
・insertable=false, updatable=falseはデータ保存時に保存しないフィールドであることを示すために記載しています。
JoinColumn (Jakarta EE 8 Specification API) – Javadoc

主テーブル・従テーブルの情報取得

リレーションの挙動を確認するサンプルコードです。
ここでは簡単に、「Springの概要とプロジェクト作成」で使用したのと同じRestController(Viewではなく値を返す)を使用して、出力結果の確認ができる形にしています。
【com/cmps/spring/controller/RelationController.java】

package com.cmps.spring.controller;

import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import com.cmps.spring.entity.*;
import com.cmps.spring.repository.*;

@RequestMapping("/relation")
@RestController
public class RelationController {

	// RepositoryインターフェースのDI
	@Autowired
	private MemberRepository memberRepository;
	@Autowired
	private ProfileRepository profileRepository;

	/**
	 * MemberからProfileへのリレーション確認
	 */
	@GetMapping("/mem-to-pro")
	public String memberToProfile() {
		//Memberオブジェクトを取得
		Optional<Member> memberOpt = memberRepository.findById(1);
		Member member = memberOpt.get();

		//リレーションから変数を取得
		Profile profile = member.getProfile();

		String text = "メンバーは" + member.getName() + "<br>";
		text += profile.getAddress() + "に住んでいます";

		return text;
	}

	/**
	 * ProfileからMemberへのリレーション確認
	 */
	@GetMapping("/pro-to-mem")
	public String profileToMember() {
		//Profileオブジェクトを取得
		Optional<Profile> profOpt = profileRepository.findById(2);
		Profile prof = profOpt.get();

		//リレーションから変数を取得
		Member member = prof.getMember();

		String text = "プロフィールNo. " + prof.getId() + "<br>";
		text += member.getName() + "さんのプロフィールです";

		return text;
	}
}

MemberからProfileへのリレーション確認として、1つ目の memberToProfile()メソッド が記述されています。
・取得したMemberオブジェクトに対し、変数profileに対するgetterメソッド getProfile()を使用するとMemberのidに紐づくProfileオブジェクトを取得できます
仮にmember.getProfile()をSQLで表すと以下のようなイメージでしょう。
SELECT profiles.id, profiles.member_id, profiles.address FROM members INNER JOIN profiles ON members.id = profiles.member_id WHERE members.id = 1;

リレーション(getProfile())から取得した変数profileは、Profileオブジェクトですので、
profile.getAddress()のようにProfileオブジェクトの変数にもアクセスできます。

・2つ目のProfileからMemberへのリレーション確認の profileToMember()メソッド は、
上記の逆向きでProfileからMemberへアクセスしています。行っていることはほとんど同じです。

URLにアクセスしてメソッドの結果を確認すると、それぞれ紐づくデータが取得できていることを確認できるでしょう。
このように、Entityの設定をするだけで関連するテーブルの情報を取得できるのがリレーションです。

1対多、多対1

1対多の関係は、1つのデータに対し、相手のテーブルの複数データが紐づくような関係です。
下記のような、ブログサイトの会員1人に対してその人が複数の投稿を持つような関係のときが該当します。
この場合、会員テーブルが主テーブルで、投稿テーブルが従テーブルです。

membersテーブルからpostsテーブルを見た時、会員テーブルのid=1に対し投稿テーブルでmember_id=1のデータは複数あるので「1対多」、
postsテーブルからmembersテーブルを見た時、投稿テーブルのmember_id=1に対し会員テーブルのid=1のデータは1つなので「多対1」と言います。

テーブル、Entity、Repositoryの準備

下記のSQL文を実行して、postsテーブルの作成・データの登録を行ってください。

//テーブルの作成
CREATE TABLE posts (id INTEGER PRIMARY KEY, member_id INTEGER, title VARCHAR(30 CHAR), body VARCHAR(200 CHAR));

//データの挿入
INSERT INTO posts VALUES (1, 1, 'Java', '難しい');
INSERT INTO posts VALUES (2, 1, 'PHP', '型付けが緩い');
INSERT INTO posts VALUES (3, 2, 'JavaScript', '廃れない');

下記の内容で各Entityを作成し、対応するRepositoryも作成してください。
【com/cmps/spring/entity/Post.java】

package com.cmps.spring.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "posts")
public class Post {
	
	// ID
	@Id
	private Integer id;

	// メンバーID(外部キー)
	@Column
	private Integer memberId;

	// タイトル
	@Column(length = 30)
	private String title;

	// 本文
	@Column(length = 200)
	private String body;

	/**
	 * リレーション Member 多対1
	 */
	@ManyToOne(targetEntity = Member.class)
	@JoinColumn(name = "memberId", referencedColumnName = "id", insertable=false, updatable=false)
	private Member member;
}

@ManyToOneアノテーションは「多対1」の関係性を表します。プロパティは@OneToOneと変わりません。
「多対1」のため、取得できるMemberは1レコードです。

・@JoinColumnのプロパティは、nameに従テーブルのカラムreferencedColumnNameに主テーブルのカラムを指定します。この場合、nameにはPostのフィールドmemberIdを、referencedColumnNameにはMemberのフィールドidを指定しています。


【com/cmps/spring/entity/Member.java】に追記

package com.cmps.spring.entity;

import java.util.List;////追記

@Entity
@Data
@Table(name = "members")
public class Member {
	
	////以下追記
	/**
	 * リレーション Post 1対多
	 */
	@OneToMany(mappedBy = "member")
	private List<Post> posts;
}

@OneToManyアノテーションは「1対多」の関係性を表します。
「1対多」のため、取得できるPostは複数レコードと想定されます。そのため、変数をList<Post>型で表現します。

・1対1のときと同様、リレーションの紐づけについては、相手のPostクラスの記述を利用するため @OneToMany(mappedBy = "member") と記述しています。

主テーブルの情報取得

リレーションの挙動を確認するサンプルコードです。
【com/cmps/spring/controller/RelationController.java】に追記

package com.cmps.spring.controller;

@RequestMapping("/relation")
@RestController
public class RelationController {

	////追記
	@Autowired
	private PostRepository postRepository;

	////追記
	/**
	 * PostからMemberへのリレーション確認
	 */
	@GetMapping("/post-to-mem")
	public String postToMember() {
		//Postオブジェクトを取得
		Optional<Post> postOpt = postRepository.findById(2);
		Post post = postOpt.get();

		//リレーションから変数を取得
		Member member = post.getMember();

		String text = "投稿ID:" + post.getId();
		text += " , タイトル:" + post.getTitle() + " , 本文:" + post.getBody() + "<br>";
		text += member.getName() + "さんの投稿です";

		return text;
	}
}

・PostからMemberへのリレーション確認 postToMember()メソッドは、
リレーションで取得できるのはオブジェクトなので、1対1と同じように取得できます。

従テーブルの情報取得

リレーションの挙動を確認するサンプルコードです。
【com/cmps/spring/controller/RelationController.java】に追記

package com.cmps.spring.controller;

@RequestMapping("/relation")
@RestController
public class RelationController {

	////追記
	/**
	 * MemberからPostへのリレーション確認
	 */
	@GetMapping("/mem-to-post")
	public String memberToPost() {
		//Memberオブジェクトを取得
		Optional<Member> memberOpt = memberRepository.findById(1);
		Member member = memberOpt.get();

		//リレーションから変数を取得
		List<Post> posts = member.getPosts();

		String text = "メンバーは" + member.getName() + "<br>";
		for (Post post : posts) {
			text += "投稿ID:" + post.getId();
			text += " , タイトル:" + post.getTitle() + " , 本文:" + post.getBody() + "<br>";
		}
		return text;
	}
}

・こちらのMemberからPostへのリレーションの確認 memberToPost()メソッド では、最初に指定したmemberのid=1と一致する投稿テーブルのデータを取得します。
投稿テーブルにはmember_id=1のデータが複数 存在し得るので、先ほどとは異なり、リレーションで取得できるのはPostのListです。


・変数postsはListですから、posts.getId()などとするとエラーになります。for文などでPostオブジェクトを取り出す必要があります。
ここでは拡張for文で取り出して全データを出力しています。

仮にmember.getPosts()をSQLで表すと以下のようなイメージでしょう。
SELECT posts.id, posts.member_id, posts.title, posts.body FROM members
INNER JOIN posts ON members.id = posts.member_id WHERE members.id = 1;

多対多

例えば上記の「注文」と「商品」の関係だと、「注文」は必ず1個以上の商品を含んでいます。つまり、注文と商品は「1対多」の関係にあります。
逆に、「商品」については必ず0個以上の注文に属しています。(注文されていない商品もある) つまり、商品と注文は「1対多」の関係にあります。
このように、関連するテーブルどちらから見ても1対多の関係にある関係性を「多対多」といいます。

多対多の関係には中間テーブルを用意する必要があります。
どの注文がどの商品と結びついているかの関係性を、中間テーブルを使って整理します。

今回の例ですと、下記のようにテーブルを用意します。
中間テーブルが互いの外部キー(order_id,product_id)を持っているのが特徴です。
中間テーブルにある「注文数」(quantity)列は、注文と商品の組み合わせに応じて値が変わるので、注文テーブルにも商品テーブルにも登録できません。(1対多では表現できない)そのため、中間テーブルに登録されています。「この注文のとき、この商品は何個注文されている」といったように、商品と注文の両方を特定しないと注文数を表せない、ということです。

各テーブルには下記の値が登録されているとします。

テーブル、Entity、Repositoryの準備

下記のSQL文を実行して、3つのテーブルの作成・データの登録を行ってください。

//テーブルの作成
CREATE TABLE orders (id INTEGER PRIMARY KEY, name VARCHAR(20 CHAR));
CREATE TABLE products (id INTEGER PRIMARY KEY, name VARCHAR(20 CHAR), price INTEGER);
CREATE TABLE order_product (id INTEGER PRIMARY KEY, order_id INTEGER, product_id INTEGER, quantity INTEGER);

//データの挿入
INSERT INTO orders VALUES(1,'山田');
INSERT INTO orders VALUES(2,'小林');

INSERT INTO products VALUES(1,'リンゴ', 130);
INSERT INTO products VALUES(2,'オレンジ', 150);
INSERT INTO products VALUES(3,'ブドウ', 180);

INSERT INTO order_product VALUES(1,2,1,8);
INSERT INTO order_product VALUES(2,1,2,3);
INSERT INTO order_product VALUES(3,1,3,5);
INSERT INTO order_product VALUES(4,1,1,10);

下記の内容で各Entityを作成し、それぞれのEntityに対応するRepositoryも作成してください。
【com/cmps/spring/entity/Order.java】

package com.cmps.spring.entity;

import java.util.List;
import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "orders")
public class Order {

	// ID
	@Id
	private Integer id;

	// 注文者名
	@Column(length = 20)
	private String name;

	/**
	 * リレーション OrderProduct(中間テーブル) 多対多
	 */
	@OneToMany(mappedBy = "order")
	private List<OrderProduct> orderProducts;
}

【com/cmps/spring/entity/Product.java】

package com.cmps.spring.entity;

import java.util.List;
import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "products")
public class Product {

	// ID
	@Id
	private Integer id;

	// 商品名
	@Column(length = 20)
	private String name;

	// 価格
	@Column
	private Integer price;
	
	/**
	 * リレーション OrderProduct(中間テーブル) 多対多
	 */
	@OneToMany(mappedBy = "product")
	private List<OrderProduct> orderProducts;
}

・Order、Productのエンティティには、中間テーブルに対する@OneToManyのリレーションを記載しています。
それぞれOrderProduct側でリレーションの設定をするため、mappedByで簡略的に記述しています。1対多の場合のMemberエンティティと同様です。

中間テーブルのEntityを作成します。(中間テーブルのRepositoryは不要です)
【com/cmps/spring/entity/OrderProduct.java】

package com.cmps.spring.entity;

import jakarta.persistence.*;
import lombok.Data;

@Entity
@Data
@Table(name = "order_product")
public class OrderProduct {

	// ID
	@Id
	private Integer id;

	// 注文ID
	@Column
	private Integer orderId;

	// 商品ID
	@Column
	private Integer productId;

	// 注文数
	private Integer quantity;

	/**
	 * リレーション Order-Product(自身が中間テーブル) 多対多
	 */
	@ManyToOne(targetEntity = Order.class)
	@JoinColumn(name = "orderId", referencedColumnName = "id", insertable = false, updatable = false)
	private Order order;

	@ManyToOne(targetEntity = Product.class)
	@JoinColumn(name = "productId", referencedColumnName = "id", insertable = false, updatable = false)
	private Product product;

}

・中間テーブルでは、それぞれのテーブルに対し@ManyToOneでリレーションします。
・1対多の場合のPostクラスと同様に、@JoinColumnを設定しています。中間テーブルから見ると、商品と注文のそれぞれのテーブルと関係しているので、それぞれ記述しています。

互いのテーブルへのアクセス、中間テーブルの値の取得

リレーションの挙動を確認するサンプルコードです。
【com/cmps/spring/controller/RelationController.java】に追記

package com.cmps.spring.controller;

@RequestMapping("/relation")
@RestController
public class RelationController {

	@Autowired
	private OrderRepository orderRepository;
	@Autowired
	private ProductRepository productRepository;
	
	/**
	 * OrderからProductへのリレーション確認
	 */
	@GetMapping("/order-to-product")
	public String orderToProduct() {
		//Orderオブジェクトを1つ取得
		Optional<Order> orderOpt = orderRepository.findById(1);
		Order order = orderOpt.get();

		//中間テーブルの情報を取得(①)
		List<OrderProduct> relations = order.getOrderProducts();

		String text = "注文者は" + order.getName() + "<br>" + "<注文内容は以下>" + "<br>";
		for (OrderProduct relate : relations) {//(②)
			//中間テーブルからProductを取得
			Product product = relate.getProduct();//(③)

			//文章を作成
			text += "商品名:" + product.getName() + " , 価格:" + product.getPrice();
			text += " , 注文数:" + relate.getQuantity() + "個" + "<br>";
		}
		return text;
	}
	
	/**
	 * ProductからOrderへのリレーション確認
	 */
	@GetMapping("/product-to-order")
	public String productToOreder() {
		//Productオブジェクトを取得
		Optional<Product> productOpt = productRepository.findById(1);
		Product product = productOpt.get();
		
		//中間テーブルの情報を取得
		List<OrderProduct> relations = product.getOrderProducts();
		
		String text = "商品は" + product.getName() + "(" + product.getPrice() + "円)<br>";
		text += "<注文内容は以下>" + "<br>";
		for (OrderProduct relate : relations) {
			//中間テーブルからOrderを取得
			Order order = relate.getOrder();

			//文章を作成
			text += "注文ID:" + order.getId() + " , 注文者名:" + order.getName();
			text += " , 注文数:" + relate.getQuantity() + "個" + "<br>";
		}
		return text;
	}
}

・@OneToMany、@ManyToOneをそれぞれ設定した通り、1対多の合わせ技でアクセスしています。

1つ目のOrderからProductへのリレーションを確認する orderToProduct()メソッド で言うと、
①【Order】から【中間テーブル】にリレーション(OneToMany):orderのidが一致する中間テーブルのデータを取得
→②取得した中間テーブルの情報はListなのでfor文で回し、
→③回した【中間テーブル】から【Product】にリレーション(ManyToOne):productのidが一致するデータを取得
という風に、2段階でリレーションしているということです。

relate.getQuantity() の記述について、注文数quantityは中間テーブルのカラムですので、中間テーブルのOrderProductオブジェクトからgetterメソッドで取得できます。

SQLで表すと、2段階でリレーションしている、というのがもう少し分かりやすいと思います。
SELECT * FROM orders INNER JOIN order_product ON orders.id = order_product.order_id
INNER JOIN products ON products.id = order_product.product_id WHERE orders.id = 1;

図で表すと以下のようなイメージです。

①【Order】から【中間テーブル】にリレーション(OneToMany) :orderのidが一致する中間テーブルのデータを取得

②取得した中間テーブルの情報はListなのでfor文で回し、
③回した【中間テーブル】から【Product】にリレーション(ManyToOne) :productのidが一致するデータをそれぞれ取得


・productToOreder()メソッド では、前述のメソッドとは逆向きで、Productに紐づくOrderの情報を取得するサンプルになっています。

@ManyToManyについて
@ManyToManyというアノテーションも存在します。
中間テーブルのEntityを作ることなく、多対多の関係にある片方のEntityから相手のEntityにアクセスできます。
しかし、@ManyToManyでは中間テーブルの他のカラム(今回の例で言うとquantity)を取得できません
中間テーブルに外部キー(order_idとproduct_id)しかないような場合であれば使用を検討してもいいでしょう。
Many-To-Many Relationship in JPA | Baeldung

練習問題

下記のテーブルを作成して、リレーションで問題に合うデータを取得してください。
取得した結果は、マニュアルと同じようにRestControllerで出力する形で構いません。

生徒テーブル

生徒番号名前性別住所年齢
001001田中沖縄17
001002山田埼玉18
001003佐々木東京16
001004藤田愛知17

期末テストテーブル

テストID題名
12021夏季期末テスト
22021冬期期末テスト
32022夏季期末テスト
42022冬期期末テスト

中間テーブル

入力番号生徒IDテストID結果
10010011100
2001001350
3001003350
4001003470
5001004277
6001004388
7001004499


問1: 「2022夏季期末テスト」を受けた生徒の情報を取得してください。

↓出力例

問2: 人ごとに、テストの点数を表示してください。

↓出力例

タイトルとURLをコピーしました