Mybatis-Plus 常用操作

MyBatis-Plus系列推薦閱讀順序:


本文目錄結(jié)構(gòu)

一屈梁、SQL日志開關(guān)
二、常用注解
三殷蛇、代碼生成器
四转锈、分頁查詢
五咙冗、Mybatis-Plus Wrapper
六购裙、自動填充數(shù)據(jù)功能
七淆储、邏輯刪除
八坯台、樂觀鎖


一聂沙、SQL日志開關(guān)

配置文件application.properties秆麸,增加最后一行,執(zhí)行時會打印出 sql 語句及汉。

spring.application.name=mybatis-plus
# 應(yīng)用服務(wù) WEB 訪問端口
server.port=8080
####數(shù)據(jù)庫連接池###
spring.datasource.url=jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8
spring.datasource.username=guo
spring.datasource.password=205010guo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

####輸出sql日志###
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl

類似JPA的日志輸出配置:

jpa:
  show-sql:true#打印SQL沮趣。

二、常用注解

注解說明的官方文檔:https://mybatis.plus/guide/annotation.html

2.1【@TableName 】

    @TableName               用于定義表名
注:
    常用屬性:
        value                用于定義表名

2.2【@TableId】

    @TableId                 用于定義表的主鍵
注:
    常用屬性:
        value           用于定義主鍵字段名
        type            用于定義主鍵類型(主鍵策略 IdType)

   主鍵策略:
      IdType.AUTO          主鍵自增豁生,系統(tǒng)分配兔毒,不需要手動輸入
      IdType.NONE          未設(shè)置主鍵
      IdType.INPUT         需要自己輸入 主鍵值。
      IdType.ASSIGN_ID     系統(tǒng)分配 ID甸箱,用于數(shù)值型數(shù)據(jù)(Long育叁,對應(yīng) mysql 中 BIGINT 類型)。
      IdType.ASSIGN_UUID   系統(tǒng)分配 UUID芍殖,用于字符串型數(shù)據(jù)(String豪嗽,對應(yīng) mysql 中 varchar(32) 類型)。

2.3【@TableField】

    @TableField            用于定義表的非主鍵字段豌骏。
注:
    常用屬性:
        value                用于定義非主鍵字段名
        exist                用于指明是否為數(shù)據(jù)表的字段龟梦, true 表示是,false 為不是窃躲。
        fill                 用于指定字段填充策略(FieldFill)计贰。
        
    字段填充策略:(一般用于填充 創(chuàng)建時間、修改時間等字段)
        FieldFill.DEFAULT         默認(rèn)不填充
        FieldFill.INSERT          插入時填充
        FieldFill.UPDATE          更新時填充
        FieldFill.INSERT_UPDATE   插入蒂窒、更新時填充躁倒。

2.4【@TableLogic】

    @TableLogic           用于定義表的字段進(jìn)行邏輯刪除(非物理刪除)
注:
    常用屬性:
        value            用于定義未刪除時字段的值
        delval           用于定義刪除時字段的值

2.5【@Version】

    @Version             用于字段實(shí)現(xiàn)樂觀鎖

三荞怒、代碼生成器

3.1 AutoGenerator 簡介

AutoGenerator 是 MyBatis-Plus 的代碼生成器,通過 AutoGenerator 可以快速生成 Entity秧秉、Mapper褐桌、Mapper XML、Service象迎、Controller 等各個模塊的代碼荧嵌,極大的提升了開發(fā)效率±剩  與 mybatis 中的 mybatis-generator-core 類似啦撮。

3.2 添加依賴

        <!--  代碼生成器 依賴-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>${mp.version}</version>
        </dependency>
        <!-- 添加 模板引擎 依賴 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.2</version>
        </dependency>

3.3 生成器代碼分析

Step1:
  創(chuàng)建一個 代碼生成器。用于生成代碼拇舀。
  此處不用修改逻族。

// Step1:代碼生成器
AutoGenerator mpg = new AutoGenerator();

Step2:
  配置全局信息。指定代碼輸出路徑骄崩,以及包名聘鳞、作者等信息。
  此處按需添加要拂,projectPath 需要修改抠璃,setAuthor 需要修改。

// Step2:全局配置
GlobalConfig gc = new GlobalConfig(); 
// 填寫代碼生成的目錄(需要修改)
String projectPath = "E:\\myProject\\test\\test_mybatis_plus"; 
// 拼接出代碼最終輸出的目錄
gc.setOutputDir(projectPath + "/src/main/java"); 
// 配置開發(fā)者信息(可選)(需要修改)
gc.setAuthor("郭秀志 jbcode@126.com");
 // 配置是否打開目錄脱惰,false 為不打開(可選)
gc.setOpen(false); 
// 實(shí)體屬性 Swagger2 注解搏嗡,添加 Swagger 依賴,開啟 Swagger2 模式(可選)
//gc.setSwagger2(true);
// 重新生成文件時是否覆蓋拉一,false 表示不覆蓋(可選)
gc.setFileOverride(false);
 // 配置主鍵生成策略采盒,此處為 ASSIGN_ID(可選)
gc.setIdType(IdType.ASSIGN_ID); 
// 配置日期類型,此處為 ONLY_DATE(可選)
gc.setDateType(DateType.ONLY_DATE); 
// 默認(rèn)生成的 service 會有 I 前綴
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);

