コンテンツにスキップ

ありがとうございます。

ここから 第8段階:Service層設計 + Controller設計 に進みます。

今回は、これまで決定した設計方針に従い、

  • Entity変更はJPA

  • 検索・表示はMyBatis

  • Serviceが業務ルールを管理

  • Controllerは画面制御のみ

という責務分離で設計します。

また、教材管理では特に重要な以下の業務フローを実装対象にします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
教師

教材作成
 ↓
下書き保存
 ↓
審査申請
 ↓
管理者承認
 ↓
公開

受講者

公開教材閲覧

1. Service層構成

パッケージ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
service
├ content
│  ├ ContentCommandService.java
│  └ ContentQueryService.java
│
├ course
│  ├ CourseCommandService.java
│  └ CourseQueryService.java
│
├ approval
│  └ ApprovalService.java
│
├ learning
│  └ LearningService.java
│
└ security
   └ LoginUserService.java

2. Command / Query分離

今回はCQRSの考え方を少し取り入れます。

Command

変更系

1
2
3
4
登録
更新
削除
状態変更

利用:

1
JPA Repository

Query

参照系

1
2
3
4
一覧
検索
集計
詳細表示

利用:

1
MyBatis Mapper

3. ContentCommandService

教材作成・更新担当です。

責務:

  • 教材作成

  • Version作成

  • 下書き保存

  • 審査申請


ContentCommandService.java

 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
package com.example.lms.service.content;

import com.example.lms.entity.content.Content;
import com.example.lms.entity.content.ContentVersion;
import com.example.lms.entity.user.User;
import com.example.lms.enums.ContentStatus;
import com.example.lms.repository.ContentRepository;
import com.example.lms.repository.ContentVersionRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
public class ContentCommandService {

    private final ContentRepository contentRepository;
    private final ContentVersionRepository contentVersionRepository;

    @Transactional
    public Long createTextContent(User teacher,String title,String summary,String body){

        Content content = new Content();
        content.setAuthor(teacher);

        contentRepository.save(content);

        ContentVersion version = new ContentVersion();
        version.setContent(content);
        version.setVersionNo(1);
        version.setTitle(title);
        version.setSummary(summary);
        version.setStatus(ContentStatus.DRAFT);

        contentVersionRepository.save(version);

        content.setCurrentVersion(version);

        return content.getId();
    }
}

4. 教材申請処理

教師が「申請」ボタンを押した場合。

状態変更のみ行います。


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Transactional
public void requestReview(Long contentId){

    Content content =
        contentRepository.findById(contentId)
        .orElseThrow();


    ContentVersion version =
        content.getCurrentVersion();


    version.setStatus(
        ContentStatus.REVIEWING
    );

}

5. ApprovalService

管理者承認処理です。

責務:

  • 承認

  • 差戻し

  • 公開


ApprovalService.java

 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
@Service
@RequiredArgsConstructor
public class ApprovalService {

    private final ContentVersionRepository versionRepository;

    @Transactional
    public void approve(Long versionId){

        ContentVersion version =
            versionRepository.findById(versionId)
            .orElseThrow();


        version.setStatus(
            ContentStatus.PUBLISHED
        );


        version.setPublishedAt(
            LocalDateTime.now()
        );
    }


    @Transactional
    public void reject(Long versionId,String comment){

        ContentVersion version =
            versionRepository.findById(versionId)
            .orElseThrow();


        version.setStatus(
            ContentStatus.REJECTED
        );
    }
}

6. ContentQueryService

表示用です。

MyBatisを利用します。


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
@RequiredArgsConstructor
public class ContentQueryService {

    private final ContentMapper contentMapper;


    public List<ContentListDto> search(
            ContentSearchCondition condition){

        return contentMapper.search(condition);

    }


    public ContentDetailDto findDetail(Long id){

        return contentMapper.findDetail(id);

    }
}

7. CourseCommandService

コース構築担当です。

責務:

  • コース作成

  • Section追加

  • Lesson追加

  • 教材配置


教材配置処理

重要なのは、

Contentをコピーしない

ことです。


 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
