基于 MybatisPlus 逻辑删除开启情况下兼容多数据库实现批量插入更新通用流程实现

基于 MybatisPlus 逻辑删除开启情况下兼容多数据库实现批量插入更新通用流程实现

背景

  • 项目上业务流程有大量使用 mysql 批量插入更新语法 insert on duplicate update
  • 由于现在公司业务需要,同时兼容达梦数据库使用,达梦数据库虽然也有类似的语法,不过使用起来也比较麻烦,生成相应的 SQL 可以看我这一篇文章

核心问题

  1. MySQL 更新插入流程如何使用实现?
  2. 如何实现多个数据库兼容插入更新的流程?
  3. 如何编写工具类优雅实现插入通用流程封装以及整合 MybatisPlus lambda 表达式查询,达到方便易用的效果?
  4. 开启 MybatisPlus 逻辑删除功能,怎么通过自定义 SQL 查询出所有数据(插入更新流程可能涉及到所有的数据,并不是只是处理未逻辑删除的数据)?
  5. 批量数据插入更新速度如何优化?

代码实现

实现多数据库兼容插入更新操作,只能根据 MySQL 插入更新原理利用代码抽象通用化流程,较好的通用化方式是使用 MybatisPlus 自带的通用 CRUD 逻辑方法实现,但是原有自带方法开启逻辑删除功能以后,查询方法都会自带过滤逻辑删除的数据,需要实现自定义 SQL 注入器,为了实现一套不带逻辑删除的通用方法

拓展自定义 SQL 注入器实现

新增自定义方法

SelectList 方法不带逻辑删除

public class SelectListWithoutLogicDelete extends AbstractMethod {

    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        String sql = String.format(
                SqlMethod.SELECT_LIST.getSql(),
                sqlFirst(),
                sqlSelectColumns(tableInfo, true),
                tableInfo.getTableName(),
                sqlWhereEntityWrapper(true, tableInfo),
                sqlComment()
        );

        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);

        String mapperMethodName = StrUtil.lowerFirst(getClass().getSimpleName());
        return addSelectMappedStatementForTable(mapperClass, mapperMethodName, sqlSource, tableInfo);
    }

    // 重写 sqlWhereEntityWrapper ,去掉逻辑删除相关代码
    @Override
    protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
        String sqlScript = table.getAllSqlWhere(false, true, WRAPPER_ENTITY_DOT);
        sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
        sqlScript += NEWLINE;
        sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                        WRAPPER_NONEMPTYOFWHERE), true);
        sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
        sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
                String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
                        WRAPPER_EMPTYOFWHERE), true);
        sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
        return newLine ? NEWLINE + sqlScript : sqlScript;
    }
}

UpdateById 方法不带逻辑删除

public class UpdateByIdWithoutLogicDelete extends AbstractMethod {
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        SqlMethod sqlMethod = SqlMethod.UPDATE_BY_ID;

        final String additional = optlockVersion(tableInfo);
        String sql = String.format(
                sqlMethod.getSql(),
                tableInfo.getTableName(),
                // 搬运 UpdateById 代码,第一个参数变化了
                sqlSet(false, false, tableInfo, false, ENTITY, ENTITY_DOT),
                tableInfo.getKeyColumn(),
                ENTITY_DOT + tableInfo.getKeyProperty(),
                additional
        );

        SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
        String mapperMethodName = StrUtil.lowerFirst(getClass().getSimpleName());
        return addUpdateMappedStatement(mapperClass, modelClass, mapperMethodName, sqlSource);
    }
}

注入不带逻辑删除自定义 mapper 方法

public class SqlInjectorExtension extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methods = super.getMethodList(mapperClass);
        // 原来基础上注入两个新方法
        methods.add(new SelectListWithoutLogicDelete());
        methods.add(new UpdateByIdWithoutLogicDelete());
        return methods;
    }
}

封装自定义 mapper

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.QueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.UpdateChainWrapper;
import org.apache.ibatis.annotations.Param;

public interface BaseMapperExtension<T> extends BaseMapper<T> {
    // === ChainWrappers 工具类转发 ===

    /**
     * 链式查询 普通
     *
     * @return QueryWrapper 的包装类
     */
    default QueryChainWrapper<T> queryChain() {
        return new QueryChainWrapper<>(this);
    }