Step3:
配置數(shù)據(jù)源信息蔚润。用于指定 需要生成代碼的 數(shù)據(jù)倉庫磅氨、數(shù)據(jù)表。
setUrl嫡纠、setDriverName烦租、setUsername、setPassword均需修改除盏。

// Step3:數(shù)據(jù)源配置(需要修改)
DataSourceConfig dsc = new DataSourceConfig(); 
// 配置數(shù)據(jù)庫 url 地址
dsc.setUrl("jdbc:mysql://localhost:3306/testMyBatisPlus?useUnicode=true&characterEncoding=utf8");
// dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定數(shù)據(jù)庫名 
// 配置數(shù)據(jù)庫驅(qū)動
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
// 配置數(shù)據(jù)庫連接用戶名
dsc.setUsername("root"); 
// 配置數(shù)據(jù)庫連接密碼
dsc.setPassword("123456");
mpg.setDataSource(dsc);

Step4:
配置包信息叉橱。
setParent、setModuleName均需修改者蠕。其余按需求修改.

// Step:4:包配置
PackageConfig pc = new PackageConfig();
 // 配置父包名(需要修改)
pc.setParent("com.erbadagang.mybatis.plus"); 
// 配置模塊名(需要修改)
//pc.setModuleName("mybatis-plus-starter");
 // 配置 entity 包名
pc.setEntity("entity"); 
// 配置 mapper 包名
pc.setMapper("mapper"); 
// 配置 service 包名
pc.setService("service"); 
// 配置 controller 包名
pc.setController("controller");
mpg.setPackageInfo(pc);

Step5:
配置數(shù)據(jù)表映射信息窃祝。
setInclude 需要修改,其余按實(shí)際開發(fā)修改踱侣。

// Step5:策略配置(數(shù)據(jù)庫表配置)
StrategyConfig strategy = new StrategyConfig(); 
// 指定表名(可以同時操作多個表锌杀,使用 , 隔開)(需要修改)
strategy.setInclude("t_user"); 
// 配置數(shù)據(jù)表與實(shí)體類名之間映射的策略
strategy.setNaming(NamingStrategy.underline_to_camel); 
// 配置數(shù)據(jù)表的字段與實(shí)體類的屬性名之間映射的策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel); 
// 配置 lombok 模式
strategy.setEntityLombokModel(true); 
// 配置 rest 風(fēng)格的控制器(@RestController)
strategy.setRestControllerStyle(true); 
// 配置駝峰轉(zhuǎn)連字符
strategy.setControllerMappingHyphenStyle(true); 
// 配置表前綴甩栈,生成實(shí)體時去除表前綴 
// 此處的表名為 test_mybatis_plus_user,模塊名為 test_mybatis_plus糕再,去除前綴后剩下為 user。
strategy.setTablePrefix(pc.getModuleName() + "_");
mpg.setStrategy(strategy);

t_user建表SQL:

/*
 Navicat Premium Data Transfer

 Source Server         : 上海
 Source Server Type    : MySQL
 Source Server Version : 50636
 Source Host           : 101.133.227.13:3306
 Source Schema         : orders_1

 Target Server Type    : MySQL
 Target Server Version : 50636
 File Encoding         : 65001

 Date: 10/07/2020 16:28:23
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `user_name` varchar(25) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `password` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  `pwd_cipher` varchar(55) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

Step6:
  執(zhí)行代碼生成操作玉转。
  此處不用修改突想。

// Step6:執(zhí)行代碼生成操作
mpg.execute();

完整代碼:

package com.erbadagang.mybatis.plus.mybatisplus;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.DateType;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * AutoGenerationTest作用是:生成Mybatis-plus代碼,AutoGenerator 是 MyBatis-Plus 的代碼生成器,通過 AutoGenerator 可以快速生成 Entity究抓、Mapper猾担、Mapper XML、Service刺下、Controller 等各個模塊的代碼绑嘹,極大的提升了開發(fā)效率。
 *
 * @ClassName: AutoGenerationTest
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/10 15:08
 * @Copyright:
 */
