コンテンツにスキップ

次の段階をお願いします。コード生成時には可読性のため、不要な改行(空行)はないようにしてください。


承知しました。

以降のコード生成では、以下の方針で統一します。

  • 不要な空行は入れない

  • 関連する処理単位では適度に改行する

  • 1メソッド・1責務を意識する

  • コメントは必要な箇所のみ記載する

  • Spring Boot 3(Jakarta namespace)対応

  • JPA EntityとMyBatis DTOの責務を混在させない

今回は 第7段階:MyBatis Mapper設計 + DTO設計 + SearchCondition設計 を行います。


第7段階の目的

今回の教材管理システムでは、画面表示は基本的に以下の流れになります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
Controller
    ↓
Service
    ↓
MyBatis Mapper
    ↓
SQL
    ↓
DTO
    ↓
Thymeleaf

1. MyBatisの責務

MyBatisでは以下を担当します。

機能 Mapper
教材一覧 ContentMapper
教材詳細 ContentMapper
教材検索 ContentMapper
利用コース確認 ContentUsageMapper
コース構成表示 CourseMapper
学習進捗 LearningMapper
管理者集計 DashboardMapper

2. DTO設計

パッケージ構成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
dto
├ content
│  ├ ContentListDto.java
│  ├ ContentDetailDto.java
│  └ ContentUsageDto.java
│
├ course
│  ├ CourseListDto.java
│  └ CourseTreeDto.java
│
└ learning
   └ LearningProgressDto.java

3. 教材一覧DTO

教師の教材ライブラリ画面で利用します。

表示例:

1
2
3
4
5
6
教材名
種類
作成者
状態
更新日時
利用数

ContentListDto.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
package com.example.lms.dto.content;

import lombok.Data;

import java.time.LocalDateTime;

@Data
public class ContentListDto {
    private Long id;
    private String title;
    private String contentType;
    private String authorName;
    private String status;
    private Integer versionNo;
    private Integer usageCount;
    private LocalDateTime updatedAt;
}

4. 教材詳細DTO

教材詳細画面用です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.lms.dto.content;

import lombok.Data;

import java.time.LocalDateTime;
import java.util.List;

@Data
public class ContentDetailDto {
    private Long id;
    private String title;
    private String summary;
    private String contentType;
    private String authorName;
    private String status;
    private Integer versionNo;
    private LocalDateTime publishedAt;
    private String body;
    private String videoType;
    private String videoId;
    private String videoUrl;
    private List<AttachmentDto> attachments;
}

AttachmentDto.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
package com.example.lms.dto.content;

import lombok.Data;

@Data
public class AttachmentDto {
    private Long id;
    private String originalName;
    private String mimeType;
    private String storageType;
    private String storagePath;
}

5. 教材検索条件

検索画面ではEntityを使いません。


ContentSearchCondition.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.lms.search;

import lombok.Data;

@Data
public class ContentSearchCondition {
    private String keyword;
    private String contentType;
    private String status;
    private Long authorId;
    private Integer page = 0;
    private Integer size = 20;
}

6. Pagination DTO

MyBatis検索ではページングします。


PageRequestDto.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.lms.dto.common;

import lombok.Data;

@Data
public class PageRequestDto {
    private int page = 0;
    private int size = 20;

    public int getOffset() {
        return page * size;
    }
}

7. ContentMapper

教材検索用Mapperです。


ContentMapper.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.example.lms.mapper;

import com.example.lms.dto.content.ContentDetailDto;
import com.example.lms.dto.content.ContentListDto;
import com.example.lms.search.ContentSearchCondition;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface ContentMapper {
    List<ContentListDto> search(ContentSearchCondition condition);
    int count(ContentSearchCondition condition);
    ContentDetailDto findDetail(Long id);
}

8. 教材一覧SQL

MyBatis XML方式を推奨します。

理由:

  • 動的検索

  • JOIN増加

  • 集計

が多いためです。


ContentMapper.xml

配置:

1
2
3
resources
└ mapper
   └ ContentMapper.xml

 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
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.lms.mapper.ContentMapper">

<select id="search"
resultType="com.example.lms.dto.content.ContentListDto">

SELECT
    c.id,
    cv.title,
    c.content_type,
    u.username AS author_name,
    cv.status,
    cv.version_no,
    COUNT(lc.id) AS usage_count,
    cv.updated_at
FROM content c
JOIN content_versions cv
    ON c.current_version_id = cv.id