    /**
     * 链式查询 lambda 式
     * <p>注意:不支持 Kotlin </p>
     *
     * @return LambdaQueryWrapper 的包装类
     */
    default LambdaQueryChainWrapper<T> lambdaQueryChain() {
        return new LambdaQueryChainWrapper<>(this);
    }

    /**
     * 链式更改 普通
     *
     * @return UpdateWrapper 的包装类
     */
    default UpdateChainWrapper<T> updateChain() {
        return new UpdateChainWrapper<>(this);
    }

    /**
     * 链式更改 lambda 式
     * <p>注意:不支持 Kotlin </p>
     *
     * @return LambdaUpdateWrapper 的包装类
     */
    default LambdaUpdateChainWrapper<T> lambdaUpdateChain() {
        return new LambdaUpdateChainWrapper<>(this);
    }

    // === 自定义 sql ===

    /**
     * 忽略逻辑删除功能的 selectList 方法
     *
     * @param queryWrapper
     *         查询条件
     *
     * @return {@code List<T>}
     */
    List<T> selectListWithoutLogicDelete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper);

    /**
     * 忽略逻辑删除功能的 updateById 方法
     *
     * @param entity
     *         实体
     *
     * @return int
     */
    int updateByIdWithoutLogicDelete(@Param(Constants.ENTITY) T entity);
}

封装自定义 IServiceExtensionServiceImplExtension 模板

public interface IServiceExtension<T extends IBaseDO<T>> extends IService<T> {
    @Override
    BaseMapperExtension<T> getBaseMapper();

    // === BaseMapperExtension 新默认方法 ===

    /**
     * 通过 id 更新实体(忽略逻辑删除功能)
     *
     * @param entity
     *         实体数据
     *
     * @return boolean
     */
    default boolean updateByIdWithoutLogicDelete(T entity) {
        return SqlHelper.retBool(getBaseMapper().updateByIdWithoutLogicDelete(entity));
    }

    default List<T> listWithoutLogicDelete(Wrapper<T> queryWrapper) {
        return getBaseMapper().selectListWithoutLogicDelete(queryWrapper);
    }

    default List<T> listWithoutLogicDelete() {
        return listWithoutLogicDelete(Wrappers.emptyWrapper());
    }

    // === 增强型方法 ===

    /**
     * 批量根据 ID 更新(忽略逻辑删除)
     *
     * @param entityList
     *         实体列表
     * @param batchSize
     *         批次大小
     *
     * @return boolean
     */
    boolean updateBatchByIdWithoutLogicDelete(Collection<T> entityList, int batchSize);
}
public abstract class ServiceImplExtension<M extends BaseMapperExtension<T>, T extends IBaseDO<T>> extends ServiceImpl<M, T> implements IServiceExtension<T> {}

新增 MybatisPlusUtils

封装 lambda 表达式通用插入更新流程

  • 基础实体类接口 IBaseDO
import java.util.Date;

public interface IBaseDO<E> {
     Long getId();

     E setId(Long id);

     Date getCreateDate();

     E setCreateDate(Date createDate);

     Date getUpdateDate();

     E setUpdateDate(Date updateDate);
}
  • 抽象实体类
@EqualsAndHashCode
public abstract class BaseDO<E extends BaseDO<E>> implements IBaseDO<E> {
    @ApiModelProperty(value = "主键")
    @TableId(value = "id", type = IdType.ASSIGN_ID)
    private Long id;
    
    @ApiModelProperty(value = "创建时间")
    @TableField(value = "create_date", fill = FieldFill.INSERT)
    private Date createDate;
    
    @ApiModelProperty(value = "更新时间")
    @TableField(value = "update_date", fill = FieldFill.INSERT_UPDATE)
    private Date updateDate;

    @Override
    public Long getId() {
        return id;
    }

    @Override
    public E setId(Long id) {
        this.id = id;
        return self();
    }

    private E self() {
        return (E) this;
    }

    @Override
    public Date getCreateDate() {
        return createDate;
    }

    @Override
    public E setCreateDate(Date createDate) {
        this.createDate = createDate;
        return self();
    }

    @Override
    public Date getUpdateDate() {
        return updateDate;
    }

    @Override
    public E setUpdateDate(Date updateDate) {
        this.updateDate = updateDate;
        return self();
    }