@SpringBootTest
public class AutoGenerationTest {
    @Test
    public void autoGenerate() {
        // Step1:代碼生成器
        AutoGenerator mpg = new AutoGenerator();

        // Step2:全局配置
        GlobalConfig gc = new GlobalConfig();
        // 填寫代碼生成的目錄(需要修改)
        String projectPath = "D:\\dev\\GitRepository\\mybatis-plus-starter";
        // 拼接出代碼最終輸出的目錄
        gc.setOutputDir(projectPath + "/src/main/java");
        // 配置開發(fā)者信息(可選)(需要修改)
        gc.setAuthor("郭秀志 jbcode@126.com");
        // 配置是否打開目錄橘茉,false 為不打開(可選)
        gc.setOpen(false);
        // 實(shí)體屬性 Swagger2 注解工腋,添加 Swagger 依賴,開啟 Swagger2 模式(可選)
        //gc.setSwagger2(true);
        // 重新生成文件時是否覆蓋畅卓,false 表示不覆蓋(可選)
        gc.setFileOverride(false);
        // 配置主鍵生成策略擅腰,此處為 ASSIGN_ID(可選)
        gc.setIdType(IdType.AUTO);
        // 配置日期類型,此處為 ONLY_DATE(可選)
        gc.setDateType(DateType.ONLY_DATE);
        // 默認(rèn)生成的 service 會有 I 前綴
        gc.setServiceName("I%sService");
        mpg.setGlobalConfig(gc);

        // Step3:數(shù)據(jù)源配置(需要修改)
        DataSourceConfig dsc = new DataSourceConfig();
        // 配置數(shù)據(jù)庫 url 地址
        dsc.setUrl("jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8");
        // dsc.setSchemaName("testMyBatisPlus"); // 可以直接在 url 中指定數(shù)據(jù)庫名
        // 配置數(shù)據(jù)庫驅(qū)動
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        // 配置數(shù)據(jù)庫連接用戶名
        dsc.setUsername("guo");
        // 配置數(shù)據(jù)庫連接密碼
        dsc.setPassword("205010guo");
        mpg.setDataSource(dsc);

        // Step:4:包配置
        PackageConfig pc = new PackageConfig();
        // 配置父包名(需要修改)
        pc.setParent("com.erbadagang.mybatis.plus.mybatisplus");
        // 配置模塊名(需要修改)
        //pc.setModuleName("mybatis-plus-starter");
        // 配置 entity 包名
        pc.setEntity("entity");
        // 配置 mapper 包名
        pc.setMapper("mapper");
        // 配置 service 包名
        pc.setService("service");
        // 配置 controller 包名
        pc.setController("controller");
        mpg.setPackageInfo(pc);

        // Step5:策略配置(數(shù)據(jù)庫表配置)
        StrategyConfig strategy = new StrategyConfig();
        // 指定表名(可以同時操作多個表翁潘,使用 , 隔開)(需要修改)
        strategy.setInclude("t_user");//表名t_user
        // 配置數(shù)據(jù)表與實(shí)體類名之間映射的策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        // 配置數(shù)據(jù)表的字段與實(shí)體類的屬性名之間映射的策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        // 配置 lombok 模式
        strategy.setEntityLombokModel(true);
        // 配置 rest 風(fēng)格的控制器(@RestController)
        strategy.setRestControllerStyle(true);
        // 配置駝峰轉(zhuǎn)連字符
        strategy.setControllerMappingHyphenStyle(true);
        // 配置表前綴趁冈,生成實(shí)體時去除表前綴
        // 此處的表名為 test_mybatis_plus_user,模塊名為 test_mybatis_plus拜马,去除前綴后剩下為 user渗勘。
        strategy.setTablePrefix(pc.getModuleName() + "_");
        mpg.setStrategy(strategy);

        // Step6:執(zhí)行代碼生成操作
        mpg.execute();
    }

}

3.4 測試生成的service

由于生成的Service接口及實(shí)現(xiàn)類有些問題,需要稍為改造一下:

  • Service接口:public interface ITUserService extends IService<TUser> 增加泛型:public interface ITUserService<TUser> extends IService<TUser> 俩莽。
  • 實(shí)現(xiàn)類:public class TUserServiceImpl extends ServiceImpl<TUserMapper, TUser> implements IService<TUser> 實(shí)現(xiàn)接口由IService變成 ITUserService旺坠。

Junit 測試代碼:

    @Autowired
    private ITUserService<TUser> tUserService;

    @Test
    public void testService() {
        TUser user = new TUser();
        user.setUserName("trek");
        user.setPassword("888999");
        user.setPwdCipher("ewifwiEFafe==");
        if (tUserService.save(user)) {
            tUserService.list().forEach(System.out::println);
        } else {
            System.out.println("添加數(shù)據(jù)失敗");
        }
    }

測試結(jié)果:


表插入新數(shù)據(jù)

控制臺輸出信息:

==>  Preparing: INSERT INTO t_user ( user_name, password, pwd_cipher ) VALUES ( ?, ?, ? ) 
==> Parameters: trek(String), 888999(String), ewifwiEFafe==(String)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@47acd13b]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@633514467 wrapping com.mysql.cj.jdbc.ConnectionImpl@297c9a9b] will not be managed by Spring
==>  Preparing: SELECT id,user_name,password,pwd_cipher FROM t_user 
==> Parameters: 
<==    Columns: id, user_name, password, pwd_cipher
<==        Row: 1, guo, bwMhZeGXyD98aToKQdXLcw==, null
<==        Row: 2, guo, bwMhZeGXyD98aToKQdXLcw==, null
<==        Row: 3, guo, 123456, bwMhZeGXyD98aToKQdXLcw==
<==        Row: 4, trek, 888999, ewifwiEFafe==
<==      Total: 4
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5e26f1ed]
TUser(id=1, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=2, userName=guo, password=bwMhZeGXyD98aToKQdXLcw==, pwdCipher=null)
TUser(id=3, userName=guo, password=123456, pwdCipher=bwMhZeGXyD98aToKQdXLcw==)
TUser(id=4, userName=trek, password=888999, pwdCipher=ewifwiEFafe==)

四、分頁查詢

4.1 配置攔截器組件

MybatisPlusApplication啟動類添加代碼:

     /**
     * 分頁插件
     */
    @Bean
    public PaginationInterceptor paginationInterceptor() {
        return new PaginationInterceptor();
    }

4.2 編寫分頁代碼

