MyBatis教程[5]----resultMap高級結果映射、自動映射


在實際應用中,我們常常會遇到數據庫中的字段和Java實體中的屬性名不一致的情況,或在多表查詢時需要將多個表的數據與實體屬性關聯起來的情況,此時就需要使用resultMap進行結果映射了。

在這裏引用一下Mybatis官網對resultMap的介紹:

resultMap 元素是 MyBatis 中最重要最強大的元素。它可以讓你從 90% 的 JDBC ResultSets數據提取代碼中解放出來,並在一些情形下允許你進行一些 JDBC 不支持的操作。實際上,在爲一些比如連接的複雜語句編寫映射代碼的時候,一份resultMap 能夠代替實現同等功能的數千行代碼。ResultMap的設計思想是,對簡單的語句做到零配置,對於複雜一點的語句,只需要描述語句之間的關係就行了。

既然官網都說它是最重要最強大的元素,那就讓我們來看看它重要在哪裏,強大在哪裏吧!

1.回顧resultType

1.1 使用回顧

在此之前,先說一說我們前面幾篇博客用到的一個關鍵字resultType

前面我們寫過這樣的查詢語句:

<select id="selectById" resultType="com.yky.springboot.entities.User">
   SELECT * FROM `user` WHERE id = #{id}
</select>

對應的實體類是這樣的:

package com.yky.springboot.entities;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private Long id;
    private String name;
    private String phone;
    private Date birthday;
	
	Getter、Setter方法....
	
    toString方法...
}

數據庫中的字段是這樣的:

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
  `name` varchar(100) DEFAULT NULL COMMENT '姓名',
  `phone` varchar(20) DEFAULT NULL COMMENT '手機號',
  `birthday` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '生日',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

可以看到,數據表中的字段名和Java實體類中的字段名完全一致。此時我們便可以直接用resultType來指定將SQL執行結果映射到com.yky.springboot.entities.User類。

1.2 簡化全類名

當然了,重複的寫完整的類名看起來並不是很優雅,此時我們可以使用類型別名來解決,Mybatis爲我們提供了typeAlias關鍵字,typeAlias需要寫在Mybatis的全局配置文件mybatis-config.xml中。在resources下創建mybatis-config.xml文件,並寫入以下代碼:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    
    <typeAliases>
        <typeAlias type="com.yky.springboot.entities.User" alias="User"></typeAlias>
    </typeAliases>

</configuration>

還需要在yml文件中指定一下Mybatis全局配置文件所在路徑:

mybatis:
  #配置mybatis映射文件路徑
  mapper-locations: classpath:mapper/*.xml
  #配置mybatis全局配置文件所在路徑
  config-location: classpath:mybatis-config.xml

此時同樣的SQL語句可以這樣寫:

<select id="selectById" resultType="User">
   SELECT * FROM `user` WHERE id = #{id}
</select>

1.3 侷限性

1. 數據庫字段需要與實體類字段完全一致

這一點不用多說,畢竟字段不一樣的話Mybatis是不知道怎麼匹配的。

2. 對多表關聯不友好甚至根本滿足不了多表關聯需求(一對一、一對多、多對多)

比如有一個員工類:Employee
有一個部門類:Department
部門類中有一個List<Employee> employees屬性,包含部門所有員工,在這裏部門與員工之間屬於一對多的關係。
有以下需求:
[1] 只通過一條多表查詢語句,直接查詢到部門信息,以及當前部門的所有員工
[2] 將上述SQL語句查詢到的信息封裝到Department類中(employees保存所有員工列表)

顯然這樣做使用resultType是滿足不了的。

鑑於resultType有種種限制,resultMap便應運而生了。

2.resultMap簡單使用

resultMap最簡單的應用場景是:解決數據庫字段與實體屬性名不一致的問題。

接下來直接上代碼

有員工類:

public class Employee implements Serializable {
    /**
     * 主鍵id
     */
    private Long id;

    /**
     * 姓名
     */
    private String name;

    /**
     * 工號
     */
    private String number;

    /**
     * 部門編號
     */
    private Long departmentId;

    /**
     * 薪資
     */
    private Double salary;

	Getter(),Setter().....
	toString()....
}

有數據表:

CREATE TABLE `employee` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `employee_name` varchar(255) NOT NULL,
  `number` varchar(255) NOT NULL,
  `department_id` int(11) NOT NULL,
  `salary` decimal(10,2) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