    public E fillLatestDate() {
        Date date = new Date();
        createDate = date;
        updateDate = date;
        return self();
    }

    public E fillDate(Date date) {
        createDate = date;
        updateDate = date;
        return self();
    }
}
  • MyBatisPlusUtils 工具类
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.util.*;
import java.util.function.BiFunction;

import static java.util.stream.Collectors.*;

@Slf4j
public abstract class MyBatisPlusUtils {
    /**
     * mysql insert on duplicate key update 替代方法
     *
     * @param service
     *         service 类
     * @param dataCollect
     *         新数据列表
     * @param isFillId
     *         无 ID 是否填充 ID
     * @param uniqueCondition
     *         唯一索引
     * @param updateMapping
     *         更新 mapping:函数入参为查询到的旧数据 & 新数据,出参为更新后的旧数据(不能设置id和相关唯一索引)
     * @param <T>
     *         实体泛型
     */
    public static <T extends IBaseDO<T>, S extends IServiceExtension<T>> List<T> insertOnUpdateBatch(
            S service,
            Collection<T> dataCollect,
            boolean isFillId,
            BiFunction<T, LambdaQueryWrapper<T>, LambdaQueryWrapper<T>> uniqueCondition,
            BiFunction<T, T, T> updateMapping
    ) {
        Objects.requireNonNull(service);
        Objects.requireNonNull(dataCollect);
        Objects.requireNonNull(updateMapping);
        Objects.requireNonNull(uniqueCondition);

        Class<T> entityClass = service.getEntityClass();
        Objects.requireNonNull(entityClass);

        if (CollectionUtils.isEmpty(dataCollect)) {
            log.debug("[{}] insert on update data size: 0,ignore", service.getEntityClass().getSimpleName());
            return new ArrayList<>();
        }

        Map<Boolean, List<T>> collect = dataCollect.stream()
                .filter(Objects::nonNull)
                .peek(newData -> execFillIdStrategy(isFillId, newData))
                .flatMap(newData -> {
                    LambdaQueryWrapper<T> chainWrapper = uniqueCondition.apply(newData, Wrappers.lambdaQuery(service.getEntityClass()));
                    Objects.requireNonNull(chainWrapper);
                    // 构造通过 ID 或者 多个唯一索引查询条件,进行数据库查询,查询结果可能查询出来多条
                    List<Pair<Boolean, T>> updateData = service.listWithoutLogicDelete(chainWrapper.or(wrapper -> wrapper.eq(IBaseDO::getId, newData.getId())))
                            .stream()
                            .map(oldData -> {
                                // 查询出来需要更新的数据
                                Date newUpdateDate = newData.getUpdateDate();
                                Date oldCreateDate = oldData.getCreateDate();
                                Date now = new Date();
                                // 更新操作回调
                                T update = updateMapping.apply(newData, oldData)
                                        // 指定填充默认更新字段
                                        .setCreateDate(oldCreateDate == null ? now : oldCreateDate)
                                        .setUpdateDate(newUpdateDate == null ? now : newUpdateDate);
                                // 转换器返回不同 ID 数据抛出异常
                                if (!Objects.equals(update.getId(), oldData.getId())) {
                                    throw new RuntimeException("mapping can't return new data");
                                }
                                return Pair.of(false, update);
                            })
                            .collect(toList());

                    if (CollectionUtils.isNotEmpty(updateData)) {
                        return updateData.stream();
                    }
                    
                    // 如果上面不符合,就是需要插入的新数据
                    List<Pair<Boolean, T>> insertData = Collections.singletonList(Pair.of(true, newData));
                    return insertData.stream();
                })
                .collect(groupingBy(Pair::getKey, mapping(Pair::getValue, toList())));

        // 提取需要新插入的数据
        List<T> insertData = collect.getOrDefault(true, new ArrayList<>());
        if (CollectionUtils.isNotEmpty(insertData)) {
            service.saveBatch(insertData);
        }

        // 提取需要更新的数据
        List<T> updateData = collect.getOrDefault(false, new ArrayList<>());
        if (CollectionUtils.isNotEmpty(updateData)) {
            // 批量更新数据
            service.updateBatchByIdWithoutLogicDelete(updateData);
        }

        log.debug(
                "[{}] data size:[{}],insert data size:[{}],update data size:[{}]",
                entityClass.getSimpleName(),
                dataCollect.size(),
                insertData.size(),
                updateData.size()
        );
        
        insertData.addAll(updateData);
        return insertData;
    }

