MyBatis详解4.映射器Mapper

字节跳动飞书内推!
北京、杭州、武汉、广州、深圳、上海,六大城市等你来投。
感兴趣的朋友可以私我咨询&内推,也可以通过链接直接投递
海量HC,极速响应,快来和我成为同事吧。
今日头条、抖音、Tik Tok也可以内推~

点击进入我的博客

MyBatis详解1.概述
MyBatis详解2.MyBatis使用入门
MyBatis详解3.MyBatis配置详解
MyBatis详解4.映射器Mapper
MyBatis详解5.动态SQL
MyBatis详解6.MyBatis技术内幕
MyBatis详解7.插件
MyBatis详解8.集成Spring

1 概述

MyBatis是针对映射器构造的SQL构建的轻量级框架,并且通过配置生成对应的JavaBean返回给调用者,而这些配置主要便是映射器。

元素名称 描述 备注
select 查询语句,最常用、最复杂的元素之一 可以自定义参数,返回结果集等
insert 插入语句 执行后返回一个整数,代表插入的条数
update 更新语句 执行后返回一个整数,代表更新的条数
delete 删除语句 执行后返回一个整数,代表删除的条数
parameterMap 定义参数映射关系 即将被删除的元素,不建议大家使用
sql 允许定义一部分的SQL,然后在各个地方引用
resultMap 用来描述从数据库结果集中来加载对象,它是它将提供映射规则最复杂、最强大的元素 它提供映射规则
cache 给定命名空间的缓存配置
cache-ref 其他命名空间缓存配置的引用

2 引入映射器

通过文件类路径引入
    <mappers>
        <mapper resource="mappers/TbNameMapper.xml"/>
    </mappers>
通过包名引入映射器
public interface TbNameMapper {
    @Select("SELECT * FROM tb_name WHERE id = #{id}")
    UserDomain select(Long id);
}
    <mappers>
        <package name="com.ankeetc.spring.mapper"/>
    </mappers>
用类注册引入映射器
    <mappers>
        <mapper class="com.ankeetc.spring.mapper.TbNameMapper"/>
    </mappers>

3 select元素

3.1 select元素的配置元素

元素 说明 备注
id 它和 Mapper的命名空间组合起来是唯一的,提供给MyBatis调用 如过命名空间和id组合起来不唯一将抛出异常
parameterType 可以给出类的全命名,也可以给出类的别名,但使用别名必须是MyBatis内部定义或者自定义的 可以选择JavaBean、Map等复杂的参数类型传递给SQL
parameterMap 即将废弃的元素,我们不再讨论它
resultType 定义类的全路径,在允许自动匹配的情况下,结果集将通过JavaBean的规范映射;也可以定义为int、 double等参数;也可以使用别名,但是要符合别名规范;不能和 resultMap同时使用 常用参数之一
resultMap 它是映射集的引用,将执行强大的映射功能 可以配置映射规则、级联、typeHandler等
flushCache 它的作用是在调用SQL后,是否要求清空之前查询的本地缓存和二级缓存 布尔值,默认值为 false
useCache 启动二级缓存的开关,是否将此次结果缓存 布尔值,默认值为tue
timeout 设置超时参数,等超时的时候将抛出异常,单位为秒 默认值是数据库厂商提供的JDBC驱动所设置的秒数
fetchSize 获取记录的总条数设定 默认值是数据库厂商提供的JDBC驱动所设置的条数
statementType 告诉MyBatis使用哪个JDBC的Statement工作,取值为STATEMENT、PREPARED、CallableStatement 默认为PREPARED
resultSetType 这是对JDBC的resultSet接口而言,它的值包括 FORWARD_ONLY(游标允许向前访问)、 SCROLL_SENSITIVE(双向滚动,但不及时更新,就是如果数据库里的数据修改过,并不在resultSet中反应出来)、 SCROLL_ INSENSITIVE(双向滚动,并及时跟踪数据库的更新,以便更改 resultSet中的数据) 默认值是数据库厂商提供的JDBC驱动所设置的
databaseId 参考第三章
resultOrdered 这个设置仅适用于嵌套结果集select语句。如果为true,就是假设包含了嵌套结果集或者是分组了。当返回一个主结果行的时候,就不能对前面结果集的引用。这就确保了在获取嵌套的结果集的时候不至于导致内存不够用 布尔值,默认为false
resultSets 适合于多个结果集的情况,它将列出执行SQL后每个结果集的名称,每个名称之间用逗号分隔 很少使用