直接 new 一個 Page 對象豹绪,對象需要傳遞兩個參數(shù)(當(dāng)前頁价淌,每頁顯示的條數(shù))。
調(diào)用 mybatis-plus 提供的分頁查詢方法瞒津,其會將 分頁查詢的數(shù)據(jù)封裝到 Page 對象中蝉衣。


    @Test
    public void selectPage() {
        // 根據(jù)Wrapper 自定義條件查詢
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.gt("age", "18");
        queryWrapper.orderByDesc("age");
        Page<User> userPage = new Page<User>(2, 2);
        // userPage.setCurrent(2L);  //當(dāng)前是第幾頁 默認(rèn)為1
        // userPage.setSize(2);  //每頁大小
        IPage<User> userIPage = userMapper.selectPage(userPage, queryWrapper);

        System.out.println("當(dāng)前頁" + userIPage.getCurrent());  //當(dāng)前頁
        System.out.println("總頁數(shù)" + userIPage.getPages()); //總頁數(shù)
        System.out.println("返回數(shù)據(jù)" + userIPage.getRecords());  //返回數(shù)據(jù)
        System.out.println("每頁大小" + userIPage.getSize());  //每頁大小
        System.out.println("滿足符合條件的條數(shù)" + userIPage.getTotal());  //滿足符合條件的條數(shù)
        System.out.println("下一頁" + userPage.hasNext());   //下一頁
        System.out.println("上一頁" + userPage.hasPrevious());  //上一頁
    }

運(yùn)行結(jié)果:
結(jié)果說明

控制臺System.out.println代碼部分日志輸出:

當(dāng)前頁2
總頁數(shù)2
返回數(shù)據(jù)[User(id=4, name=Oliver, age=21, email=xds@erbadagang.com), User(id=2, name=xiu, age=20, email=specialized@erbadagang.com)]
每頁大小2
滿足符合條件的條數(shù)4
下一頁false
上一頁true

五、Mybatis-Plus Wrapper

參考上篇文章:MyBatis-Plus 條件構(gòu)造器(Wrapper)

5.1 刪除

    /**
     * <p>
     * 根據(jù)根據(jù) entity 條件巷蚪,刪除記錄,QueryWrapper實(shí)體對象封裝操作類(可以為 null)
     * 下方獲取到queryWrapper后刪除的查詢條件為name字段為null的and年齡大于等于12的and email字段不為null的
     * 同理寫法條件添加的方式就不做過多介紹了病毡。
     * </p>
     */
    @Test
    public void delete() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper
                .isNull("name")
                .ge("age", 12)
                .isNotNull("email");
        int delete = userMapper.delete(queryWrapper);
        System.out.println("delete return count = " + delete);
    }

SQL輸出:

==>  Preparing: DELETE FROM user WHERE (name IS NULL AND age >= ? AND email IS NOT NULL) 
==> Parameters: 12(Integer)
<==    Updates: 0

5.2 selectOne

    /**
     * <p>
     * 根據(jù) entity 條件,查詢一條記錄,
     * 這里和上方刪除構(gòu)造條件一樣屁柏,只是seletOne返回的是一條實(shí)體記錄啦膜,當(dāng)出現(xiàn)多條時會報錯
     * </p>
     */
    @Test
    public void selectOne() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        User user = userMapper.selectOne(queryWrapper);
        System.out.println(user);
    }

SQL輸出:

==>  Preparing: SELECT id,name,age,email FROM user WHERE (name = ?) 
==> Parameters: guo(String)
<==    Columns: id, name, age, email
<==        Row: 1, Guo , 18, trek@erbadagang.com
<==      Total: 1

5.3 selectCount

   /**
     * <p>
     * 根據(jù) Wrapper 條件有送,查詢總記錄數(shù)
     * </p>
     *
     * @param queryWrapper 實(shí)體對象
     */
    @Test
    public void selectCount() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        Integer count = userMapper.selectCount(queryWrapper);
        System.out.println(count);
    }

SQL輸出:

==>  Preparing: SELECT COUNT( 1 ) FROM user WHERE (name = ?) 
==> Parameters: guo(String)
<==    Columns: COUNT( 1 )
<==        Row: 1
<==      Total: 1

5.4 selectList


    /**
     * <p>
     * 根據(jù) entity 條件,查詢?nèi)坑涗?     * </p>
     *
     * @param queryWrapper 實(shí)體對象封裝操作類(可以為 null)為null查詢?nèi)?     */
    @Test
    public void selectListByEntity() {
        List<User> list = userMapper.selectList(null);//null為無條件

        System.out.println(list);
    }

    /**
     * <p>
     * 根據(jù) Wrapper 條件僧家,查詢?nèi)坑涗?     * </p>
     *
     * @param queryWrapper 實(shí)體對象封裝操作類(可以為 null)
     */
    @Test
    public void selectListByMapper() {
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("name", "guo");

        List<User> list = userMapper.selectList(queryWrapper);//null為無條件

        System.out.println(list);
    }

5.5 selectMaps

    @Test
    public void selectMaps() {
        Page<User> page = new Page<User>(1, 5);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();

        List<Map<String, Object>> maps = userMapper.selectMaps(queryWrapper);
        maps.forEach(map -> {
            System.out.println("name-->" + map.get("name"));
            System.out.println("email-->" + map.get("email"));
        });
        System.out.println(maps);
    }