    /**
     * 执行填充ID策略
     *
     * @param isFillId
     *         是否填充 ID
     * @param newData
     *         数据
     * @param <T>
     *         类型
     */
    private static <T extends IBaseDO<T>> void execFillIdStrategy(boolean isFillId, T newData) {
        Long id = newData.getId();
        if (id != null) {
            return;
        }

        if (!isFillId) {
            throw new RuntimeException("data id can't null,data:" + newData);
        }

        newData.setId(IdWorker.getId());
    }
}

session 批量写库优化

  • 由于原来的实现只需要一条 SQL 进行批量更新,现在插入更新逻辑是在业务代码中实现,最少是需要 2 条 SQL ,频繁发送 SQL 进行读写操作耗时非常大,我们需要对插入更新操作的 SQL 进行批量输入执行,使用 BATCH 批处理模式,减少重复预编译的次数。

工具类整合

为了更加方便使用, MybatisPlusUtils 工具类整合进去自定义 IServiceExtensionServiceImplExtension 模板

  • 整合后 IServiceExtension
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.toolkit.SqlHelper;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;

public interface IServiceExtension<T extends IBaseDO<T>> extends IService<T> {
    @Override
    BaseMapperExtension<T> getBaseMapper();

    // === BaseMapperExtension 新默认方法 ===

    /**
     * 通过 id 更新实体(忽略逻辑删除功能)
     *
     * @param entity
     *         实体数据
     *
     * @return boolean
     */
    default boolean updateByIdWithoutLogicDelete(T entity) {
        return SqlHelper.retBool(getBaseMapper().updateByIdWithoutLogicDelete(entity));
    }

    default List<T> listWithoutLogicDelete(Wrapper<T> queryWrapper) {
        return getBaseMapper().selectListWithoutLogicDelete(queryWrapper);
    }

    default List<T> listWithoutLogicDelete() {
        return listWithoutLogicDelete(Wrappers.emptyWrapper());
    }

    // === 增强型方法 ===

    /**
     * 批量根据 ID 更新(忽略逻辑删除)
     *
     * @param entityList
     *         实体列表
     * @param batchSize
     *         批次大小
     *
     * @return boolean
     */
    boolean updateBatchByIdWithoutLogicDelete(Collection<T> entityList, int batchSize);

    /**
     * 批量根据 ID 更新(忽略逻辑删除)
     *
     * @param entityList
     *         实体列表
     *
     * @return boolean
     */
    @Transactional(rollbackFor = Exception.class)
    default boolean updateBatchByIdWithoutLogicDelete(Collection<T> entityList) {
        return updateBatchByIdWithoutLogicDelete(entityList, DEFAULT_BATCH_SIZE);
    }

    /**
     * mysql insert on duplicate key update 替代方法
     *
     * @param dataCollect
     *         新数据列表
     * @param isFillId
     *         无 ID 是否填充 ID
     * @param uniqueCondition
     *         唯一索引
     * @param updateMapping
     *         更新函数:函数入参为查询到的旧数据 & 新数据,出参为更新后的旧数据(不能设置id和相关唯一索引)
     *
     * @return {@code  List<T>}
     */
    List<T> insertOnUpdateBatch(
            Collection<T> dataCollect,
            boolean isFillId,
            BiFunction<T, LambdaQueryWrapper<T>, LambdaQueryWrapper<T>> uniqueCondition,
            BiFunction<T, T, T> updateMapping
    );

    @Transactional(rollbackFor = Exception.class)
    default List<T> insertOnUpdateBatch(
            Collection<T> dataCollect,
            BiFunction<T, LambdaQueryWrapper<T>, LambdaQueryWrapper<T>> uniqueCondition,
            BiFunction<T, T, T> updateMapping
    ) {
        return insertOnUpdateBatch(dataCollect, true, uniqueCondition, updateMapping);
    }
}
  • 整合后 ServiceImplExtension
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.apache.ibatis.binding.MapperMethod;
import org.springframework.transaction.annotation.Transactional;

import java.util.Collection;
import java.util.List;
import java.util.function.BiFunction;