很顯然,數據表中的列名與實體類中的字段名並不一致,這時就需要用到resultMap來指定數據表中的字段與實體屬性字段的一一對應關係了。
Mapper接口代碼:

@Mapper
public interface EmployeeMapper {
    List<Employee> selectAll();
}

寫在EmployeeMapper.xml中的resultMap代碼:

    <resultMap id="empMap" type="com.yky.springboot.entities.Employee">
        <!--主鍵需要使用id標籤單獨寫,可以提高性能-->
        <id column="id" property="id"/>

        <!--普通字段直接使用result標籤-->
        <result column="employee_name" property="name"/>
        <result column="number" property="number"/>
        <result column="department_id" property="departmentId"/>
        <result column="salary" property="salary"/>
    </resultMap>

寫在EmployeeMapper.xml中的SQL查詢語句代碼:

    <select id="selectAll" resultMap="empMap">
        SELECT * FROM employee
    </select>

上面我所舉的例子是resultMap的最簡單的應用了,resultMap的高級應用則體現在了多表聯合映射上。

3.resultMap高級結果映射

resultMap高級結果映射,即在多表聯合查詢時,將結果聯合映射到目標實體中。這裏的目標實體通常包含其他實體對象、集合等元素。常用在一對一,一對多、多對一、多對多的關係表中。

在此之前,先列出resultMap的子元素(摘自官方教程):

  • constructor - 用於在實例化類時,注入結果到構造方法中-----當創建實體類時需要傳構造參數時使用
    。idArg - ID 參數;標記出作爲 ID 的結果可以幫助提高整體性能
    。arg - 將被注入到構造方法的一個普通結果
  • id – 一個 ID 結果;標記出作爲 ID 的結果可以幫助提高整體性能-----標示主鍵id,可以提高性能
  • result – 注入到字段或 JavaBean 屬性的普通結果-----映射普通字段
  • association – 一個複雜類型的關聯;許多結果將包裝成這種類型------映射實體類內部包含的其他實體類
    。嵌套結果映射 – 關聯可以是 resultMap 元素,或是對其它結果映射的引用
  • collection – 一個複雜類型的集合------映射實體類中包含的集合
    。嵌套結果映射 – 集合可以是 resultMap 元素,或是對其它結果映射的引用
  • discriminator – 使用結果值來決定使用哪個 resultMap-----根據判斷條件,有選擇的映射
    。case – 基於某些值的結果映射
    • 嵌套結果映射 – case 也是一個結果映射,因此具有相同的結構和元素;或者引用其它的結果映射

在這裏,我們不去討論數據表的一對一、一對多、多對一、多對多關係。我們只試圖通過下面兩節展示一個稍微複雜一點的例子來演示resultMap的高級用法。

4.一條SQL語句實現多表關聯映射

4.1 實體類創建

假設我們需要定義一個實體類來表示一個大學生,需要包含以下信息:

  • 姓名、性別、年齡、學號,這些屬於基本信息
  • 所加入的社團,這裏需要是一個包含社團對象的集合
  • 所屬專業(在這裏不考慮多學位情況),這裏需要是一個表示專業的對象

社團實體類:

public class Club implements Serializable {
    //社團數據表主鍵
    private Long id;
    //社團名稱
    private String name;
    //社團描述
    private String desc;

	Getter、Setter、toString....
}

專業實體類:

public class Major implements Serializable {
    //專業數據表主鍵id
    private Long id;
    //專業名稱
    private String name;
    //專業創辦時間
    private Date startTime;
    //年級,如2014級
    private Integer grade;

	Getter、Setter、toString....
}

學生實體類:

public class Student implements Serializable {
    //數據表主鍵id
    private Long id;
    //姓名
    private String name;
    //性別:1,男;2女
    private Integer sex;
    //年齡
    private Integer age;
    //學號
    private String codeNumber;
    //所加入的社團
    private List<Club> clubs;
    //班級
    private Major major;
	
	Getter、Setter、toString....
}

4.2 數據表創建

在這裏我們需要三個數據表,分別是:

  • major表,用於表示專業
  • club表,用於表示社團
  • student表,用於表示學生
  • student_club表,中間表,用於表示學生表與社團表之間的一對多關係

三個表的模型如下:
在這裏插入圖片描述
數據表創建以及導入的sql語句如下:

創建表的SQL語句

CREATE TABLE `club` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `desc` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

