Java之Mybatis

官方文檔

簡介

什麼是Mybatis

MyBatis 是一款優秀的持久層框架,它支持自定義 SQL、存儲過程以及高級映射。MyBatis 免除了幾乎所有的 JDBC 代碼以及設置參數和獲取結果集的工作。MyBatis 可以通過簡單的 XML 或註解來配置和映射原始類型、接口和 Java POJO(Plain Old Java Objects,普通老式 Java 對象)爲數據庫中的記錄。

<!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
<dependency>
    <groupId>org.mybatis</groupId>
    <artifactId>mybatis</artifactId>
    <version>3.5.2</version>
</dependency>
持久化

數據持久化

  • 持久化就是將程序的數據在持久狀態和瞬間狀態轉化的過程
  • 內存:斷電即失
  • 數據庫(jdbc) io文件持久化

爲什麼需要持久化
有一些對象不能丟掉

持久層
  • 完成持久化工作的代碼塊
  • 層界限十分明顯

第一個Mybatis程序

    <dependencies>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>
public interface UserDao {
    List<User> getUserList();
}
// 接口實現類由原來的UserDaoImpl轉變爲一個Mapper配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wang.dao.UserDao">
    <select id="getUserList" resultType="com.wang.pojo.User">
    select * from mybatis.user
  </select>
</mapper>

Test

public class UserMapperTest {


    @Test
    public void getUserList() {
        //第一步 獲取sqlSession對象
        SqlSession sqlSession = null;

        try {
            //第二步 方式一:getMapper
            sqlSession = MybatisUtils.getSqlSession();
            UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
            List<User> userList = userMapper.getUserList();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            sqlSession.close();
        }
        
        // 方式二:
//        誠然,這種方式能夠正常工作,對使用舊版本 MyBatis 的用戶來說也比較熟悉。
//        但現在有了一種更簡潔的方式——使用和指定語句的參數和返回值相匹配的接口(比如 BlogMapper.class),現在你的代碼不僅更清晰,
//        更加類型安全,還不用擔心可能出錯的字符串字面值以及強制類型轉換。
//        List<User> userList1 = sqlSession.selectList("com.wang.dao.userMapper.getUserList"); 不推薦使用
    }
}

注意

org.apache.ibatis.binding.BindingException: Type interface com.wang.dao.UserMapper is not known to the MapperRegistry.

	at org.apache.ibatis.binding.MapperRegistry.getMapper(MapperRegistry.java:47)
	at org.apache.ibatis.session.Configuration.getMapper(Configuration.java:779)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.getMapper(DefaultSqlSession.java:291)
	at com.wang.dao.UserMapperTest.getUserList(UserMapperTest.java:18)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
	at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
	at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
	at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
	at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)
	at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:57)
	at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
	at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
	at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
	at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
	at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
	at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
	at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
	at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
	at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:33)
	at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:230)
	at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:58)

每一個Mapper.xml 都需要在Mybatis核心配置文件中註冊

    <mappers>
        <mapper resource="com/wang/dao/UserMapper.xml"></mapper>
    </mappers>

註冊之後可能找不到Mapper.xml ,因爲Mapper.xml 沒放在maven默認掃描的目錄下

    <!--    在build中配置resources 來防止我們資源導出失敗的問題-->
    <build>
        <resources>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.properties</include>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>

SqlSessionFactoryBuilder
這個類可以被實例化、使用和丟棄,一旦創建了 SqlSessionFactory,就不再需要它了。 因此 SqlSessionFactoryBuilder 實例的最佳作用域是方法作用域(也就是局部方法變量)。 你可以重用 SqlSessionFactoryBuilder 來創建多個 SqlSessionFactory 實例,但最好還是不要一直保留着它,以保證所有的 XML 解析資源可以被釋放給更重要的事情。

