1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JPAにおける単方向と双方向のリレーションについて

Posted at

Spring Data JPAを使用して自作サービスを作成する過程で、Entity間のリレーションの表すには @OneToMany@ManyToOne アノテーションを使用し、これらのアノテーションの付け方でリレーションが単方向か双方向かを表すことができることを知った。

この記事では、単方向と双方向の参照の違いについて以下のようなTaskとStatusの関係を例を用いてまとめていく。

+----------------+          1       *        +----------------+
|     Status     |-------------------------->|      Task      |
+----------------+                           +----------------+
| - id: Long     |                           | - id: Long     |
| - name: String |                           | - title: String|
|                |                           | - userId: Long |
|                |                           |                |
+----------------+                           +----------------+

※まだ勉強中のため、間違いがあれば指摘いただけますと幸いです。。

単方向リレーション

@OneToMany の単方向リレーション

以下は、Status が複数の Task を持つケース。
Status から Task を参照し、Task 側には Status へのリファレンスがないため、
これを名前の通り「単方向」と呼ぶ。

@Entity
@Table(name = "statuses")
public class Status {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany
    @JoinColumn(name = "statusId")  // 外部キーとしてstatusIdカラムがtasksテーブルに追加される
    private List<Task> tasks;
}

@Entity
@Table(name = "tasks")
public class Task {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String title;
 }

この場合、以下のようなテーブルが自動生成される。

-- 生成されるDDL

create table statuses (
    id bigint generated by default as identity, 
    name varchar(255), 
    primary key (id)
);

create table tasks (
    id bigint generated by default as identity, 
    title varchar(255), 
    status_id bigint, 
    primary key (id)
);

-- 自動で外部キー制約を作成してくれる
alter table if exists tasks 
add constraint FKhp48vnj340wm1stg2kux5dqcx 
foreign key (status_id) references statuses;

ここでそれぞれのデータを取得するためにリクエストを送ってみる。

まずはタスクの一覧を取得する"/api/tasks"にリクエストを送る。
すると、以下のSQLが実行される。

select t1_0.id,t1_0.title from tasks t1_0

Task側にはリレーションの記述を指定をしていないため、もちろんStatusに関するデータを持ってこない。

[
    {
        "id": 1,
        "title": "開発"
    },
    {
        "id": 2,
        "title": "お出かけ"
    },
    {
        "id": 3,
        "title": "読書"
    },
    .
    .
    .
]

次にステータスの一覧を取得する"/api/statuses"にリクエストを送る。
すると、以下のSQLが実行される。

select s1_0.id,s1_0.name from statuses s1_0
select t1_0.status_id,t1_0.id,t1_0.title from tasks t1_0 where t1_0.status_id=?
select t1_0.status_id,t1_0.id,t1_0.title from tasks t1_0 where t1_0.status_id=?
select t1_0.status_id,t1_0.id,t1_0.title from tasks t1_0 where t1_0.status_id=?
select t1_0.status_id,t1_0.id,t1_0.title from tasks t1_0 where t1_0.status_id=?

そして、それぞれのステータスに紐づくタスクが取得できていることがわかる。

[
    {
        "id": 1,
        "name": "未着手",
        "tasks": [
            {
                "id": 1,
                "title": "開発"
            },
            {
                "id": 2,
                "title": "お出かけ"
            }
        ]
    },
    {
        "id": 2,
        "name": "進行中",
        "tasks": [
            {
                "id": 5,
                "title": "料理"
            }
        ]
    },
    {
        "id": 3,
        "name": "完了",
        "tasks": [
            {
                "id": 3,
                "title": "読書"
            },
            {
                "id": 4,
                "title": "勉強"
            }
        ]
    },
    {
        "id": 4,
        "name": "保留",
        "tasks": []
    }
]

@ManyToOne の単方向リレーション

今度は逆にTask側にStatusのリレーションを記述する。

@Entity
@Table(name = "tasks")
public class Task {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String title;
	
	@ManyToOne
	private Status status;
}

@Entity
@Table(name = "statuses")
public class Status {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
 }

すると以下のDDLが生成される。

create table statuses (
    id bigint generated by default as identity, 
    name varchar(255), 
    primary key (id)
)

create table tasks (
    id bigint generated by default as identity, 
    title varchar(255), 
    status_id bigint, primary key (id)
)

alter table if exists tasks
add constraint FK7rfie9afjter7xqs940p99roi 
foreign key (status_id) references statuses

こちらもTaskテーブルにstatusIdが追加されていることがわかる。
ただ、先ほどと同様それぞれのデータを取得するエンドポイントにリクエストを送ってみる。

・/api/tasks

select t1_0.id,t1_0.status_id,t1_0.title from tasks t1_0
select t1_0.id,t1_0.status_id,t1_0.title from tasks t1_0
select s1_0.id,s1_0.name from statuses s1_0 where s1_0.id=?
select s1_0.id,s1_0.name from statuses s1_0 where s1_0.id=?
select s1_0.id,s1_0.name from statuses s1_0 where s1_0.id=?
[
    {
        "id": 1,
        "title": "開発",
        "status": {
            "id": 1,
            "name": "未着手"
        }
    },
    {
        "id": 2,
        "title": "お出かけ",
        "status": {
            "id": 1,
            "name": "未着手"
        }
    },
    {
        "id": 3,
        "title": "読書",
        "status": {
            "id": 3,
            "name": "完了"
        }
    },
    {
        "id": 4,
        "title": "勉強",
        "status": {
            "id": 3,
            "name": "完了"
        }
    },
    {
        "id": 5,
        "title": "料理",
        "status": {
            "id": 2,
            "name": "進行中"
        }
    }
]