返回類型List<Map<String, Object>>雀摘。Map的key為字段名稱,value為對應(yīng)的字段值八拱。
控制臺輸出:

==>  Preparing: SELECT id,name,age,email FROM user 
==> Parameters: 
<==    Columns: id, name, age, email
<==        Row: 1, Guo , 18, trek@erbadagang.com
<==        Row: 2, xiu, 20, specialized@erbadagang.com
<==        Row: 3, zhi, 28, giant@erbadagang.com
<==        Row: 4, Oliver, 88, winspace@erbadagang.com
<==        Row: 5, Messi, 24, look@erbadagang.com
<==      Total: 5
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@294aba23]
name-->Guo 
email-->trek@erbadagang.com
name-->xiu
email-->specialized@erbadagang.com
name-->zhi
email-->giant@erbadagang.com
name-->Oliver
email-->winspace@erbadagang.com
name-->Messi
email-->look@erbadagang.com
[{name=Guo , id=1, age=18, email=trek@erbadagang.com}, {name=xiu, id=2, age=20, email=specialized@erbadagang.com}, {name=zhi, id=3, age=28, email=giant@erbadagang.com}, {name=Oliver, id=4, age=88, email=winspace@erbadagang.com}, {name=Messi, id=5, age=24, email=look@erbadagang.com}]

六阵赠、自動填充數(shù)據(jù)功能

添加、修改數(shù)據(jù)時肌稻,每次都會使用相同的方式進(jìn)行填充清蚀。比如: 數(shù)據(jù)的創(chuàng)建時間、修改時間爹谭、操作者等枷邪。

6.1 數(shù)據(jù)庫準(zhǔn)備

Mybatis-plus 支持自動填充這些字段的數(shù)據(jù)。給之前的數(shù)據(jù)表新增3個字段:創(chuàng)建時間诺凡、修改時間东揣、操作人。
SQL語句:

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `create_time` datetime(0) COMMENT '創(chuàng)建時間' AFTER `email`,
ADD COLUMN `update_time` datetime(0) COMMENT '修改時間' AFTER `create_time`,
ADD COLUMN `operator` varchar(20) COMMENT '操作人' AFTER `update_time`;

6.2 重新生成代碼

并使用 代碼生成器重新生成代碼绑洛,注意修改生成器配置為可覆蓋老代碼救斑。

        // 重新生成文件時是否覆蓋,false 表示不覆蓋(可選)
        gc.setFileOverride(true);

6.3 修改entity

使用@TableField注解真屯,標(biāo)注需要進(jìn)行填充的字段脸候。

package com.erbadagang.mybatis.plus.mybatisplus.entity;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import lombok.EqualsAndHashCode;

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

/**
 * <p>
 * 
 * </p>
 *
 * @author 郭秀志 jbcode@126.com
 * @since 2020-07-11
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class User implements Serializable {

    private static final long serialVersionUID=1929834928304L;

    /**
     * 主鍵ID
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

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

    /**
     * 年齡
     */
    private Integer age;

    /**
     * 郵箱
     */
    private String email;

    /**
     * 創(chuàng)建時間
     */
    @TableField(fill = FieldFill.INSERT)
    private Date createTime;

    /**
     * 修改時間
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date updateTime;

    /**
     * 操作人
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private String operator;


}

填充策略FieldFill.INSERT_UPDATE表示插入和更新都進(jìn)行自動填充。

6.4 自定義MetaObjectHandler

自定義一個類绑蔫,實(shí)現(xiàn) MetaObjectHandler 接口运沦,并重寫方法。添加 @Component 注解配深,交給 Spring 去管理携添。

package com.erbadagang.mybatis.plus.mybatisplus.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * @description 自定義的數(shù)據(jù)填充handler,分別寫insert和update的寫入策略篓叶。
 * @ClassName: MyFillDataMetaObjectHandler
 * @author: 郭秀志 jbcode@126.com
 * @date: 2020/7/11 9:39
 * @Copyright:
 */
@Component
public class MyFillDataMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "operator", String.class, "梅西愛騎車");
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
        this.strictInsertFill(metaObject, "operator", String.class, "梅西愛騎車");
    }
}

6.5 測試

6.5.1 插入測試


    /**
     * 測試插入的自動填充數(shù)據(jù)功能烈掠。
     */
    @Test
    public void testAutoFillInsert() {
        User user = new User();
        user.setId(0l);
        user.setName("崔克");
        user.setAge(18);
        user.setEmail("trek@erbadagang.cn");

        int id = userMapper.insert(user);//自動返回插入的id

        System.out.println(id);
    }

運(yùn)行測試用例,如果報錯:Caused by: java.sql.SQLException: Field 'id' doesn't have a default value
需要把id列勾上自增缸托。

自增

輸出的SQL信息:

==>  Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator ) VALUES ( ?, ?, ?, ?, ?, ? ) 
==> Parameters: 崔克(String), 18(Integer), trek@erbadagang.cn(String), 2020-07-11 09:57:43.386(Timestamp), 2020-07-11 09:57:43.388(Timestamp), 梅西愛騎車(String)
<==    Updates: 1
表中數(shù)據(jù)

6.5.2 更新測試

