mybatis入门详解

什么是mybatis

MyBatis 是支持定制化 SQL、存储过程以及高级映射的优秀的持久层框架。MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集。MyBatis 可以对配置和原生Map使用简单的 XML 或注解,将接口和 Java 的 POJOs(Plain Old Java Objects,普通的 Java对象)映射成数据库中的记录。--官方文档

说白了,mybatis就是用来简化和数据库的操作,让程序员不太需要关注和数据库连接、事务等的操作。同时对动态 sql 有很好的支持,并且可以让java对象方便的映射到数据库sql上,进而映射到数据库中的记录,这样就从重点关注业务逻辑。

我们的项目是通过maven管理的话,使用mybatis的时候,需要引入mybatis依赖的包

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

关于SqlSessionFactory

SqlSessionFactory是mbatis中的核心概念,它相当于是数据库在代码中的映射了。基于mybatis的应用都会有一个sqlSessionFactory,SqlSessionFactory 一旦被创建就应该在应用的运行期间一直存在,没有任何理由对它进行清除或重建。使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次,多次重建 SqlSessionFactory 被视为一种代码“坏味道(bad smell)”。因此 SqlSessionFactory 的最佳作用域是应用作用域。有很多方法可以做到,最简单的就是使用单例模式或者静态单例模式。因为现在的项目都是集成了spring,所以我在这里只说使用spring的情况下,sqlSessionFactory是如何得到的。

mybatis要和spring集成,需要引入Mybatis-Spring模块

<dependency>
  <groupId>org.mybatis</groupId>
  <artifactId>mybatis-spring</artifactId>
  <version>x.x.x</version>
</dependency>

在基本的 MyBatis 中,session 工厂可以使用 SqlSessionFactoryBuilder 来创建。而在 MyBatis-Spring 中,则使用 SqlSessionFactoryBean 来替代,通过spring的xml配置文件中,加入下面的代码,就可以得到sqlSessionFactory

 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="datasource" />
        <property name="typeAliasesPackage" value="com.xdf.ucan.vps.avatar.pojo" />
        <property name="mapperLocations"
                  value="classpath*:sample/config/mappers/**/*.xml" />
    </bean>

大家看到了sqlSessionFactory需要注入一个DataSource,dataSource就是一个数据源,对应一个数据库的连接,它主要是用来设置数据的环境信息和数据库的配置,看一下下面的配置

<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://xxx.xxx.xxx.xxx/avatar?useUnicode=true&characterEncoding=UTF8" />
    <property name="username" value="admin" />
    <property name="password" value="admin" />
    <!-- 连接初始值,连接池启动时创建的连接数量的初始值 -->
    <property name="initialSize" value="10" />
    <!-- 连接池的最大值,同一时间可以从池分配的最多连接数量,0时无限制 -->
    <property name="maxActive" value="100" />
    <!-- 最大空闲值.当经过一个高峰时间后,连接池可以慢慢将已经用不到的连接慢慢释放一部分,一直减少到maxIdle为止 ,0时无限制-->
    <property name="maxIdle" value="0
" />
    <!-- 最小空闲值.当空闲的连接数少于阀值时,连接池就会预申请去一些连接,以免洪峰来时来不及申请 -->
    <property name="minIdle" value="${minIdle}" />
    <!-- 是否对已备语句进行池管理(布尔值),是否对PreparedStatement进行缓存 -->
    <property name="poolPreparedStatements" value="true" />
    <!-- 是否对sql进行自动提交 -->
    <property name="defaultAutoCommit" value="true" />
</bean>

可以是任意的JDBC datasource,上面那个只是mysql的一个例子
从配置信息看,mapperLocations属性含义就很明显了。mapperLocations 属性使用一个资源位置的 list。 这个属性可以用来指定 MyBatis 的 XML 映射器文件的位置。 它的值可以包含 Ant 样式来加载一个目录中所有文件, 或者从基路径下 递归搜索所有路径

<property name="mapperLocations" value="classpath*:sample/config/mappers/**/*.xml" />

这会从类路径下加载在 sample.config.mappers 包和它的子包中所有的 MyBatis 映射器 XML 文件。这个文件具体是什么内容我们一会细讲
typeAliasesPackage :它一般对应我们的实体类所在的包,这个时候会自动取对应包中不包括包名的简单类名作为包括包名的别名。多个package之间可以用逗号或者分号等来进行分隔。这样的话,其他mapper的配置文件中就可以通过别名使用它了
sqlSessionFactory还有很多其他属性的配置,想要了解可以查看官方文档去。我们这儿只说一些常用的配置信息。