SqlSessionFactory
SqlSessionFactory 一旦被創建就應該在應用的運行期間一直存在,沒有任何理由丟棄它或重新創建另一個實例。 使用 SqlSessionFactory 的最佳實踐是在應用運行期間不要重複創建多次,多次重建 SqlSessionFactory 被視爲一種代碼“壞習慣”。因此 SqlSessionFactory 的最佳作用域是應用作用域。 有很多方法可以做到,最簡單的就是使用單例模式或者靜態單例模式。

SqlSession
每個線程都應該有它自己的 SqlSession 實例。SqlSession 的實例不是線程安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。 絕對不能將 SqlSession 實例的引用放在一個類的靜態域,甚至一個類的實例變量也不行。 也絕不能將 SqlSession 實例的引用放在任何類型的託管作用域中,比如 Servlet 框架中的 HttpSession。 如果你現在正在使用一種 Web 框架,考慮將 SqlSession 放在一個和 HTTP 請求相似的作用域中。 換句話說,每次收到 HTTP 請求,就可以打開一個 SqlSession,返回一個響應後,就關閉它。 這個關閉操作很重要,爲了確保每次都能執行關閉操作,你應該把這個關閉操作放到 finally 塊中。 下面的示例就是一個確保 SqlSession 關閉的標準模式:

try (SqlSession session = sqlSessionFactory.openSession()) {
  // 你的應用邏輯代碼
}

CRUD

namespace

namespace中的包名要和Dao/mapper接口的包名一致

select

選擇,查詢語句

    <select id="getUserList" resultType="com.wang.pojo.User">
    select * from mybatis.user
  </select>
  • id就是對應的namespace的方法名
  • resultType是SQL語句執行的返回值