更新name為英文的trek左敌,age為28。

    /**
     * 測試更新的自動填充數(shù)據(jù)功能俐镐。
     */
    @Test
    public void testAutoFillUpdate() {
        User user = new User();
        user.setId(7l);
        user.setName("trek");
        user.setAge(28);
        user.setEmail("trek@erbadagang.cn");

        int id = userMapper.updateById(user);//自動返回插入的id

        System.out.println(id);
    }

運(yùn)行測試矫限。
輸出的SQL信息,只更新了update_time沒更新create_time字段:

==>  Preparing: UPDATE user SET name=?, age=?, email=?, update_time=?, operator=? WHERE id=? 
==> Parameters: trek(String), 28(Integer), trek@erbadagang.cn(String), 2020-07-11 10:05:30.249(Timestamp), 梅西愛騎車(String), 7(Long)
<==    Updates: 1

如果入庫的時間跟上面打印的SQL不一致,需要在jdbc連接加入時區(qū)設(shè)置:
jdbc:mysql://101.133.227.13:3306/orders_1?useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai

七叼风、邏輯刪除

刪除數(shù)據(jù)取董,可以通過物理刪除,也可以通過邏輯刪除无宿。

  • 物理刪除指的是直接將數(shù)據(jù)從數(shù)據(jù)庫中刪除茵汰,不保留。
  • 邏輯刪除指的是修改數(shù)據(jù)的某個字段懈贺,使其表示為已刪除狀態(tài)经窖,而非刪除數(shù)據(jù),保留該數(shù)據(jù)在數(shù)據(jù)庫中梭灿,但是查詢時不顯示該數(shù)據(jù)(查詢時過濾掉該數(shù)據(jù))。

7.1 表結(jié)構(gòu)

給數(shù)據(jù)表增加一個字段:delete_flag冰悠,用于表示該數(shù)據(jù)是否被邏輯刪除堡妒。
SQL語句:

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `delete_flag` tinyint(1) COMMENT '邏輯刪除(0 未刪除、1 刪除)' AFTER `operator`;

7.3 使用邏輯刪除溉卓。

可以定義一個自動填充規(guī)則皮迟,初始值為 0。0 表示未刪除桑寨, 1 表示刪除伏尼。
在Entity類新增:

    /**
     * 邏輯刪除(0 未刪除、1 刪除)
     */
    @TableLogic(value = "0", delval = "1")//定義邏輯刪除功能尉尾。
    @TableField(fill = FieldFill.INSERT)//定義在insert的時候自動填充功能
    private Integer deleteFlag;

@TableLogic定義邏輯刪除功能爆阶,若去除 TableLogic 注解,再執(zhí)行 Delete 時進(jìn)行物理刪除沙咏,直接刪除這條數(shù)據(jù)辨图。
@TableField定義在自動填充功能。

在自動填充規(guī)則MyFillDataMetaObjectHandler類的insertFill方法添加:

@Override
public void insertFill(MetaObject metaObject) {
......
    this.strictInsertFill(metaObject, "deleteFlag", Integer.class, 0);
}

7.4 測試

新增一條閃電牌自行車數(shù)據(jù):

        User user = new User();
        user.setId(0l);
        user.setName("閃電");
        user.setAge(18);
        user.setEmail("specialized@erbadagang.cn");

        int id = userMapper.insert(user);//自動返回插入的id

delete_flag字段為自動填充代碼定義的默認(rèn)值0肢藐,當(dāng)然也可以使用數(shù)據(jù)庫定義默認(rèn)值故河。


表層面定義的初始值0

新增數(shù)據(jù)delete_flag值:
delete_flag=0

刪除數(shù)據(jù):

    //這次使用IUserService而不是mapper進(jìn)行測試
    @Autowired
    private IUserService userService;

    /**
     * 邏輯刪除測試。
     */
    @Test

    public void testDelete() {
        if (userService.removeById(8)) {
            System.out.println("刪除數(shù)據(jù)成功");
            userService.list().forEach(System.out::println);
        } else {
            System.out.println("刪除數(shù)據(jù)失敗");
        }
    }

執(zhí)行測試吆豹,輸出的日志:

==>  Preparing: UPDATE user SET delete_flag=1 WHERE id=? AND delete_flag=0 
==> Parameters: 8(Integer)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@22bdb1d0]
刪除數(shù)據(jù)成功

==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag FROM user WHERE delete_flag=0 
==> Parameters: 
<==      Total: 0

可以看到更新delete_flag=1的操作鱼的,以及查詢時自動加上了WHERE delete_flag=0的判斷。

表數(shù)據(jù)變化:


delete_flage變?yōu)?

八痘煤、樂觀鎖

8.1 基礎(chǔ)知識

(1)首先認(rèn)識一下: 讀問題凑阶、寫問題
  操作數(shù)據(jù)庫數(shù)據(jù)時,遇到的最基本問題就是 讀問題與寫問題速勇。
  讀問題 指的是從數(shù)據(jù)庫中讀取數(shù)據(jù)時遇到的問題晌砾,比如:臟讀、幻讀烦磁、不可重復(fù)讀养匈。
臟讀哼勇、幻讀、不可重復(fù)讀 參考地址
  寫問題 指的是數(shù)據(jù)寫入數(shù)據(jù)庫時遇到的問題呕乎,比如:丟失更新(多個線程同時對某條數(shù)據(jù)更新积担,無論執(zhí)行順序如何,都會丟失其他線程更新的數(shù)據(jù))

