コンテンツにスキップ

ありがとうございます。

ここから 第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を有効化します。

1
2
3
4
5
@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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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設定

推奨:

1
2
3
4
5
6
7
8
9
Content

 ↓ cascade ALL

ContentVersion

 ↓ cascade ALL

Attachment

理由:

教材削除時に不要ファイルを残さない。


Cascade禁止

以下は禁止します。

1
2
3
4
5
Course

↓

Content

理由:

同じ教材を複数コースで利用するため。


17. Fetch設定

基本:

1
@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をそのまま使いません。

例:

悪い例:

1
List<Content>

Thymeleafで

1
content.author.name

大量SQL発生。


良い例:

1
ContentListDto

を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表示用データ構造

を作成します。

この部分で、実際の「教師画面」「管理者画面」「受講者画面」のデータ取得設計が完成します。