・/api/statuses

select s1_0.id,s1_0.name from statuses s1_0
[
    {
        "id": 1,
        "name": "未着手"
    },
    {
        "id": 2,
        "name": "進行中"
    },
    {
        "id": 3,
        "name": "完了"
    },
    {
        "id": 4,
        "name": "保留"
    }
]

上記の通り、Task側ではStatusの情報まで取得できていて、
逆にStatus側にはTaskの情報は含まれていないることがわかる。

双方向リレーション

次に双方向リレーションについて試してみる。
単方向と同様、Entityにリレーションを記述する。

@Entity
@Data
@Table(name = "statuses")
public class Status {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String name;
	
	@OneToMany(mappedBy = "status")
	private List<Task> tasks;
}

@Entity
@Data
@Table(name = "tasks")
public class Task {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Long id;
	
	private String title;
	
	@ManyToOne
	private Status status;
	
}

すると、単方向参照の際と同様、Taskテーブル側にstatusIdが外部キーつとして作成される。

create table statuses (
    id bigint generated by default as identity, 
    name varchar(255), 
    primary key (id)
);

create table tasks (
    id bigint generated by default as identity, 
    title varchar(255), 
    status_id bigint, 
    primary key (id)
);

alter table if exists tasks 
add constraint FKhp48vnj340wm1stg2kux5dqcx 
foreign key (status_id) references statuses;

先ほどと同様、リクエストを送ってみる。
すると、以下のエラーが出力される。

Ignoring exception, response committed already:org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Document nesting depth (1001) exceeds the maximum allowed (1000, from`StreamWriteConstraints.getMaxNestingDepth()`)

上記は循環参照をしているため発生している。

返ってきたデータを見てみると以下のようになっている。

[
    {
        "id": 1,
        "name": "未着手",
        "tasks": [
            {
                "id": 1,
                "title": "開発",
                "status": {
                    "id": 1,
                    "name": "未着手",
                    "tasks": [
                        {
                            "id": 1,
                            "title": "開発",
                            "status": {
                                "id": 1,
                                "name": "未着手",
                                "tasks": [
                                .
                                .
                                .
]

名前の通り、Status が Task を持ち、Task が再度 Status を参照し、さらにStatusが・・・
というように、無限にデータがネストされる状況が発生する。

循環参照を回避するには?

@JsonBackReference@JsonManagedReferenceを使用することで
双方向のリレーションを保ったまま循環参照を回避できるとのこと。

試しに以下のようにEntityクラスを修正する。

@Entity
public class Status {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "status")
    @JsonManagedReference
    private List<Task> tasks;
}

@Entity
public class Task {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String title;

    @ManyToOne
    @JoinColumn(name = "status_id")
    @JsonBackReference
    private Status status;
}

リクエストを送ってみる。

・/api/tasks

[
    {
        "id": 1,
        "title": "開発"
    },
    {
        "id": 2,
        "title": "お出かけ"
    },
    {
        "id": 3,
        "title": "読書"
    },
    {
        "id": 4,
        "title": "勉強"
    },
    {
        "id": 5,
        "title": "料理"
    }
]

・/api/statuses

[
    {
        "id": 1,
        "name": "未着手",
        "tasks": [
            {
                "id": 1,
                "title": "開発"
            },
            {
                "id": 2,
                "title": "お出かけ"
            }
        ]
    },
    {
        "id": 2,
        "name": "進行中",
        "tasks": [
            {
                "id": 5,
                "title": "料理"
            }
        ]
    },
    {
        "id": 3,
        "name": "完了",
        "tasks": [
            {
                "id": 3,
                "title": "読書"
            },
            {
                "id": 4,
                "title": "勉強"
            }
        ]
    },
    {
        "id": 4,
        "name": "保留",
        "tasks": []
    }
]

循環参照を回避することができた。
しかし、Task側ではStatusの情報を持てていない。
調べてみると以下の記載を見つけた。

@JsonManagedReference is the forward part of reference, the one that gets serialized normally.
@JsonBackReference is the back part of reference; it’ll be omitted from serialization.
The serialized Item object doesn’t contain a reference to the User object.
Also note that we can’t switch around the annotations. The following will work for the serialization:
But when we attempt to deserialize the object, it’ll throw an exception, as @JsonBackReference can’t be used on a collection.

JsonBackReference側はシリアライズ時に省略されるとのこと。
また、このアノテーションは1に@JsonManagedReference、多に@JsonBackReferenceをつける必要があるとのこと。

上記の記事を見てみるとほかにも循環参照を回避する術があるとのことだったので、
試したらまた記事にまとめようと思う。

参考記事

https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion#bd-managed-back-reference
https://sora-tomita.hatenablog.com/entry/2017/08/15/001134
https://itpfdoc.hitachi.co.jp/manuals/link/cosmi_v0870/APKC/EU070312.HTM
https://codezine.jp/article/detail/5061

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?