3.2 自动映射

mybatis-config.xml文件中<settings>标签有一个参数autoMapping Behavior,当它不设置为NONE的时候,MyBatis会提供自动映射的功能,只要返回的SQL列名和JavaBean的属性一致,MyBatis就会帮助我们回填这些字段而无需任何配置,它可以在很大程度上简化我们的配置工作。

自动映射策略

自动映射可以在settings元素中配置autoMappingBehavior属性值来设置其策略:

  • NONE,取消自动映射。
  • PARTIAL(默认值),只会自动映射,没有定义嵌套结果集映射的结果集。
  • FULL,会自动映射任意复杂的结果集(无论是否嵌套),在性能上会下降。
下划线转驼峰

如果你的数据库是规范命名的,即每一个单词都用下划线分隔,POJO采用驼峰式命名方法,那么可以在<settings>设置 mapUnderscoreToCamelCase为true,这样就可以实现从数据库到POJO的自动映射了。

3.3 传递多个参数

使用Map传递多个参数

可是使用Map来传递多个参数,如下所示。这种方式的不足之处是业务关联性不强,造成可读性下降。

public interface TbNameMapper {
    UserDomain select(Map<String, Object> map);
}
    <!-- parameterType也可以缩写为map -->
    <select id="select" resultType="com.ankeetc.spring.domain.UserDomain" parameterType="java.util.Map" >
        SELECT * FROM tb_name WHERE id = #{id}
    </select>
使用注解传递多个参数

可是使用@Param来实现传递多个参数,此时无需在XML中配置parameterType。

public interface TbNameMapper {
    UserDomain select(@Param("id") Long id);
}
    <!-- 无须parameterType -->
    <select id="select" resultType="com.ankeetc.spring.domain.UserDomain" >
        SELECT * FROM tb_name WHERE id = #{id}
    </select>
也可以使用JavaBean传递多个参数
public interface TbNameMapper {
    UserDomain select(UserDomain userDomain);
}
    <select id="select" resultType="com.ankeetc.spring.domain.UserDomain" parameterType="com.ankeetc.spring.domain.UserDomain">
        SELECT * FROM tb_name WHERE id = #{id}
    </select>

3.4 使用resultMap映射结果集

如果自动映射满足不了需求,此时需要通过resultMap来映射。可以创建一个<resultMap>元素,然后通过<select>中的resultMap属性来引用resultMap,后面会详细分析<resultMap>元素。

4 insert元素

4.1 insert元素的配置元素

insert的很多配置元素是和select一样的,就不再赘述,下面列出select中没有的属性

属性 说明 备注
keyProperty 表示以哪个列作为属性的主键,不能和keyColumn同时使用 设置哪个键为主键,联合主键可以用逗号将其隔开
keyColumn 表示第几列是主键,不能和keyProperty同时使用 联合主键可以用逗号将其隔开
useGeneratedKeys 这会令MyBatis使用JDBC的getGeneratedKeys方法来取出由数据库内部生成的主键,但是使用它就必须要给keyProperty或者 keyColumn赋值 布尔值,默认为false
lang 自定义语言,可使用第三方语言 使用得较少,不做介绍