insert
update
delete
    User getUserById(int id);

    void addUser(User user);

    void updateUser(User user);

    void deleteUser(int id);
    <!--    對象中的屬性 可以直接取出來-->
    <insert id="addUser" parameterType="com.wang.pojo.User">
        insert into mybatis.user (id,name,pwd) values (#{id},#{name},#{pwd});
    </insert>
    <update id="updateUser" parameterType="com.wang.pojo.User">
        update mybatis.user set name=#{name},pwd=#{pwd} where id=#{id};
    </update>
    <delete id="deleteUser" parameterType="int">
        delete from mybatis.user where id = #{id};
    </delete>
    <select id="getUserList" resultType="com.wang.pojo.User">
    select * from mybatis.user;
  	</select>
    <select id="getUserById" parameterType="int" resultType="com.wang.pojo.User">
        select * from mybatis.user where id = #{id};
    </select>
 @Test
    public void getUserById() {
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = mapper.getUserById(1);
        sqlSession.close();
    }

    //增刪改需要提交事務
    @Test
    public void addUser() {
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = new User(4, "D", "123456");
        mapper.addUser(user);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void updateUser(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        User user = new User(4, "DDD", "123456");
        mapper.updateUser(user);
        sqlSession.commit();
        sqlSession.close();
    }

    @Test
    public void deleteUser(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        mapper.deleteUser(4);
        sqlSession.commit();
        sqlSession.close();
    }
萬能Map

假設實體類或者數據庫中的表字段或者參數過多,我們應當考慮使用Map

    void addUserWithMap(Map<String,Object> map);
    <insert id="addUserWithMap" parameterType="map">
        insert into mybatis.user (id,name,pwd) values (#{id},#{name},#{pwd});
    </insert>
    @Test
    public void addUserWithMap() {
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        Map<String,Object> map = new HashMap<String, Object>();
        map.put("id",5);
        map.put("name","E");
        map.put("pwd","123456");
        mapper.addUserWithMap(map);
        sqlSession.commit();
        sqlSession.close();
    }

Map傳遞參數 直接在sql中取key即可
對象傳遞參數 直接在sql中取對象的屬性即可
只有一個基本類型參數的情況下 可以直接在sql取

配置解析

核心配置文件
  • mybatis-config.xml
  • Mybatis的配置文件
configuration(配置)
properties(屬性)
settings(設置)
typeAliases(類型別名)
typeHandlers(類型處理器)
objectFactory(對象工廠)
plugins(插件)
environments(環境配置)
environment(環境變量)
transactionManager(事務管理器)
dataSource(數據源)
databaseIdProvider(數據庫廠商標識)
mappers(映射器)
environments

MyBatis 可以配置成適應多種環境,這種機制有助於將 SQL 映射應用於多種數據庫之中, 現實情況下有多種理由需要這麼做。例如,開發、測試和生產環境需要有不同的配置;或者想在具有相同 Schema 的多個生產數據庫中使用相同的 SQL 映射。還有許多類似的使用場景。
不過要記住:儘管可以配置多個環境,但每個 SqlSessionFactory 實例只能選擇一種環境。
所以,如果你想連接兩個數據庫,就需要創建兩個 SqlSessionFactory 實例,每個數據庫對應一個。而如果是三個數據庫,就需要三個實例,依此類推,記起來很簡單:
每個數據庫對應一個 SqlSessionFactory 實例
Mybatis默認的事務管理器是JDBC,連接池:POOLED

properties

我們可以通過properties屬性來實現引用配置文件
這些屬性可以在外部進行配置,並可以進行動態替換。你既可以在典型的 Java 屬性文件中配置這些屬性,也可以在 properties 元素的子元素中設置。【db.properties】
編寫一個配置文件
db.properties

driver=com.mysql.jdbc.Driver
url=jdbc:mysql://aliyun:3306/mybatis?useUnicode=true&characterEncoding=UTF-8
username=root
password=Lndlwyh919190.
  • 可以引入外部文件
  • 可以在其中增加一些屬性配置
  • 如果兩個文件有同一字段,優先使用外部配置文件的
typeAliases

類型別名可爲 Java 類型設置一個縮寫名字。 它僅用於 XML 配置,意在降低冗餘的全限定類名書寫。

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
  <typeAlias alias="Comment" type="domain.blog.Comment"/>
  <typeAlias alias="Post" type="domain.blog.Post"/>
  <typeAlias alias="Section" type="domain.blog.Section"/>
  <typeAlias alias="Tag" type="domain.blog.Tag"/>
</typeAliases>

也可以指定一個包名,MyBatis 會在包名下面搜索需要的 Java Bean,比如:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>

每一個在包 domain.blog 中的 Java Bean,在沒有註解的情況下,會使用 Bean 的首字母小寫的非限定類名來作爲它的別名。 比如 domain.blog.Author 的別名爲 author;若有註解,則別名爲其註解值。見下面的例子

@Alias("author")
public class Author {
    ...
}
settings

在這裏插入圖片描述
在這裏插入圖片描述
在這裏插入圖片描述

其他配置
  • typeHandlers(類型處理器)
  • objectFactory(對象工廠)
  • plugins
    • mybatis-generator-core
    • mybatis-plus
    • 通用mapper
映射器 mappers

既然 MyBatis 的行爲已經由上述元素配置完了,我們現在就要來定義 SQL 映射語句了。 但首先,我們需要告訴 MyBatis 到哪裏去找到這些語句。 在自動查找資源方面,Java 並沒有提供一個很好的解決方案,所以最好的辦法是直接告訴 MyBatis 到哪裏去找映射文件。 你可以使用相對於類路徑的資源引用,或完全限定資源定位符(包括 file:/// 形式的 URL),或類名和包名等。例如:

<!-- 使用相對於類路徑的資源引用 -->
<!-- 無限制 寫一個綁定一個就行 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口實現類的完全限定類名 -->
<!-- 這個方式注意點:接口和Mapper配置文件必須同名 必須在同一包下 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 將包內的映射器接口實現全部註冊爲映射器 -->
<!-- 這個方式注意點:接口和Mapper配置文件必須同名 必須在同一包下 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
<!-- 使用完全限定資源定位符(URL)  不採用這個方式-->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>

生命週期和作用域

生命週期和作用域是至關重要的,錯誤的使用會導致非常嚴重的併發問題
SqlSessionFactoryBuilder

  • 一旦創建了SqlSessionFactory就不再需要它了
  • 局部變量
    SqlSessionFactory
  • 說白了可以想象爲數據庫連接池
  • SqlSessionFactory 一旦被創建就應該在應用的運行期間一直存在,沒有任何理由丟棄它或重新創建另一個實例,因此 SqlSessionFactory 的最佳作用域是應用作用域,最簡單的就是使用單例模式或者靜態單例模式。
    SqlSession
  • 連接到連接池的一個請求
  • SqlSession 的實例不是線程安全的,因此是不能被共享的,所以它的最佳的作用域是請求或方法作用域。
  • 用完之後需要關閉、

解決屬性名和數據庫字段名不一致的問題

解決方法:

  • SQL語句起別名
select id,name,pwd from User where id = #{id}
改爲 select id,name,pwd as password from User where id = #{id}
  • resultMap
    結果集映射
id name pwd
id name password
    <resultMap id="UserMap" type="User">
        <!--        column 數據庫中的字段 property 實例類中的屬性-->
        <result column="id" property="id"></result>
        <result column="name" property="name"></result>
        <result column="pwd" property="password"></result>
    </resultMap>
      </select>
    <select id="getUserById" resultMap="UserMap">
        select * from mybatis.user where id = #{id};
    </select>
  • resultMap 元素是 MyBatis 中最重要最強大的元素。
  • ResultMap 的設計思想是,對簡單的語句做到零配置,對於複雜一點的語句,只需要描述語句之間的關係就行了。
  • 在學習了上面的知識後,你會發現上面的例子沒有一個需要顯式配置 ResultMap,這就是 ResultMap 的優秀之處——你完全可以不用顯式地配置它們。

日誌

日誌工廠

如果一個數據庫操作出現異常,需要日誌排錯
在這裏插入圖片描述

  • SLF4J
  • LOG4J【掌握】
  • LOG4J2
  • JDK_LOGGING
  • COMMONS_LOGGING
  • STDOUT_LOGGING【掌握】
  • NO_LOGGING
    STDOUT_LOGGING標準日誌輸出
    在mybatis核心配置文件中配置日誌:
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

在這裏插入圖片描述
LOG4J

  1. 先導入LOG4J的包
  2. log4j.properties
# 將等級爲DEBUG的日誌信息輸出到console和file兩個目的地,console和file的定義在下面的代碼
log4j.rootLogger=DEBUG,console,file
# 控制檯輸出的相關設置
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.Target=System.out
log4j.appender.console.Threshold=DEBUG
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=[%c]-%m%n
# 文件輸出的相關設置
log4j.appender.file=org.apache.log4j.RollingFileAppender
log4j.appender.file.File=./log/app.log
log4j.appender.file.MaxFileSize=10mb
log4j.appender.file.Threshold=DEBUG
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=[%p][%d{yy-MM-dd}][%c]%m%n
# 日誌輸出級別
log4j.logger.org.mybatis=DEBUG
log4j.logger.java.sql=DEBUG
log4j.logger.java.sql.Statement=DEBUG
log4j.logger.java.sql.ResultSet=DEBUG
log4j.logger.java.sql.PreparedStatement=DEBUG
  1. 配置log4j爲日誌的實現
    <settings>
<!--        <setting name="logImpl" value="STDOUT_LOGGING"/>-->
        <setting name="logImpl" value="LOG4J"/>
    </settings>

**簡單使用** 1. 導包 import org.apache.log4j.Logger; 2. 日誌對象 參數爲當前類的class static Logger logger = Logger.getLogger(UserMapperTest.class); 3. 日誌級別 ```xml [INFO][20-06-05][com.wang.dao.UserMapperTest]info:testLog4j [DEBUG][20-06-05][com.wang.dao.UserMapperTest]debug:testLog4j [ERROR][20-06-05][com.wang.dao.UserMapperTest]error:testLog4j ``` #### 分頁 limit分頁 select * from user limit 0,2 -------0 1 select * from user limit n -------0到n

使用mybatis分頁

  1. 接口
  List<User> getUserByLimit(Map<String,Object> map);
  1. mapper.xml
    <select id="getUserByLimit" parameterType="map" resultType="User">
        select * from  mybatis.user limit #{startIndex},#{pageSize};
    </select>
分頁插件

mybatis pageHelper

使用註解開發 底層主要應用反射 動態代理

面向接口編程

Mybatis詳細的執行流程

在這裏插入圖片描述

CRUD

我們可以在工具類創建的時候實現自動提交事務

 public static SqlSession getSqlSession() {
        return sqlSessionFactory.openSession(true);
    }

編寫接口 增加註解 測試類(注意 將接口註冊到mybatis-config.xml)

    <!--    綁定接口-->
    <mappers>
        <mapper class="com.wang.dao.UserMapper"></mapper>
    </mappers>

關於@Param()註解

  • 基本類型的參數或者String類型 需要加上
  • 引用類型不需要加
  • 如果只有一個基本類型的話 可以不加 但是建議加上
  • 我們在SQL中引用的就是我們這裏的@Param()中設定的屬性名

#{} 和 ${}的區別
#{}可以防止SQL注入 類似PreparedStatement
${}會有SQL注入 類似Statement

一對多 多對一

看Mybatis官網

動態SQL

動態SQL:根據不同的條件生成不同的SQL語句

緩存

一次查詢的結果,暫存到內存—> 緩存

一級緩存

一級緩存也叫本地緩存 SqlSession緩存

  • 與數據庫同一次會話期間查詢到的數據會放到本地緩存中
  • 以後如果需要獲取相同的數據,直接從緩存中獲取
sqlSession = MybatisUtils.getSqlSession();
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
List<User> userList = userMapper.getUserList();
List<User> userList1 = userMapper.getUserList(); //緩存中獲取
sqlSession.close();
二級緩存

默認情況下,只啓用了本地的會話緩存,它僅僅對一個會話中的數據進行緩存。 要啓用全局的二級緩存,只需要在你的 SQL 映射文件中添加一行

<cache/>

基本上就是這樣。這個簡單語句的效果如下:

映射語句文件中的所有 select 語句的結果將會被緩存。
映射語句文件中的所有 insert、update 和 delete 語句會刷新緩存。
緩存會使用最近最少使用算法(LRU, Least Recently Used)算法來清除不需要的緩存。
緩存不會定時進行刷新(也就是說,沒有刷新間隔)。
緩存會保存列表或對象(無論查詢方法返回哪種)的 1024 個引用。
緩存會被視爲讀/寫緩存,這意味着獲取到的對象並不是共享的,可以安全地被調用者修改,而不干擾其他調用者或線程所做的潛在修改。
提示 緩存只作用於 cache 標籤所在的映射文件中的語句。如果你混合使用 Java API 和 XML 映射文件,在共用接口中的語句將不會被默認緩存。你需要使用 @CacheNamespaceRef 註解指定緩存作用域。
這些屬性可以通過 cache 元素的屬性來修改。比如:

<cache
  eviction="FIFO"
  flushInterval="60000"
  size="512"
  readOnly="true"/>

這個更高級的配置創建了一個 FIFO 緩存,每隔 60 秒刷新,最多可以存儲結果對象或列表的 512 個引用,而且返回的對象被認爲是隻讀的,因此對它們進行修改可能會在不同線程中的調用者產生衝突。
可用的清除策略有:
LRU – 最近最少使用:移除最長時間不被使用的對象。
FIFO – 先進先出:按對象進入緩存的順序來移除它們。
SOFT – 軟引用:基於垃圾回收器狀態和軟引用規則移除對象。
WEAK – 弱引用:更積極地基於垃圾收集器狀態和弱引用規則移除對象。
默認的清除策略是 LRU。

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