关于SqlSession

正如从名字上看到的,sqlsession是有生命周期,可以理解对应一次数据库事务,基本可以理解为一次事务对应一个sqlsession。如果你现在正在使用一种 Web 框架(SpringMVC),开发一个web项目,要考虑 SqlSession 放在一个和 HTTP 请求对象相似的作用域中。换句话说,每次收到的 HTTP 请求,就可以打开一个 SqlSession,返回一个响应,就关闭它。这个关闭操作是很重要的,你应该把这个关闭操作放到 finally 块中以确保每次都能执行关闭。下面的示例就是一个确保 SqlSession 关闭的标准模式:

SqlSession session = sqlSessionFactory.openSession();
try {
  // do work
} finally {
  session.close();
}

sqlsession是从SqlSessionFactory获取的。sqlsession包含了数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。在使用spring的时候不需要像上面那样关注sqlsession的创建和关闭,sqlsession的整个生命周期都由spring来管理,但是你还是要理解sqlsession的生命周期的。在spring配置中获取sqlSession方法有好几种
1.SqlSessionTemplate
2.SqlSessionDaoSupport
3.MapperFactoryBean
我们在这儿重点说一下第三种方式。因为第三种使用起来是最方便的。现在大部分项目也是使用第三种方式。它在背后使用了java动态代理技术。使用注入的映射器代码,在 MyBatis,Spring 或 MyBatis-Spring 上面不会有直接的依赖。 MapperFactoryBean 创建的代理控制开放和关闭 session,翻译任意的异常到 Spring 的 DataAccessException 异常中。此外,如果需要或参与到一个已经存在活动事务中,代理将 会开启一个新的 Spring 事务。
数据映射器接口可以按照如下做法加入到 Spring 中:

<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
  <property name="mapperInterface" value="org.mybatis.spring.sample.mapper.UserDAO" />
  <property name="sqlSessionFactory" ref="sqlSessionFactory" />
</bean>

MapperFactoryBean 创建的代理类实现了 UserDAO 接口,并且在运行时注入到应用程序中。 因为代理创建在运行时环境中(Runtime,译者注) ,那么指定的映射器必须是一个接口,而 不是一个具体的实现类,也就是说你不需要写一个UserDAO的实现类。spring在运行时会通过代理创建一个UserDAO实现类,这个实现类会创建sqlSession,并且会和一个Mybatis的映射器文件对应。下面是UserDAO接口的示例代码

package cn.xdf.ucan.vps.avatar.dao;
import cn.xdf.ucan.vps.avatar.pojo.A_Update;
import java.util.List;
public interface A_UpdateDao {
    int deleteByPrimaryKey(Long id);
    int insert(A_Update record);
    int insertSelective(A_Update record);
    A_Update selectByPrimaryKey(Long id);
    A_Update selectByVersion(String version);
    List<A_Update> selectNextVersionsById(Long id);
    List<A_Update> selectAll();
    int updateByPrimaryKeySelective(A_Update record);

    int updateByPrimaryKey(A_Update record);
}

这个类的方法怎么和一个sql对应,我们后面会说明。
如果 UserDAO 有一个对应的 MyBatis 的 XML 映射器文件, 如果 XML 文件在类路径的 位置和映射器类相同时, 它会被 MapperFactoryBean 自动解析。 没有必要在 MyBatis 配置文 件 中 去 指 定 映 射 器 , 除 非 映 射 器 的 XML 文 件 在 不 同 的 类 路 径 下 ,关于怎么映射,我们在下面说明
注意,当 MapperFactoryBean 需要 SqlSessionFactory 或 SqlSessionTemplate 时。 这些可以通过各自的 SqlSessionFactory 或 SqlSessionTemplate 属性来设置, 或者可以由 Spring 来自动装配。如果两个属性都设置了,那么 SqlSessionFactory 就会被忽略,因为SqlSessionTemplate 是需要有一个 session 工厂的设置; 那个工厂会由 MapperFactoryBean. 来使用。

Mapper XML 文件

MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。
SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):

  • cache – 给定命名空间的缓存配置。
  • cache-ref – 其他命名空间缓存配置的引用。
  • resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
  • parameterMap – 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
  • sql – 可被其他语句引用的可重用语句块。
  • insert – 映射插入语句
  • update – 映射更新语句
  • delete – 映射删除语句
  • select – 映射查询语句
    我们通过一个例子,一步步说清楚这些元素,随便找的一个配置文件,请忽略字段名,实在不想改了。
<?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="cn.xdf.ucan.vps.avatar.dao.UserDao" >
<resultMap id="BaseResultMap" type="cn.xdf.ucan.vps.avatar.pojo.User" >
    <id column="ID" property="id" jdbcType="DECIMAL" />
    <result column="VERSION" property="version" jdbcType="VARCHAR" />
    <result column="URL" property="url" jdbcType="VARCHAR" />
    <result column="FILENAME" property="filename" jdbcType="VARCHAR" />
    <result column="MD5SUM" property="md5sum" jdbcType="VARCHAR" />
    <result column="ANGEL_USER" property="angelUser" jdbcType="VARCHAR" />
    <result column="UPDATE_TYPE" property="updateType" jdbcType="VARCHAR" />
    <result column="updateinfo" property="updateinfo" jdbcType="VARCHAR" />
    <result column="CREATETIME" property="createtime" jdbcType="TIMESTAMP" />
    <result column="UPDATETIME" property="updatetime" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="Base_Column_List" >
    ID, VERSION, URL, FILENAME, MD5SUM, ANGEL_USER, UPDATE_TYPE, updateinfo, CREATETIME, UPDATETIME
  </sql>
  <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" >
    select
    <include refid="Base_Column_List" />
    from a_update
    where ID = #{id,jdbcType=DECIMAL}
  </select>
  <select id="selectByVersion" parameterType="java.lang.String" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from a_update
    where version = #{version,jdbcType=VARCHAR}
  </select>
  <select id="selectNextVersionsById" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from a_update
    where id > #{id,jdbcType=DECIMAL}
    ORDER BY id ASC
  </select>
  <select id="selectAll" resultMap="BaseResultMap">
    select
    <include refid="Base_Column_List" />
    from a_update
    ORDER BY id DESC
  </select>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long" >
    delete from a_update
    where ID = #{id,jdbcType=DECIMAL}
  </delete>
  <insert id="insert" parameterType="cn.xdf.ucan.vps.avatar.pojo.A_Update" >
    insert into a_update (VERSION, URL,
      FILENAME, MD5SUM, ANGEL_USER, 
      UPDATE_TYPE,updateinfo, CREATETIME, UPDATETIME
      )
    values (#{version,jdbcType=VARCHAR}, #{url,jdbcType=VARCHAR},
      #{filename,jdbcType=VARCHAR}, #{md5sum,jdbcType=VARCHAR}, #{angelUser,jdbcType=VARCHAR}, 
      #{updateType,jdbcType=VARCHAR},#{updateinfo,jdbcType=VARCHAR}, now(),now()
      )
  </insert>
  <insert id="insertSelective" parameterType="cn.xdf.ucan.vps.avatar.pojo.A_Update" >
    insert into a_update
    <trim prefix="(" suffix=")" suffixOverrides="," >
      <if test="version != null" >
        VERSION,
      </if>
      <if test="url != null" >
        URL,
      </if>
      <if test="filename != null" >
        FILENAME,
      </if>
      <if test="md5sum != null" >
        MD5SUM,
      </if>
      <if test="angelUser != null" >
        ANGEL_USER,
      </if>
      <if test="updateType != null" >
        UPDATE_TYPE,
      </if>
      <if test="updateinfo != null" >
        updateinfo,
      </if>
        CREATETIME,
        UPDATETIME,
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
      <if test="version != null" >
        #{version,jdbcType=VARCHAR},
      </if>
      <if test="url != null" >
        #{url,jdbcType=VARCHAR},
      </if>
      <if test="filename != null" >
        #{filename,jdbcType=VARCHAR},
      </if>
      <if test="md5sum != null" >
        #{md5sum,jdbcType=VARCHAR},
      </if>
      <if test="angelUser != null" >
        #{angelUser,jdbcType=VARCHAR},
      </if>
      <if test="updateType != null" >
        #{updateType,jdbcType=VARCHAR},
      </if>
      <if test="updateinfo != null" >
        #{updateinfo,jdbcType=VARCHAR},
      </if>
      now(),
      now(),
    </trim>
  </insert>
  <update id="updateByPrimaryKeySelective" parameterType="cn.xdf.ucan.vps.avatar.pojo.A_Update" >
    update a_update
    <set >
      <if test="version != null" >
        VERSION = #{version,jdbcType=VARCHAR},
      </if>
      <if test="url != null" >
        URL = #{url,jdbcType=VARCHAR},
      </if>
      <if test="filename != null" >
        FILENAME = #{filename,jdbcType=VARCHAR},
      </if>
      <if test="md5sum != null" >
        MD5SUM = #{md5sum,jdbcType=VARCHAR},
      </if>
      <if test="angelUser != null" >
        ANGEL_USER = #{angelUser,jdbcType=VARCHAR},
      </if>
      <if test="updateType != null" >
        UPDATE_TYPE = #{updateType,jdbcType=VARCHAR},
      </if>
      <if test="updateinfo != null" >
        updateinfo = #{updateinfo,jdbcType=VARCHAR},
      </if>
      <if test="updatetime != null" >
        UPDATETIME = now(),
      </if>
    </set>
    where ID = #{id,jdbcType=DECIMAL}
  </update>
  <update id="updateByPrimaryKey" parameterType="cn.xdf.ucan.vps.avatar.pojo.A_Update" >
    update a_update
    set VERSION = #{version,jdbcType=VARCHAR},
      URL = #{url,jdbcType=VARCHAR},
      FILENAME = #{filename,jdbcType=VARCHAR},
      MD5SUM = #{md5sum,jdbcType=VARCHAR},
      ANGEL_USER = #{angelUser,jdbcType=VARCHAR},
      UPDATE_TYPE = #{updateType,jdbcType=VARCHAR},
      updateinfo = #{updateinfo,jdbcType=VARCHAR},
      UPDATETIME = now()
    where ID = #{id,jdbcType=DECIMAL}
  </update>