@Service
@RequiredArgsConstructor
public class CourseCommandService {

    private final LessonContentRepository lessonContentRepository;
    private final LessonRepository lessonRepository;
    private final ContentRepository contentRepository;


    @Transactional
    public void addContent(
            Long lessonId,
            Long contentId){


        Lesson lesson =
            lessonRepository.findById(lessonId)
            .orElseThrow();


        Content content =
            contentRepository.findById(contentId)
            .orElseThrow();


        LessonContent lc =
            new LessonContent();

        lc.setLesson(lesson);
        lc.setContent(content);
        lc.setRequiredFlag(true);


        lessonContentRepository.save(lc);

    }
}

8. Controller設計

Controllerは薄くします。


教師用Controller

URL:

1
/teacher/content

ContentController.java

 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
@Controller
@RequestMapping("/teacher/content")
@RequiredArgsConstructor
public class ContentController {

    private final ContentCommandService commandService;
    private final ContentQueryService queryService;


    @GetMapping
    public String list(
        Model model,
        ContentSearchCondition condition){

        model.addAttribute(
            "contents",
            queryService.search(condition)
        );

        return "teacher/content/list";
    }


    @PostMapping("/{id}/review")
    public String review(
        @PathVariable Long id){

        commandService.requestReview(id);

        return "redirect:/teacher/content";
    }
}

9. 管理者Controller

URL:

1
/admin/approval

 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
@Controller
@RequestMapping("/admin/approval")
@RequiredArgsConstructor
public class ApprovalController {


    private final ApprovalService approvalService;


    @PostMapping("/{id}/approve")
    public String approve(
        @PathVariable Long id){

        approvalService.approve(id);

        return "redirect:/admin/approval";
    }


    @PostMapping("/{id}/reject")
    public String reject(
        @PathVariable Long id){

        approvalService.reject(
            id,
            "修正してください"
        );

        return "redirect:/admin/approval";
    }
}

10. 受講者Controller

URL:

1
/student/course

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Controller
@RequestMapping("/student/course")
@RequiredArgsConstructor
public class StudentCourseController {

    private final CourseQueryService queryService;


    @GetMapping("/{id}")
    public String detail(
        @PathVariable Long id,
        Model model){

        model.addAttribute(
            "course",
            queryService.findTree(id)
        );


        return "student/course/detail";
    }
}

11. Spring Security連携

教師の場合:

1
2
@PreAuthorize(
"hasRole('TEACHER')")

例:

1
2
3
@PreAuthorize("hasRole('TEACHER')")
@PostMapping("/{id}/review")
public String review(...)

管理者:

1
2
@PreAuthorize(
"hasRole('ADMIN')")

12. 認可ルール

操作 ADMIN TEACHER MENTOR STUDENT
教材作成 × ×
教材編集 ○(自身のみ) × ×
教材承認 × × ×
教材閲覧
質問対応 × ×

13. 教材編集権限チェック

教師は自分の教材だけ変更可能にします。

Serviceで確認します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private void checkOwner(
        Content content,
        User loginUser){

    if(!content.getAuthor()
        .getId()
        .equals(loginUser.getId())){

        throw new AccessDeniedException(
            "編集権限がありません"
        );
    }
}

14. 現時点の完成フロー

 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
教師ログイン

↓

教材作成画面

↓

ContentCommandService

↓

Content
ContentVersion

↓

DRAFT


申請

↓

REVIEWING


管理者

↓

ApprovalService

↓

PUBLISHED


受講者

↓

CourseMapper

↓

公開教材表示

第8段階終了時点

実装可能な業務フローが完成しました。

次の 第9段階 では、画面設計とThymeleaf実装に進みます。

作成内容:

  1. 教師画面

    • 教材一覧

    • 教材登録

    • Markdown入力

    • 動画登録

    • クイズ作成

  2. 管理者画面

    • 承認待ち一覧

    • 承認詳細

  3. 受講者画面

    • コース一覧

    • セクションツリー

    • レッスン表示

    • 動画再生

    • Markdown HTML表示

  4. Bootstrap 5デザイン

まで設計します。