4.2 主键回填

  1. 设置useGeneratedKeys=true
  2. 设置keyProperty为需要回填的bean的属性
    <insert id="insert" parameterType="com.ankeetc.spring.domain.UserDomain" useGeneratedKeys="true" keyProperty="id" >
        INSERT INTO tb_name (name) VALUES (#{name})
    </insert>
    public static void func() {
        // 获取主键值
        UserDomain userDomain = new UserDomain("LOL");
        tbNameMapper.insert(userDomain);
        System.out.println(userDomain.getId());
    }

4.3 使用selectKey元素

在insert元素中可以使用selectKey元素来对回填的主键进行修改

    <insert id="insert" parameterType="com.ankeetc.spring.domain.UserDomain" useGeneratedKeys="true" keyProperty="id" >
        <!-- 在原有id的基础上加2 -->
        <selectKey keyProperty="id" resultType="int" order="AFTER">
            SELECT (max(id) + 2) AS newId FROM tb_name
        </selectKey>
        INSERT INTO tb_name (name) VALUES (#{name})
    </insert>

5 update和delete元素

这两个元素和insert元素基本一样,都会返回一个整数表示此次执行影响的行数。

6 参数

6.1 参数配置

我们可以传入一个简单的参数如基本类型和字符串;也可以传入JavaBean;还可以指定特定的类型,以确定使用哪个 typeHandler处理它们。定义参薮属性的时候,MyBatis不允许换行

  • #{age, javaType=int, jdbcType=NUMERIC}
  • 指定typeHandler:#{age, javaType=int, jdbcType=NUMERIC, typeHandler=MyTypeHandler}
  • 数值型的参数设置其保存的精度:#{price, javaType=double, jdbcType=NUMERIC, numericScale=2}

6.2 支持存储过程

  1. 存储过程存在3种参数,通过指定mode属性来确定是哪一种参数:IN、OUT、INOUT。
  2. 当参数设置为OUT、INOUT的时候,MyBatis会将存储过程返回的结果设置到你制定的参数中。
  3. 当你返回的是一个游标(JdbcType=CURSOR)的时候,还需要去设置resultMap以便MyBatis将存储过程的参数映射到对应的类型。

6.3 特殊字符串替换和处理

有时候我们需要的是传递SQL语句的本身,而不是SQL所需要的参数,MyBatis也对这样的场景进行了支持,这些是Hibernate难以做到的。例如,我们需要传递变量columns="name, value"给SQL,让其组装成为SQL语句,而不想被像普通参数一样被处理,这个时候可以用${param}来实现。

public static void func() {
    UserDomain userDomain = tbNameMapper.select("name, value", 18L);
}
public interface TbNameMapper {
    UserDomain select(@Param("columns") String columns, @Param("id") Long id);
}
<select id="select" resultType="com.ankeetc.spring.domain.UserDomain">
    SELECT ${columns} FROM tb_name WHERE id = #{id}
</select>

7 sql元素

sql元素的意义,在于我们可以定义一串SQL语句的组成部分,其他的语句可以通过引
用来使用它。这样就可以实现一处定义多处引用,大大减少工作量。

    <sql id="sql1">
        name, value
    </sql>
    <select id="select" resultType="com.ankeetc.spring.domain.UserDomain">
        SELECT 
        <include refid="sql1"/>
        FROM tb_name WHERE id = #{id}
    </select>

8 resultMap结果映射集

resultMap的作用是定义映射规则、级联的更新、定制类型转化器等,主要作用是一个结果集的映射关系。

8.1 resultMap元素里的元素

resultMap里的元素
  • constructor元素用于配置构造方法,一个POJO可能不存在无参数的构造方法,这个时候我们就可以使用 constructor进行配置。
  • id元素是表示哪个列是主键,允许多个主键构成联合主键。
  • result是配置POJO到SQL列名的映射关系。
  • 级联属性association、discriminator、collection后面详细介绍

8.2 使用POJO存储结果集

我们在select中使用map存储结果集,这样虽然可以匹配所有结果集,但会造成可读性的下降,最好的方式是使用POJO。我们可以在select元素中使用resultMap属性引用我们定义好的resultMap元素。

    <!-- id是resultMap的唯一标识,type是对应POJO的全路径名 -->
    <resultMap id="base_result_map" type="com.ankeetc.spring.domain.UserDomain">
        <id property="id" column="id" javaType="Long" jdbcType="BIGINT"/>
        <result property="name" column="name" javaType="String" jdbcType="VARCHAR"/>
        <result property="value" column="value" javaType="Integer" jdbcType="INTEGER"/>
    </resultMap>

    <select id="select" resultMap="base_result_map">
        SELECT * FROM tb_name WHERE id = #{id}
    </select>

8.3 级联属性

在数据库中包含着一对一、一对多、多对多的关系,比如一个数据库中有一张班级表:

  • 一对一的关系:班级表里每个班级都有一个班主任
  • 一对多的关系:每个班级有多个学生
  • 多对多的关系:一个老师既可以是多个班级的老师,一个班级也可以有多个老师(在实际中,多对多的关系应用不多,因为它会增加理解和关联的复杂度,更好的方法是用一对多的关系把它分解为双向关系)
MyBatis中3种级联
  • association,代表一对一关系,比如每个班级都有一个班长
  • collection,代表一对多关系,比如每个班级有多个学生
  • discriminator,鉴别器,它可以根据实际选择采用哪个类作为实例,允许你根据特定的条件去关联不同的结果集。
association与collection

现在有两张表分别是班级表和学生表,我们在班级表的Mapper文件中,通过association映射了班级和班长一对一的关系;通过collection映射了班级和学生的一对多关系。此时我们通过ClassMapper的select方法,能够自动装填对应的班长和学生信息。

CREATE TABLE `class` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '',
  `grade` int(11) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

CREATE TABLE `student` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL DEFAULT '',
  `sex` int(11) NOT NULL DEFAULT '0',
  `class_id` bigint(20) NOT NULL DEFAULT '0',
  `monitor` int(11) NOT NULL DEFAULT '0' COMMENT '0不是班长,1是班长',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
<!-- 这是班级表 -->
<mapper namespace="com.ankeetc.spring.mapper.ClassMapper" >
    <resultMap id="base_result_map" type="com.ankeetc.spring.domain.ClassDomain">
        <id property="id" column="id" javaType="Long" jdbcType="BIGINT" />
        <result property="name" column="name" javaType="String" jdbcType="VARCHAR"/>
        <!-- 班级与班长的一对一关系 -->
        <association property="classMonitor" column="id" select="com.ankeetc.spring.mapper.StudentMapper.selectClassMonitor"/>
        <!-- 班级与学生的一对一关系 -->
        <collection property="students" column="id" select="com.ankeetc.spring.mapper.StudentMapper.selectByClassId"/>
    </resultMap>

    <select id="select" resultMap="base_result_map">
        SELECT * FROM class WHERE id = #{id}
    </select>
</mapper>
<!-- 这是学生表 -->
<mapper namespace="com.ankeetc.spring.mapper.StudentMapper" >
    <resultMap id="base_result_map" type="com.ankeetc.spring.domain.StudentDomain">
        <id property="id" column="id" javaType="Long" jdbcType="BIGINT" />
        <result property="name" column="name" javaType="String" jdbcType="VARCHAR"/>
        <result property="sex" column="sex" javaType="Integer" jdbcType="INTEGER"/>
        <result property="classId" column="class_id" javaType="Long" jdbcType="BIGINT"/>
        <result property="monitor" column="monitor" javaType="Integer" jdbcType="INTEGER"/>
    </resultMap>

    <select id="selectByClassId" resultMap="base_result_map">
        SELECT * FROM student WHERE class_id = #{classId}
    </select>

    <select id="selectClassMonitor" resultMap="base_result_map">
        SELECT * FROM student WHERE class_id = #{classId} AND monitor = 1 LIMIT 1
    </select>
</mapper>
discriminator鉴别器

鉴别器级联是在特定的条件下去使用不同的POJO,比如在上例中,我们可以根据班级中的年级属性(grade)进行判断去关联是小学还是初中,然后分别进行不同的映射。discriminator相当于Java语言中的switch语句

<mapper namespace="com.ankeetc.spring.mapper.ClassMapper" >

    <resultMap id="base_result_map" type="com.ankeetc.spring.domain.ClassDomain">
        <id property="id" column="id" javaType="Long" jdbcType="BIGINT" />
        <result property="name" column="name" javaType="String" jdbcType="VARCHAR"/>
        <association property="classMonitor" column="id" select="com.ankeetc.spring.mapper.StudentMapper.selectClassMonitor"/>
        <collection property="students" column="id" select="com.ankeetc.spring.mapper.StudentMapper.selectByClassId"/>
        <!-- 通过discriminator进行分配 -->
        <discriminator javaType="Integer" column="grade">
            <case value="1" resultMap="primary"/>
            <case value="2" resultMap="junior_high"/>
        </discriminator>
    </resultMap>
    
    <!-- 如果grade=1则会使用这个resultMap,返回的实际类型是PrimaryClassDomain -->
    <resultMap id="primary" type="com.ankeetc.spring.domain.PrimaryClassDomain" extends="base_result_map">
        <result property="grade" column="grade" javaType="Integer" jdbcType="INTEGER"/>
    </resultMap>
    
    <!-- 如果grade=2则会使用这个resultMap,返回的实际类型是PrimaryClassDomain -->
    <resultMap id="junior_high" type="com.ankeetc.spring.domain.PrimaryClassDomain" extends="base_result_map">
        <result property="grade" column="grade" javaType="Integer" jdbcType="INTEGER"/>
    </resultMap>

    <select id="select" resultMap="base_result_map">
        SELECT * FROM class WHERE id = #{id}
    </select>
</mapper>
级联的优缺点

级联的优势:能够方便快捷地获取数据。
级联的缺点:有时候我们并不需要获取所有的数据;有一个关联我们就要多执行一次SQL,这样会造成SQL执行过多导致性能下降,这就是N+1的问题,为了解决这个问题我们应该考虑采用延迟加载的功能。

全局延迟加载

为了处理N+1的问题,My Batis引入了延迟加载的功能。延迟加载功能的意义在于,开始并不取出级联数据,只有当使用它了才发送SQL去取回数据。MyBatis的配置中有两个全局的参数lazyLoadingEnabled和aggressiveLazyLoading。

  • lazyLoadingEnabled的含义是是否开启延迟加载功能;
  • aggressiveLazyLoading的含义是对任意延迟属性的调用会使带有延迟加载属性的对象完整加载;反之,每种属性将按需加载。
局部延迟加载

由于全局配置的灵活性较差,MyBatis提供了局部延迟加载的功能。我们可以在association和 collection元素上加入属性值fetchType就可以了。它有两个取值范围,eager和lazy。它的默认值取决于你在配置文件settings的配置。假如我们没有配置它,那么它们就是eager一旦你配置了它们,那么全局的变量就会被它们所覆盖,这样我们就可以灵活地指定哪些东西可以立即加载,哪些东西可以延迟加载。

9 缓存cache

目前流行的缓存服务器有MongoDB、Redis、Ehcache等,缓存将数据保存在内存中,在读取的时候无需再从磁盘读入,因此具备快速读取和使用的特点。如果缓存命中率高,那么可以极大地提高系统的性能;如果缓存命中率很低,那么缓存就不存在使用的意义了,所以使用缓存的关键就是提高命中。

9.1 一级系统缓存

  • MyBatis对缓存提供支持,在没有配置的默认的情况下,它只开启一级缓存(一级缓存只是相对于同一个 SqlSession而言),所以在参数和SQL完全一样的情况下,我们使用同一个 SqlSession对象调用同一个Mapper的方法,往往只执行一次SQL。
  • 使用SqlSession第一次查询后,MyBatis会将其放在缓存中。以后再查询的时候,如果没有声明需要刷新且缓存没超时,SqlSession都只会取出当前缓存的数据,而不会再次发送SQL到数据库。
  • 如果使用的是不同的SqlSession对象,因为不同的SqlSession都是相互隔离的,所以用相同的 Mapper、参数和方法,它还是会再次发送SQL到数据库去执行。
    public static void func() throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class);
            // 在此处执行了两次select操作,只有依次数据库访问操作
            studentMapper.selectClassMonitor(1L);
            studentMapper.selectClassMonitor(1L);

            sqlSession.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
==>  Preparing: SELECT * FROM student WHERE class_id = ? AND monitor = 1 LIMIT 1 
==> Parameters: 1(Long)
<==    Columns: id, name, sex, class_id, monitor
<==        Row: 2, lucas, 20, 1, 1
<==      Total: 1
    <!--使用标准log来实现日志打印-->
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>
    </settings>

9.2 二级系统缓存

一级缓存在各个SqlSession间是相互隔离的。为了克服这个问题,我们往往需要配置二级缓存,使得缓存在SqlsessionFactory层面上能够提供给各个SqISession对象共享,二级缓存默认是不开启的

  1. 二级缓存的时候要求返回的POJO必须是可序列化的,也就是要求实现Serializable接口。
  2. 配置的缓存的方法很简单,只需要在Mapper.xml文件中加入<cache/>
    public static void func() throws Exception {
        InputStream is = Resources.getResourceAsStream("mybatis-config.xml");
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(is);
        try (SqlSession sqlSession1 = sqlSessionFactory.openSession(); SqlSession sqlSession2 = sqlSessionFactory.openSession()) {
            StudentMapper studentMapper1 = sqlSession1.getMapper(StudentMapper.class);
            StudentMapper studentMapper2 = sqlSession2.getMapper(StudentMapper.class);

            // 如果没有<cache/>标签,这条语句将会执行两次select操作
            studentMapper1.selectClassMonitor(1L);
            studentMapper2.selectClassMonitor(1L);

            sqlSession1.commit();
            sqlSession2.commit();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
cache的默认配置
  • 映射语句文件中的所有select语句将会被缓存。
  • 映射语句文件中的所有insert、update和delete语句会刷新缓存。
  • 缓存会使用默认的Least Recently Used(LRU,最近最少使用的)算法来收回。
  • 根据时间表,比如No Flush Interval,(NFI,没有刷新间隔),缓存不会以任何时
    间顺序来刷新
  • 缓存会存储列表集合或对象(无论查询方法返回什么)的1024个引用。
  • 缓存会被视为是read/write(可读/可写)的缓存,意味着对象检索不是共享的,且可以安全地被调用者修改,不干扰其他调用者或线程所做的潜在修改。
配置缓存属性
  • eviction:代表的是缓存回收策略,LRU(最近最少使用的,移除最长时间不用的对象)、FFO(先进先出,按对象进入缓存的顺序来移除它们)、SOFT(软引用,移除基于垃圾回收器状态和软引用规则的对象)、WEAK(弱引用,更积极地移除基于垃圾收集器状态和弱引用规则的对象)。
  • flushInterval:刷新间隔时间,单位为亳秒,这里配置的是100秒刷新,如果不配置它,那么当SQL被执行的时候才会去刷新缓存。
  • size:引用数目,一个正整数,代表缓存最多可以存储多少个对象,设置过大会导致内存溢出。
  • readOnly:只读,意味着缓存数据只能读取而不能修改,这样设置的好处是我们可以快速读取缓存,缺点是我们没有办法修改缓存,默认不允许我们修改。

9.3 自定义缓存

系统缓存是MyBatis应用机器上的本地缓存,但是在分布式系统上,需要使用各类不同的缓存服务器,这是需要实现MyBatis为我们提供的接口org.apache.ibatis.cache.Cache,缓存接口简介如下所示:

  // 获取缓存编号
  String getId();
  // 保存key value
  void putObject(Object key, Object value);
  // 根据key获取value
  Object getObject(Object key);
  // 根据key移除对象
  Object removeObject(Object key);
  // 清空缓存
  void clear();
  // 获取缓存大小
  int getSize();
  // 获取缓存对读写锁
  ReadWriteLock getReadWriteLock();

我们在映射器上的insert、delete、select、update等元素,可以配置useCache和 flushCache两个属性来控制缓存的使用,useCache表示是否需要使用缓存,而flushCache表示插入后是否需要刷新缓存。

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

推荐阅读更多精彩内容