ありがとうございます。
ここから 第6段階:JPA Entity完全実装設計 に進みます。
今回は、前段階で作成したDDLをそのままSpring Boot 3 + JPAで扱えるようにします。
設計方針は以下です。
第6段階の設計方針
JPA担当範囲
JPA Entityは以下を担当します。
-
登録
-
更新
-
削除
-
状態変更
-
単体取得
-
関連データ更新
MyBatis担当範囲
MyBatisは後続段階で以下を担当します。
-
教材一覧
-
コースツリー表示
-
教材検索
-
ダッシュボード集計
-
学習進捗一覧
1. 共通Entity設計
BaseEntity
すべてのEntityで共通利用します。
管理項目:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | package com.example.lms.entity.common;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(
nullable = false,
updatable = false
)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
}
|
JpaConfig
Auditingを有効化します。
| @Configuration
@EnableJpaAuditing
public class JpaConfig {
}
|
2. User Entity
Spring Securityのユーザーです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | @Entity
@Table(name="users")
@Getter
@Setter
public class User extends BaseEntity {
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Column(nullable=false)
private String username;
@Column(nullable=false,unique=true)
private String email;
@Column(nullable=false)
private String password;
private boolean enabled = true;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name="user_roles",
joinColumns=@JoinColumn(
name="user_id"
),
inverseJoinColumns=@JoinColumn(
name="role_id"
)
)
private Set<Role> roles =
new HashSet<>();
}
|
3. Role Entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 | @Entity
@Table(name="roles")
@Getter
@Setter
public class Role {
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Column(nullable=false,unique=true)
private String code;
private String name;
}
|
4. Content Entity
教材ライブラリの中心です。
重要:
本文は持たない
です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | @Entity
@Table(name="content")
@Getter
@Setter
public class Content extends BaseEntity {
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable=false)
private ContentType contentType;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="author_id",
nullable=false
)
private User author;
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="current_version_id"
)
private ContentVersion currentVersion;
}
|
ContentType
| public enum ContentType {
VIDEO,
TEXT,
QUIZ,
FILE
}
|
5. ContentVersion Entity
教材の版管理です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 | @Entity
@Table(
name="content_versions",
uniqueConstraints={
@UniqueConstraint(
columnNames={
"content_id",
"version_no"
}
)
}
)
@Getter
@Setter
public class ContentVersion
extends BaseEntity {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="content_id",
nullable=false
)
private Content content;
private Integer versionNo;
private String title;
@Lob
private String summary;
@Enumerated(EnumType.STRING)
private ContentStatus status;
private LocalDateTime publishStart;
private LocalDateTime publishEnd;
@OneToMany(
mappedBy="version",
cascade=CascadeType.ALL,
orphanRemoval=true
)
private List<Attachment> attachments =
new ArrayList<>();
}
|
ContentStatus
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | public enum ContentStatus {
DRAFT,
REVIEWING,
APPROVED,
PUBLISHED,
REJECTED,
ARCHIVED
}
|
6. TextContent
Markdown教材です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 | @Entity
@Table(name="text_contents")
@Getter
@Setter
public class TextContent {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@OneToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="content_version_id"
)
private ContentVersion version;
@Lob
private String body;
private String format;
}
|
7. VideoContent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | @Entity
@Table(name="video_contents")
@Getter
@Setter
public class VideoContent {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@OneToOne(fetch=FetchType.LAZY)
private ContentVersion version;
private String videoType;
private String videoId;
private String videoUrl;
private Integer duration;
}
|
8. QuizContent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | @Entity
@Table(name="quiz_contents")
@Getter
@Setter
public class QuizContent {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@OneToOne(fetch=FetchType.LAZY)
private ContentVersion version;
private String category;
@Lob
private String question;
private boolean multipleAnswer;
private Integer answerCount;
@Lob
private String explanation;
@OneToMany(
mappedBy="quiz",
cascade=CascadeType.ALL
)
private List<QuizChoice> choices =
new ArrayList<>();
}
|
9. QuizChoice
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30 | @Entity
@Table(name="quiz_choices")
@Getter
@Setter
public class QuizChoice {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="quiz_content_id"
)
private QuizContent quiz;
private String choiceText;
private boolean correct;
private Integer displayOrder;
}
|
10. FileContent
ファイル自体はAttachment管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | @Entity
@Table(name="file_contents")
@Getter
@Setter
public class FileContent {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@OneToOne(fetch=FetchType.LAZY)
private ContentVersion version;
@Lob
private String description;
}
|
11. Attachment
Cloudflare R2対応。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45 | @Entity
@Table(name="attachments")
@Getter
@Setter
public class Attachment
extends BaseEntity {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="content_version_id"
)
private ContentVersion version;
private String attachmentType;
private String originalName;
private String storageType;
private String storagePath;
private String mimeType;
private Long fileSize;
private Integer displayOrder;
}
|
12. Course Entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 | @Entity
@Table(name="courses")
@Getter
@Setter
public class Course extends BaseEntity {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
private String title;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="teacher_id"
)
private User teacher;
@OneToOne(fetch=FetchType.LAZY)
private CourseVersion currentVersion;
}
|
13. Section(無限階層)
ここは重要です。
Lazy必須
理由:
1000階層を一気に取得すると危険です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | @Entity
@Table(name="sections")
@Getter
@Setter
public class Section {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
private CourseVersion courseVersion;
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(
name="parent_section_id"
)
private Section parent;
private String title;
private Integer displayOrder;
}
|
14. Lesson
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 | @Entity
@Table(name="lessons")
@Getter
@Setter
public class Lesson {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
private Section section;
private String title;
private Integer displayOrder;
}
|
15. LessonContent
教材配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33 | @Entity
@Table(name="lesson_contents")
@Getter
@Setter
public class LessonContent {
@Id
@GeneratedValue(
strategy=GenerationType.IDENTITY
)
private Long id;
@ManyToOne(fetch=FetchType.LAZY)
private Lesson lesson;
@ManyToOne(fetch=FetchType.LAZY)
private Content content;
private Integer displayOrder;
private boolean requiredFlag;
}
|
16. 設定上の重要ポイント
Cascade設定
推奨:
| Content
↓ cascade ALL
ContentVersion
↓ cascade ALL
Attachment
|
理由:
教材削除時に不要ファイルを残さない。
Cascade禁止
以下は禁止します。
理由:
同じ教材を複数コースで利用するため。
17. Fetch設定
基本:
| @ManyToOne(fetch = FetchType.LAZY)
|
です。
理由:
教材一覧表示時に
1
2
3
4
5
6
7
8
9
10
11
12
13 | Content
↓
Version
↓
Video
↓
Attachment
|
を全部読まないためです。
18. N+1対策
一覧取得はEntityをそのまま使いません。
例:
悪い例:
↓
Thymeleafで
大量SQL発生。
良い例:
をMyBatisで取得。
第6段階終了時点
完成した責務分担:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 | 登録・更新
Controller
↓
Service
↓
Repository(JPA)
↓
Entity
検索・一覧
Controller
↓
Service
↓
Mapper(MyBatis)
↓
DTO
↓
Thymeleaf
|
次の 第7段階 では、画面表示・検索処理を担当するため、
-
MyBatis Mapper設計
-
SearchCondition
-
DTO設計
-
SQL(教材一覧、教材詳細、コースツリー)
-
Thymeleaf表示用データ構造
を作成します。
この部分で、実際の「教師画面」「管理者画面」「受講者画面」のデータ取得設計が完成します。