public abstract class ServiceImplExtension<M extends BaseMapperExtension<T>, T extends IBaseDO<T>> extends ServiceImpl<M, T> implements IServiceExtension<T> {
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean updateBatchByIdWithoutLogicDelete(Collection<T> entityList, int batchSize) {
        String sqlStatement = mapperClass.getName() + StringPool.DOT + "updateByIdWithoutLogicDelete";
        // 批量更新优化  使用 BATCH 批处理模式
        return executeBatch(entityList, batchSize, (sqlSession, entity) -> {
            MapperMethod.ParamMap<T> param = new MapperMethod.ParamMap<>();
            param.put(Constants.ENTITY, entity);
            sqlSession.update(sqlStatement, param);
        });
    }

    @Transactional(rollbackFor = Exception.class)
    @Override
    public List<T> insertOnUpdateBatch(
            Collection<T> dataCollect,
            boolean isFillId,
            BiFunction<T, LambdaQueryWrapper<T>, LambdaQueryWrapper<T>> uniqueCondition,
            BiFunction<T, T, T> updateMapping
    ) {
        return MyBatisPlusUtils.insertOnUpdateBatch(this, dataCollect, isFillId, uniqueCondition, updateMapping);
    }
}

使用案例

基础实体

继承 BaseDO

@ApiModel(value = "网络拓扑图-区域")
@Data
@TableName(value = "cmdb_topology_region")
public class CmdbTopologyRegion extends BaseDO<CmdbTopologyRegion> {
    @TableField(value = "tenant_code")
    @ApiModelProperty(value = "租户编码")
    private Long tenantCode;
}

Mapper 接口

继承 BaseMapperExtension 接口

public interface CmdbTopologyRegionMapper extends BaseMapperExtension<CmdbTopologyRegion> {
}

Service 接口

继承 IServiceExtension 接口

@Validated
public interface CmdbTopologyRegionService extends IServiceExtension<CmdbTopologyRegion> {
    @Validated
    int insertOrUpdate(List<CmdbTopologyRegion> regions);
}

Service 实现

@Slf4j
@Service
public class CmdbTopologyRegionServiceImpl extends ServiceImplExtension<CmdbTopologyRegionMapper, CmdbTopologyRegion> implements CmdbTopologyRegionService {
    @Override
    public int insertOrUpdate(List<CmdbTopologyRegion> regions) {
        // regions 为插入更新的数据列表
        List<CmdbTopologyRegion> update = insertOnUpdateBatch(regions,
                // 唯一索引查询条件构造,这个案例只是一个唯一索引 TenantCode,如果没有唯一索引,直接 return wrapper,如果存在多个唯一索引,使用 wrapper.eq(唯一索引1 构造).or(w->w.eq(唯一索引2 构造))
                (newData, wrapper) -> wrapper.eq(CmdbTopologyRegion::getTenantCode,"1002"),
                (newData, oldData) -> {
                    // 存在数据时候,会调用这个更新回调方法,入参为(新数据,数据库查询数据),这里需要把 newData 的更新字段值填充进去 oldData 返回
                    BeanUtil.copyProperties(newData, oldData, "id","tenantCode");
                    return oldData;
                }
        );
        return update.size();
    }
}

问题回顾

  1. 详细查看MySQL 插入更新原理
  2. 不同数据库的语法不一致,只能使用标准 SQL ,所以最好使用业务代码逻辑实现
  3. 详细查看 MyBatisPlusUtils 工具类的实现方式
  4. 自定义 SQL 注入器实现
  5. 使用 Mybatis BATCH 批处理模式

优点缺点

优点

  • 简单易用,支持原有 MybatisPlusLambda 表达式查询方式,不需要自定义 SQL 实现
  • 需要使用插入更新的时候,不需要考虑不同数据库兼容性
  • 有清晰的业务代码逻辑实现,可以对每条需要更新数据的自定义属性拷贝和转换,同时具备对更新数据进行回调
  • 对原有代码改造少:把原来继承实现的基础模板替换(BaseMapper ,IService ,ServiceImpl

缺点

  • 执行速度慢,SQL 执行数量变多,原来只需要一条 SQL 就可以实现批量插入更新,现在需要多条 SQL (包括查询、插入和更新语句)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容