</mapper>

我们来一步步解释xml文件

<mapper namespace="cn.xdf.ucan.vps.avatar.dao.A_UpdateDao" >

namespace:Mybatis中namespace可以简单的理解为用于绑定dao接口,这也就解释了上面的UserDAO是怎么找到对应的mapper方法的。dao接口的方法对应mapper中的sql语名,具体怎么对应,我们下面会说明

<resultMap id="BaseResultMap" type="cn.xdf.ucan.vps.avatar.pojo.A_Update" >
    <id column="ID" property="id" jdbcType="DECIMAL" />
    <result column="VERSION" property="version" jdbcType="VARCHAR" />
    <result column="URL" property="url" jdbcType="VARCHAR" />
    <result column="FILENAME" property="filename" jdbcType="VARCHAR" />
    <result column="MD5SUM" property="md5sum" jdbcType="VARCHAR" />
    <result column="ANGEL_USER" property="angelUser" jdbcType="VARCHAR" />
    <result column="UPDATE_TYPE" property="updateType" jdbcType="VARCHAR" />
    <result column="updateinfo" property="updateinfo" jdbcType="VARCHAR" />
    <result column="CREATETIME" property="createtime" jdbcType="TIMESTAMP" />
    <result column="UPDATETIME" property="updatetime" jdbcType="TIMESTAMP" />
  </resultMap>

这段xml的作用是实现javaBean到数据库字段的映射,如果数据库中的字段名和javaBean的字段名一样,也是可以自动映射的,但是为了保险(可能会修改字段名)和直观起见,还通过上面的映射文件来做吧

<sql id="Base_Column_List" >
    ID, VERSION, URL, FILENAME, MD5SUM, ANGEL_USER, UPDATE_TYPE, updateinfo, CREATETIME, UPDATETIME
  </sql>

这段xml的含义就是,相当于给一段sql语句定义了一个别名,你在其他地方可以使用,这也编程里面的一个重要思想,避免重复代码

<select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long" >
    select
    <include refid="Base_Column_List" />
    from a_update
    where ID = #{id,jdbcType=DECIMAL}
  </select>