CREATE TABLE `major` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `start_time` timestamp NULL DEFAULT NULL,
  `grade` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `student` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `sex` tinyint(4) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `code_number` varchar(64) DEFAULT NULL,
  `major_id` bigint(20) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_major` (`major_id`),
  CONSTRAINT `fk_major` FOREIGN KEY (`major_id`) REFERENCES `major` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

CREATE TABLE `student_club` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `student_id` bigint(20) NOT NULL,
  `club_id` bigint(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `fk_student` (`student_id`),
  KEY `fk_club` (`club_id`),
  CONSTRAINT `fk_club` FOREIGN KEY (`club_id`) REFERENCES `club` (`id`),
  CONSTRAINT `fk_student` FOREIGN KEY (`student_id`) REFERENCES `student` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

導入數據的SQL語句

INSERT INTO `club`(`id`, `name`, `desc`) VALUES (1, '電子科技創新協會', '電子愛好者聚集地');
INSERT INTO `club`(`id`, `name`, `desc`) VALUES (2, '計算機協會', '程序員聚集地');
INSERT INTO `club`(`id`, `name`, `desc`) VALUES (3, '輪滑社', '滑樣年華');
INSERT INTO `club`(`id`, `name`, `desc`) VALUES (4, '橋牌協會', '橋牌錦標賽等你來挑戰');
INSERT INTO `club`(`id`, `name`, `desc`) VALUES (5, '攝影協會', '攝影窮三代,想好了再來');

INSERT INTO `major`(`id`, `name`, `start_time`, `grade`) VALUES (1, '電子信息工程', '1990-06-21 00:00:00', 2014);
INSERT INTO `major`(`id`, `name`, `start_time`, `grade`) VALUES (2, '電子信息工程', '1990-06-21 00:00:00', 2015);
INSERT INTO `major`(`id`, `name`, `start_time`, `grade`) VALUES (3, '計算機科學與技術', '2002-05-10 00:00:00', 2014);
INSERT INTO `major`(`id`, `name`, `start_time`, `grade`) VALUES (4, '通信工程', '2002-05-10 00:00:00', 2016);

INSERT INTO `student`(`id`, `name`, `sex`, `age`, `code_number`, `major_id`) VALUES (3, '張三', 1, 21, '2016100001', 4);
INSERT INTO `student`(`id`, `name`, `sex`, `age`, `code_number`, `major_id`) VALUES (4, '李四', 2, 23, '2015100001', 2);
INSERT INTO `student`(`id`, `name`, `sex`, `age`, `code_number`, `major_id`) VALUES (5, '王五', 1, 24, '2014010012', 3);
INSERT INTO `student`(`id`, `name`, `sex`, `age`, `code_number`, `major_id`) VALUES (6, '李明', 1, 24, '2014101003', 1);

INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (3, 3, 2);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (4, 3, 5);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (6, 4, 1);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (7, 5, 3);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (8, 5, 4);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (9, 5, 5);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (10, 6, 1);
INSERT INTO `student_club`(`id`, `student_id`, `club_id`) VALUES (11, 6, 2);

數據表已經創建好了,數據也有了,接下來就是寫SQL語句根據id把student的信息全部查出來。如果你對數據庫的多表查詢還不太瞭解,可以看本人的這篇博客學習。

4.3 查詢語句編寫

SELECT
	student.id AS id,
	student.age AS age,
	student.sex AS sex,
	student.code_number AS code_number,
	student.NAME AS name,
	major.id AS major_id,
	major.NAME AS major_name,
	major.start_time AS major_start_time,
	major.grade AS grade,
	club.id AS club_id,
	club.name AS club_name,
	club.desc AS club_desc
FROM
	student
	JOIN major ON student.major_id = major.id AND student.id = 5
	LEFT JOIN student_club ON student.id = student_club.student_id
	LEFT JOIN club ON student_club.club_id = club.id

查詢結果:
在這裏插入圖片描述
既然SQL查詢語句確認了,接下來就開始寫數據訪問層代碼了。

4.4 數據訪問層代碼編寫

StudentMapper.java接口文件代碼:

@Mapper
public interface StudentMapper {
    Student findById(@Param("stuId") Long id);
}

StudentMapper.xml代碼:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--namespace對應Mapper接口的全類名,這樣就可以自動匹配上-->
<mapper namespace="com.yky.springboot.mapper.StudentMapper">

    <!--club實體的映射文件,應該寫在ClubMapper.xml文件中的,爲了好演示,寫在了這裏-->
    <resultMap id="clubMap" type="Club">
        <id property="id" column="club_id"/>
        <result property="name" column="club_name"/>
        <result property="desc" column="club_desc"/>
    </resultMap>
    
    <resultMap id="stuMap" type="Student">
        <id property="id" column="id"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <result property="codeNumber" column="code_number"/>
        <result property="name" column="name"/>
        <!--可以直接寫javaType,標籤體內的寫法就是resultMap的標籤體寫法-->
        <association property="major" javaType="Major">
            <id property="id" column="major_id"/>
            <result property="name" column="major_name"/>
            <result property="startTime" column="major_start_time"/>
            <result property="grade" column="grade"/>
        </association>
        <!--也可以直接寫resultMap,指定已有的resultMap-->
        <collection property="clubs" resultMap="clubMap"/>
    </resultMap>

    <select id="findById" resultMap="stuMap">
        SELECT
            student.id AS id,
            student.age AS age,
            student.sex AS sex,
            student.code_number AS code_number,
            student.NAME AS name,
            major.id AS major_id,
            major.NAME AS major_name,
            major.start_time AS major_start_time,
            major.grade AS grade,
            club.id AS club_id,
            club.name AS club_name,
            club.desc AS club_desc
        FROM
            student
            JOIN major ON student.major_id = major.id AND student.id = #{stuId}
            LEFT JOIN student_club ON student.id = student_club.student_id
            LEFT JOIN club ON student_club.club_id = club.id
    </select>
</mapper>

4.5 單元測試

單元測試代碼:

@SpringBootTest
class StudentMapperTest {

    @Autowired
    StudentMapper studentMapper;

    @Test
    void findById() {
        Student student = studentMapper.findById(5L);

        System.out.println(student);

    }
}

輸出結果:
在這裏插入圖片描述

5.多條語句組合實現多表關聯映射

在第4節中,我們用一條語句實現了多表聯合查詢操作。其實,並非只有這一條途徑可進行多表關聯。resultMap的強大、靈活,可以讓我們通過組合多個數據訪問層的查詢方法來實現多表關聯操作。

這種方式具有以下特點。

優點:

  • 不必使用多表連接查詢語句去查詢多表,只需要查詢當前表即可
  • 按需調用所關聯表的查詢語句,去查詢所關聯的表
  • 把單條語句進行了分解,不容易出錯,且更簡潔

缺點:

  • 可能沒有單條語句效率高(單次查詢通過執行多條SQL語句實現)

下面來實現一下:

5.1 實現Major實體的數據訪問層

MajorMapper.java接口:

@Mapper
public interface MajorMapper {
    Major findById(@Param("id") Long id);
}

MajorMapper.xml:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!--namespace對應Mapper接口的全類名,這樣就可以自動匹配上-->
<mapper namespace="com.yky.springboot.mapper.MajorMapper">
    <select id="findById" resultType="Major">
        SELECT * FROM major WHERE id = #{id}
    </select>
</mapper>

5.2 實現Club實體的數據訪問層

ClubMapper.java接口:

@Mapper
public interface ClubMapper {
    Club findByStudentId(@Param("sid") Long sid);
}

ClubMapper.xml:

<!--namespace對應Mapper接口的全類名,這樣就可以自動匹配上-->
<mapper namespace="com.yky.springboot.mapper.ClubMapper">
    <select id="findByStudentId" resultType="Club">
        SELECT
            club.id,
            club.name,
            club.desc
        FROM
            student_club,
            club
        WHERE
            student_club.club_id = club.id
            AND student_club.student_id = #{sid}
    </select>
</mapper>

5.3 Student數據訪問層調用Major、Club代碼

StudentMapper.java接口新增加的方法:

Student lazyFindById(@Param("stuId") Long id);

StudentMapper.xml新增加的映射:

	<resultMap id="lazyStuMap" type="Student">
        <id property="id" column="id"/>
        <result property="age" column="age"/>
        <result property="sex" column="sex"/>
        <result property="codeNumber" column="code_number"/>
        <result property="name" column="name"/>
        <!--
        com.yky.springboot.mapper.MajorMapper.findById:namespace.方法名 調用相應的查詢語句,column指定給方法傳入的參數
		property="major":將方法的調用結果綁定到major屬性上
		column="major_id":傳入findById方法的參數
		-->
        <association select="com.yky.springboot.mapper.MajorMapper.findById" property="major" column="major_id"/>
   		<!--解釋同上-->
        <collection select="com.yky.springboot.mapper.ClubMapper.findByStudentId" property="clubs" column="id"/>
    </resultMap>

    <select id="lazyFindById" resultMap="lazyStuMap">
        SELECT student.* FROM student WHERE id = #{stuId}
    </select>

5.4 單元測試

單元測試代碼:

    @Test
    void lazyFindId() {
        Student student = studentMapper.lazyFindById(5L);
        System.out.println("--------------------------分割線-----------------------");
        System.out.println(student);
    }

輸出結果:
在這裏插入圖片描述
可以看到,當前程序運行結果與第三節的運行結果一致,不同的是這一次查詢是通過執行三條SQL語句完成的。

6. 自動映射

在簡單的場景下,MyBatis 可以爲你自動映射查詢結果(resultType)。但如果遇到複雜的場景,你需要構建一個結果映射(resultMap)。Mybatis支持兩種方式混合使用。

有三種自動映射等級:

  • NONE - 禁用自動映射。僅對手動映射的屬性進行映射。
  • PARTIAL - 對除在內部定義了嵌套結果映射(也就是連接的屬性)以外的屬性進行映射
  • FULL - 自動映射所有屬性。

Mybatis默認使用PARTIAL等級,即如果程序員針對某些屬性指定了映射關係,則按照指定的來。沒有指定的屬性,纔會自動映射。當自動映射查詢結果時,MyBatis 會獲取結果中返回的列名並在 Java 類中查找相同名字的屬性(忽略大小寫)

因此,第5節的resultMap可以寫成下面這樣的:

    <resultMap id="lazyStuMap" type="Student">
        <id property="id" column="id"/>
<!--        <result property="age" column="age"/>-->
<!--        <result property="sex" column="sex"/>-->
        <result property="codeNumber" column="code_number"/>
<!--        <result property="name" column="name"/>-->
        <!--可以直接寫javaType,標籤體內的寫法就是resultMap的標籤體寫法-->
        <association select="com.yky.springboot.mapper.MajorMapper.findById" property="major" column="major_id"/>
        <!--也可以直接寫resultMap,指定已有的resultMap-->
        <collection select="com.yky.springboot.mapper.ClubMapper.findByStudentId" property="clubs" column="id"/>
    </resultMap>

age、sex、name字段完全可以讓Mybatis爲我們自動映射。

我們還可以看到codeNumber與code_number這樣的。通常數據庫列使用小寫字母組成的單詞命名,單詞間用下劃線分隔;而 Java 屬性一般遵循駝峯命名法約定。爲了在這兩種命名方式之間啓用自動映射,需要將 mapUnderscoreToCamelCase 設置爲 true。

mybatis-config.xml代碼如下:

<configuration>

    <!-- 開啓駝峯命名自動映射-->
    <!--設置啓用數據庫字段下劃線映射到java對象的駝峯式命名屬性,默認爲false-->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true"/>
    </settings>
    
    <!--配置別名-->
    <typeAliases>
        <typeAlias type="com.yky.springboot.entities.User" alias="User"></typeAlias>
        <typeAlias type="com.yky.springboot.entities.Student" alias="Student"></typeAlias>
        <typeAlias type="com.yky.springboot.entities.Major" alias="Major"></typeAlias>
        <typeAlias type="com.yky.springboot.entities.Club" alias="Club"></typeAlias>
    </typeAliases>

</configuration>

開啓了駝峯命名後,上面寫的resultMap可以再精簡:

    <resultMap id="lazyStuMap" type="Student">
        <id property="id" column="id"/>
<!--        <result property="age" column="age"/>-->
<!--        <result property="sex" column="sex"/>-->
<!--        <result property="codeNumber" column="code_number"/>-->
<!--        <result property="name" column="name"/>-->
        <!--可以直接寫javaType,標籤體內的寫法就是resultMap的標籤體寫法-->
        <association select="com.yky.springboot.mapper.MajorMapper.findById" property="major" column="major_id"/>
        <!--也可以直接寫resultMap,指定已有的resultMap-->
        <collection select="com.yky.springboot.mapper.ClubMapper.findByStudentId" property="clubs" column="id"/>
    </resultMap>

無論設置的自動映射等級是哪種,你都可以通過在結果映射上設置 autoMapping 屬性來爲指定的結果映射設置啓用/禁用自動映射。
在這裏插入圖片描述

7. 寫在最後

關於結果映射就講到這裏,這已經涵蓋了絕大多數的應用場景。至於本文沒講到的,可以去官網查看相關說明文檔。歡迎大家在評論區討論,也歡迎大家指出本文章的錯誤。如有疑問,可直接在評論區留言,我看到後就會回覆。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章