(2)如何解決寫問題猬仁?
  樂觀鎖帝璧、悲觀鎖就是為了解決 寫問題而存在的。
    樂觀鎖:總是假設(shè)最好的情況湿刽,每次讀取數(shù)據(jù)時認(rèn)為數(shù)據(jù)不會被修改(即不加鎖)的烁,當(dāng)進(jìn)行更新操作時,會判斷這條數(shù)據(jù)是否被修改诈闺,未被修改渴庆,則進(jìn)行更新操作。若被修改雅镊,則數(shù)據(jù)更新失敗襟雷,可以對數(shù)據(jù)進(jìn)行重試(重新嘗試修改數(shù)據(jù))。
    悲觀鎖:總是假設(shè)最壞的情況仁烹,每次讀取數(shù)據(jù)時認(rèn)為數(shù)據(jù)會被修改(即加鎖)耸弄,當(dāng)進(jìn)行更新操作時,直接更新數(shù)據(jù)卓缰,結(jié)束操作后釋放鎖(此處才可以被其他線程讀燃瞥省)。

(3)樂觀鎖僚饭、悲觀鎖使用場景震叮?
  樂觀鎖一般用于讀比較多的場合,盡量減少加鎖的開銷鳍鸵。
  悲觀鎖一般用于寫比較多的場合苇瓣,盡量減少 類似 樂觀鎖重試更新引起的性能開銷。

(4)樂觀鎖兩種實(shí)現(xiàn)方式
方式一:通過版本號機(jī)制實(shí)現(xiàn)偿乖。
  在數(shù)據(jù)表中增加一個 version 字段击罪。
  取數(shù)據(jù)時,獲取該字段贪薪,更新時以該字段為條件進(jìn)行處理(即set version = newVersion where version = oldVersion)媳禁,若 version 相同,則更新成功(給新 version 賦一個值画切,一般加 1)竣稽。若 version 不同,則更新失敗,可以重新嘗試更新操作毫别。

方式二:通過 CAS 算法實(shí)現(xiàn)娃弓。
  CAS 為 Compare And Swap 的縮寫,即比較交換岛宦,是一種無鎖算法(即在不加鎖的情況實(shí)現(xiàn)多線程之間的變量同步)台丛。
  CAS 操作包含三個操作數(shù) —— 內(nèi)存值(V)、預(yù)期原值(A)和新值(B)砾肺。如果內(nèi)存地址里面的值 V 和 A 的值是一樣的挽霉,那么就將內(nèi)存里面的值更新成B。若 V 與 A 不一致变汪,則不執(zhí)行任何操作(可以通過自旋操作侠坎,不斷嘗試修改數(shù)據(jù)直至成功修改)。即 V == A 裙盾? V = B : V = V硅蹦。
  CAS 可能導(dǎo)致 ABA 問題(兩次讀取數(shù)據(jù)時值相同,但不確定值是否被修改過)闷煤,比如兩個線程操作同一個變量,線程 A涮瞻、線程B 初始讀取數(shù)據(jù)均為 A鲤拿,后來 線程B 將數(shù)據(jù)修改為 B,然后又修改為 A署咽,此時線程 A 再次讀取到的數(shù)據(jù)依舊是 A近顷,雖然值相同但是中間被修改過,這就是 ABA 問題宁否≈仙可以加一個額外的標(biāo)志位 C,用于表示數(shù)據(jù)是否被修改慕匠。當(dāng)標(biāo)志位 C 與預(yù)期標(biāo)志位相同饱须、且 V == A 時,則更新值 B台谊。

(5)mybatis-plus 實(shí)現(xiàn)樂觀鎖(通過 version 機(jī)制)
實(shí)現(xiàn)思路:
  Step1:取出記錄時蓉媳,獲取當(dāng)前version
  Step2:更新時,帶上這個version
  Step3:執(zhí)行更新時锅铅, set version = newVersion where version = oldVersion
  Step4:如果version不對酪呻,就更新失敗

(6)mybatis-plus 代碼實(shí)現(xiàn)樂觀鎖

8.2 MP實(shí)現(xiàn)樂觀鎖

配置樂觀鎖插件。
啟動類MybatisPlusApplication新增如下代碼(類似分頁插件)盐须,將 OptimisticLockerInterceptor通過@Bean交給 Spring 管理玩荠。

/**
 * 樂觀鎖插件
 * @return 樂觀鎖插件的實(shí)例
 */
@Bean
public OptimisticLockerInterceptor optimisticLockerInterceptor() {
    return new OptimisticLockerInterceptor();
}

8.3 定義一個數(shù)據(jù)庫字段 version

ALTER TABLE `orders_1`.`user` 
ADD COLUMN `version` int COMMENT '版本號(用于樂觀鎖, 默認(rèn)為 1)' AFTER `delete_flag`;

8.4 實(shí)體類

使用@Version注解標(biāo)注對應(yīng)的實(shí)體類〗赘裕可以通過@TableField進(jìn)行數(shù)據(jù)自動填充闷尿。

/**
 * 版本號(用于樂觀鎖, 默認(rèn)為 1)
 */
@Version
@TableField(fill = FieldFill.INSERT)
private Integer version;

8.5 自動填充規(guī)則