JOIN users u
    ON c.author_id = u.id
LEFT JOIN lesson_contents lc
    ON c.id = lc.content_id

<where>

<if test="keyword != null and keyword != ''">
AND cv.title LIKE CONCAT('%',#{keyword},'%')
</if>

<if test="contentType != null">
AND c.content_type = #{contentType}
</if>

<if test="status != null">
AND cv.status = #{status}
</if>

<if test="authorId != null">
AND c.author_id = #{authorId}
</if>

</where>

GROUP BY
    c.id,
    cv.id,
    u.username

ORDER BY
    cv.updated_at DESC

LIMIT #{size}
OFFSET #{offset}

</select>

</mapper>

9. 教材詳細SQL

教材詳細ではタイプ別JOINします。

 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
<select id="findDetail"
resultType="com.example.lms.dto.content.ContentDetailDto">

SELECT
    c.id,
    cv.title,
    cv.summary,
    c.content_type,
    u.username AS author_name,
    cv.status,
    cv.version_no,
    cv.published_at,
    tc.body,
    vc.video_type,
    vc.video_id,
    vc.video_url

FROM content c

JOIN content_versions cv
    ON c.current_version_id=cv.id

JOIN users u
    ON c.author_id=u.id

LEFT JOIN text_contents tc
    ON cv.id=tc.content_version_id

LEFT JOIN video_contents vc
    ON cv.id=vc.content_version_id

WHERE c.id=#{id}

</select>

10. コースツリーDTO

受講画面用です。

構造:

1
2
3
4
5
Course
 └ Section
     └ Section
         └ Lesson
             └ Content

CourseTreeDto.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package com.example.lms.dto.course;

import lombok.Data;

import java.util.List;

@Data
public class CourseTreeDto {
    private Long id;
    private String title;
    private String type;
    private List<CourseTreeDto> children;
}

11. CourseMapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.example.lms.mapper;

import com.example.lms.dto.course.CourseTreeDto;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface CourseMapper {
    List<CourseTreeDto> findCourseTree(Long courseId);
}

12. コースツリー取得SQL

階層構造なのでMySQL8のCTEを利用します。

 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
<select id="findCourseTree"
resultType="com.example.lms.dto.course.CourseTreeDto">

WITH RECURSIVE section_tree AS (

SELECT
    id,
    title,
    parent_section_id,
    0 AS depth
FROM sections
WHERE course_version_id=#{courseId}
AND parent_section_id IS NULL

UNION ALL

SELECT
    s.id,
    s.title,
    s.parent_section_id,
    st.depth+1
FROM sections s
JOIN section_tree st
ON s.parent_section_id=st.id

)

SELECT
    id,
    title,
    'SECTION' AS type
FROM section_tree

</select>

13. 教材利用状況DTO

教材編集時に重要です。

例:

1
2
3
4
5
この教材は以下で利用されています

Java基礎
SpringBoot入門
新人研修

ContentUsageDto.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
package com.example.lms.dto.content;

import lombok.Data;

@Data
public class ContentUsageDto {
    private Long courseId;
    private String courseName;
    private String lessonTitle;
}

14. ContentUsageMapper

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.example.lms.mapper;

import com.example.lms.dto.content.ContentUsageDto;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface ContentUsageMapper {
    List<ContentUsageDto> findUsage(Long contentId);
}

15. Service利用イメージ

例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Service
@RequiredArgsConstructor
public class ContentQueryService {

    private final ContentMapper contentMapper;

    public List<ContentListDto> search(ContentSearchCondition condition) {
        return contentMapper.search(condition);
    }
}

この段階で完成したもの

現在の責務分離:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
登録・更新

Controller
 ↓
Service
 ↓
JPA Repository
 ↓
Entity


検索・表示

Controller
 ↓
Service
 ↓
MyBatis Mapper
 ↓
DTO
 ↓
Thymeleaf

次の第8段階

次は画面と業務フローを実装する段階になります。

内容:

第8段階:Service層設計 + Controller設計

作成予定:

  1. ContentService

    • 教材作成

    • Version作成

    • 更新

    • 申請

  2. ApprovalService

    • 承認

    • 差戻し

    • 公開

  3. CourseService

    • コース作成

    • セクション管理

    • Lesson登録

    • 教材配置

  4. Spring Security連携

    • ログインユーザー取得

    • 教師のみ編集可能

    • 管理者のみ承認可能

  5. Thymeleaf画面URL設計

ここから実際のアプリケーション処理になります。