这段xml我们要重点说一下,这里面包含了很多信息。
1.id 这个sql语句在这个命名空间(namespace)的唯一标识,这个id值对应于UserDao的一个方法名
2.** parameterType** 将会传入这条语句的参数类的完全限定名或别名,可以是java类,也可以是自定义的类
3.resultMap sql查询结果对应java映射,resultMap的值就是上面定义的resulMap元素的id值,resultMap元素可以有多个,当然id必须是唯一的
4.resultType 也是sql查询结果对应的java映射,是一个java的类,比如说可以返回一个java.util.Map,还比如你的select语句只返回一个Id,那么你就可以用resultType="java.lang.Long"来直接映射,而不用去定义一个resultMap,注意:resultType不能和resultMap同时使用
还有其他属性,就不一一说明了,感兴趣的,可以去看官方文档~

我们来看一下sql 语句中的一些信息

    <include refid="Base_Column_List" />

这个很好理解,就是把上面用<sql>元素定义的sql片段引入进来。

** where ID = #{id,jdbcType=DECIMAL}**这个是mybatis的核心所在!id的值是在使用的时候,动态传入的,这样sql语句才能复用。这就告诉 MyBatis 创建一个预处理语句参数,通过 JDBC,这样的一个参数在 SQL 中会由一个“?”来标识,并被传递到一个新的预处理语句中,如果用java代码的话,就像这样:

// Similar JDBC code, NOT MyBatis…
String selectPerson = "select *from a_update where ID = ?";
PreparedStatement ps = conn.prepareStatement(selectPerson);
ps.setInt(1,id);

这部分我们一会会再单独讲到,就是mybatis中强大的动态SQL。上面配置文件中其他insert、update、delete和select的配置差不多,我们就不一一展开了。他们不一样的地方就是sql语句中的动态sql使用,我着重说一下

动态sql

MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其他类似框架的经验,你就能体会到根据不同条件拼接 SQL 语句有多么痛苦。拼接的时候要确保不能忘了必要的空格,还要注意省掉列名列表最后的逗号。利用动态 SQL 这一特性可以彻底摆脱这种痛苦。

通常使用动态 SQL 不可能是独立的一部分,MyBatis 当然使用一种强大的动态 SQL 语言来改进这种情形,这种语言可以被用在任意的 SQL 映射语句中。

动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。在 MyBatis 之前的版本中,有很多的元素需要来了解。MyBatis 3 大大提升了它们,现在用不到原先一半的元素就可以了。MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。

  • if
  • choose (when, otherwise)
  • trim (where, set)
  • foreach
    这部分官方文档说的很详细了,请移步官方文档去看吧动态sql

事务

事务是数据库操作的重要部分,mybatis-spring 会让 mybatis 参入到 spring 的事务管理中。MyBatis-Spring 利用了存在于 Spring 中的 DataSourceTransactionManager来给mybatis 加上事务特性。
一旦 Spring 的 PlatformTransactionManager 配置好了,你可以在 Spring 中以你通常的做 法来配置事务。@Transactional 注解和 AOP(Aspect-Oriented Program,面向切面编程,译 者注)样式的配置都是支持的。在事务处理期间,一个单独的 SqlSession 对象将会被创建 和使用。当事务完成时,这个 session 会以合适的方式提交或回滚。
一旦事务创建之后,MyBatis-Spring 将会透明的管理事务。在你的 DAO 类中就不需要额 外的代码了
要 开 启 Spring 的 事 务 处 理 , 在 Spring 的 XML 配 置 文 件 中 简 单 创 建 一 个 DataSourceTransactionManager 对象:

<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <property name="dataSource" ref="dataSource" />
</bean>

关于dataSource我们前面已经讲过了。但是要注意的是,为事务管理器指定的 DataSource 必须和用来创建 SqlSessionFactoryBean 的 是同一个数据源,否则事务管理器就无法工作了。
配置了DataSourceTransactionManager,就可以配置事务作用在那些类和方法上了。

<!-- 拦截器方式配置事物 -->
    <tx:advice id="transactionAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="add*"  propagation="REQUIRED" />
            <tx:method name="insert*"  propagation="REQUIRED" />
            <tx:method name="get*"  propagation="SUPPORTS" />
            <tx:method name="find*"  propagation="SUPPORTS" />
        </tx:attributes>
    </tx:advice>
    <aop:config>
        <aop:pointcut id="transactionPointcut" expression="execution(* cn.test.manage.*.*(..))" />
        <aop:advisor pointcut-ref="transactionPointcut" advice-ref="transactionAdvice" />
    </aop:config>

上面的配置就表示事务作用于 cn.test.manage 包下的所有的类会在事务管理下,同时transactionAdvice配置了那些方法受事务管理

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

推荐阅读更多精彩内容