在自動填充規(guī)則MyFillDataMetaObjectHandler類的insertFill方法添加:

@Override
public void insertFill(MetaObject metaObject) {
......
    //樂觀鎖version初始化值為1
    this.strictInsertFill(metaObject, "version", Integer.class, 1);
}

8.6 測試

    /**
     * 樂觀鎖測試
     */
    @Test
    public void testVersion() {
        User user = new User();
        user.setName("Look");
        user.setAge(8);
        user.setEmail("look@erbadagang.cn");
        userService.save(user);//新增數(shù)據(jù)
        userService.list().forEach(System.out::println);//查詢數(shù)據(jù)

        user.setName("梅花");
        userService.update(user, null);//修改數(shù)據(jù)
        userService.list().forEach(System.out::println);//查詢數(shù)據(jù)
    }

運(yùn)行結(jié)果(語句增加了我的注釋):

##插入數(shù)據(jù)眼溶,version=1
==>  Preparing: INSERT INTO user ( name, age, email, create_time, update_time, operator, delete_flag, version ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? ) 
==> Parameters: Look(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西愛騎車(String), 0(Integer), 1(Integer)
<==    Updates: 1
##查詢數(shù)據(jù)悠砚,讀取的version為1
==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0 
==> Parameters: 
<==    Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<==        Row: 9, Look, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西愛騎車, 0, 1
<==      Total: 1
##更新數(shù)據(jù),條件是version=1如果此時被其他程序更新了堂飞,這里條件不滿足不會更新數(shù)據(jù)灌旧。
##version的值自動+1,現(xiàn)在是2绰筛。
==>  Preparing: UPDATE user SET name=?, age=?, email=?, create_time=?, update_time=?, operator=?, version=? WHERE delete_flag=0 AND (version = ?) 
==> Parameters: 梅花(String), 8(Integer), look@erbadagang.cn(String), 2020-07-11 12:03:50.712(Timestamp), 2020-07-11 12:03:50.715(Timestamp), 梅西愛騎車(String), 2(Integer), 1(Integer)
<==    Updates: 1
##再次查詢version為2枢泰。
==>  Preparing: SELECT id,name,age,email,create_time,update_time,operator,delete_flag,version FROM user WHERE delete_flag=0 
==> Parameters: 
<==    Columns: id, name, age, email, create_time, update_time, operator, delete_flag, version
<==        Row: 9, 梅花, 8, look@erbadagang.cn, 2020-07-11 12:03:51, 2020-07-11 12:03:51, 梅西愛騎車, 0, 2
<==      Total: 1
##查詢出來的最新數(shù)據(jù),delete_flag=0
User(id=9, name=梅花, age=8, email=look@erbadagang.cn, createTime=Sat Jul 11 12:03:51 CST 2020, updateTime=Sat Jul 11 12:03:51 CST 2020, operator=梅西愛騎車, deleteFlag=0, version=2)

底線


本文源代碼使用 Apache License 2.0開源許可協(xié)議铝噩,可從Gitee代碼地址通過git clone命令下載到本地或者通過瀏覽器方式查看源代碼衡蚂。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市骏庸,隨后出現(xiàn)的幾起案子毛甲,更是在濱河造成了極大的恐慌,老刑警劉巖具被,帶你破解...
    沈念sama閱讀 211,423評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件玻募,死亡現(xiàn)場離奇詭異,居然都是意外死亡一姿,警方通過查閱死者的電腦和手機(jī)七咧,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叮叹,“玉大人艾栋,你說我怎么就攤上這事◎韧纾” “怎么了蝗砾?”我有些...
    開封第一講書人閱讀 157,019評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蜂林。 經(jīng)常有香客問我衡未,道長夹界,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,443評論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮琳钉,結(jié)果婚禮上樟蠕,老公的妹妹穿的比我還像新娘秕狰。我一直安慰自己族阅,他們只是感情好债朵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評論 6 385
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著瀑凝,像睡著了一般序芦。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上粤咪,一...
    開封第一講書人閱讀 49,798評論 1 290
  • 那天谚中,我揣著相機(jī)與錄音,去河邊找鬼寥枝。 笑死宪塔,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的囊拜。 我是一名探鬼主播某筐,決...
    沈念sama閱讀 38,941評論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼冠跷!你這毒婦竟也來了南誊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,704評論 0 266
  • 序言:老撾萬榮一對情侶失蹤蜜托,失蹤者是張志新(化名)和其女友劉穎抄囚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體橄务,經(jīng)...
    沈念sama閱讀 44,152評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡怠苔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了仪糖。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,629評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡迫肖,死狀恐怖锅劝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蟆湖,我是刑警寧澤故爵,帶...
    沈念sama閱讀 34,295評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站隅津,受9級特大地震影響诬垂,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜伦仍,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評論 3 313
  • 文/蒙蒙 一结窘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧充蓝,春花似錦隧枫、人聲如沸喉磁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽协怒。三九已至,卻和暖如春卑笨,著一層夾襖步出監(jiān)牢的瞬間孕暇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評論 1 266
  • 我被黑心中介騙來泰國打工赤兴, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留妖滔,地道東北人。 一個月前我還...
    沈念sama閱讀 46,333評論 2 360
  • 正文 我出身青樓搀缠,卻偏偏與公主長得像铛楣,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子艺普,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評論 2 348