如何在SpringBoot项目中使用JPA

Spring Data JPA 是 Spring 基于 ORM 框架、JPA 规范的基础上封装的一套 JPA 应用框架,底层使用了 Hibernate 的 JPA 技术实现,可使开发者用极简的代码即可实现对数据的访问和操作。它提供了包括增删改查等在内的常用功能,且易于扩展!学习并使用SpringDataJPA可以极大提高开发效率!让我们解脱DAO层的操作,基本上所有CRUD都可以依赖于它来实现

本文将介绍如何在SpringBoot中的使用案例和原理分析

一、SpringBoot使用

1、导入依赖

swagger和common为附加包,不使用的话可以不需要导的哈

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <scope>runtime</scope>
</dependency>
<dependency>
   <groupId>cn.gjing</groupId>
   <artifactId>tools-starter-swagger</artifactId>
   <version>1.3.0</version>
</dependency>
<dependency>
   <groupId>cn.gjing</groupId>
   <artifactId>tools-common</artifactId>
   <version>1.2.5</version>
</dependency>

2、配置文件

server:
  port: 8082
spring:
  application:
    name: jpa-demo
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/demo?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2b8
    password: root
    username: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimum-idle: 1
      maximum-pool-size: 5
      idle-timeout: 30000
      connection-timeout: 20000
  jpa:
    # 是否打印sql
    show-sql: true
    hibernate:
      # 开启自动建表功能,一般选update,每次启动会对比实体和数据表结构是否相同,不相同会更新
      ddl-auto: update
    # 设置创表引擎为Innodb,不然默认为MyiSam
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect

3、创建实体类

/**
 * @author Gjing
 **/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "jpa_user")
@EntityListeners(AuditingEntityListener.class)
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "user_name", columnDefinition = "varchar(10) not null comment '用户名'")
    @ApiModelProperty(name = "userName",value = "用户名")
    private String userName;

    @Column(name = "user_age", columnDefinition = "int not null comment '年龄'")
    @ApiModelProperty(name = "userAge",value = "年龄")
    private Integer userAge;

    @Column(name = "user_phone", columnDefinition = "varchar(11) not null comment '手机号'")
    @ApiModelProperty(name = "userPhone", value = "手机号")
    private String userPhone;

    @Column(name = "create_time", columnDefinition = "datetime")
    @CreatedDate
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")
    private Date createTime;

    @Column(name = "update_time", columnDefinition = "datetime")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @LastModifiedDate
    private Date updateTime;
}

4、定义Repository层

    /**
     * 通过手机号查询
     * @param userPhone 手机号
     * @return user
     */
    User findByUserPhone(String userPhone);
    /**
     * 根据用户名分页查询
     * @param userName 用户名
     * @param pageable 分页对象
     * @return page
     */
    Page<User> findByUserName(String userName, Pageable pageable);

    /**
     * 根据用户id更新用户
     * @param userId 用户id
     * @param userName 用户名
     */
    @Query("update User u set u.userName = ?1 where u.id = ?2")
    @Modifying
    @Transactional(rollbackFor = Exception.class)
    void updateById(String userName, Long userId);

    /**
     * 查询指定用户
     * @param userPhone 用户号码
     * @return user
     */
    @Query("select u from User u where u.userPhone = ?1")
    User findUserByUserPhone(String userPhone);

5、定义service层

/**
 * @author Gjing
 **/
@Service
public class UserService {
    @Resource
    private UserRepository userRepository;

    /**
     * 保存用户
     *
     * @param userDto 用户传输对象
     */
    public void saveUser(UserDto userDto) {
        User userDb = userRepository.findByUserPhone(userDto.getUserPhone());
        if (userDb != null) {
            throw new ServiceException("用户已存在");
        }
        User user = userRepository.saveAndFlush(User.builder().userName(userDto.getUserName())
                .userAge(userDto.getUserAge())
                .userPhone(userDto.getUserPhone())
                .build());
    }

    /**
     * 分页查询用户列表
     *
     * @param pageable 分页条件
     * @return PageResult
     */
    public PageResult<List<User>> listForUser(Pageable pageable) {
        Page<User> userPage = userRepository.findAll(pageable);
        return PageResult.of(userPage.getContent(), userPage.getTotalPages());
    }

    /**
     * 删除用户
     *
     * @param userId 用户id
     */
    public void deleteUser(Long userId) {
        userRepository.findById(userId).ifPresent(u -> userRepository.delete(u));
    }

    /**
     * 更新用户
     *
     * @param userName 用户名
     * @param userId   用户id
     */
    public void updateUser(String userName, Long userId) {
        userRepository.updateById(userName, userId);
    }

    /**
     * 根据手机号查询
     *
     * @param userPhone 手机号
     * @return user
     */
    public User findByUserPhone(String userPhone) {
        return userRepository.findUserByUserPhone(userPhone);
    }

    /**
     * 动态查询
     *
     * @param age      岁数
     * @param userName 用户名
     * @return list
     */
    public List<User> dynamicFind(Integer age, String userName) {
        Specification<User> specification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicateList = new ArrayList<>();
            if (ParamUtils.isNotEmpty(age)) {
                predicateList.add(criteriaBuilder.equal(root.get("userAge"), age));
            }
            if (ParamUtils.isNotEmpty(userName)) {
                predicateList.add(criteriaBuilder.equal(root.get("userName"), userName));
            }
            return criteriaBuilder.and(predicateList.toArray(new Predicate[0]));
        };
        return userRepository.findAll(specification);
    }

6、定义接口

/**
 * @author Gjing
 **/
@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    @PostMapping("/user")
    @ApiOperation(value = "增加用户", httpMethod = "POST")
    @NotNull
    public ResponseEntity saveUser(@RequestBody UserDto userDto) {
        userService.saveUser(userDto);
        return ResponseEntity.ok("添加成功");
    }

    @DeleteMapping("/user/{user_id}")
    @ApiOperation(value = "删除用户", httpMethod = "DELETE")
    public ResponseEntity deleteUser(@PathVariable("user_id") Long userId) {
        userService.deleteUser(userId);
        return ResponseEntity.ok("删除成功");
    }

    @GetMapping("/user_page")
    @ApiOperation(value = "分页查询用户列表", httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "page", value = "页数(0开始)", required = true, dataType = "int", paramType = "query", defaultValue = "0"),
            @ApiImplicitParam(name = "size", value = "条数", required = true, dataType = "int", paramType = "query", defaultValue = "5")
    })
    @NotNull
    public ResponseEntity<PageResult<List<User>>> listForUser(Integer page, Integer size) {
        return ResponseEntity.ok(userService.listForUser(PageRequest.of(page, size, Sort.Direction.DESC,"id")));
    }

    @GetMapping("/user/{user_phone}")
    @ApiOperation(value = "根据手机号查询", httpMethod = "GET")
    public ResponseEntity<User> findUser(@PathVariable("user_phone") String userPhone) {
        return ResponseEntity.ok(userService.findByUserPhone(userPhone));
    }

    @PutMapping("/user")
    @ApiOperation(value = "更新用户信息", httpMethod = "PUT")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "long", paramType = "Query"),
            @ApiImplicitParam(name = "userName", value = "用户名", required = true, dataType = "String", paramType = "Query")
    })
    @NotNull
    public ResponseEntity updateUser(Long userId, String userName) {
        userService.updateUser(userName, userId);
        return ResponseEntity.ok("更新成功");
    }

    @GetMapping("/user_list")
    @ApiOperation(value = "动态查询用户", httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "userAge", value = "用户年龄", dataType = "int", paramType = "Query"),
            @ApiImplicitParam(name = "userName", value = "用户名", dataType = "String", paramType = "Query")
    })
    public ResponseEntity<List<User>> userList(Integer userAge, String userName) {
        return ResponseEntity.ok(userService.dynamicFind(userAge, userName));
    }
}

这样启动后就能对数据进行增删查的功能了,其中我们dao层的方法并没有定义过多方法,因为JPA内部已经帮我们提供了基本的CRUD功能

二、原理分析

为啥我们可以使用一些我们并没有定义的功能呢,接下来就一起一探究竟吧

1、执行过程

我们启动之前的案例,对其中任意一个方法打个端点,然后执行的时候


debug.jpg

会发现我们自己定义的userRepository被注入了一个代理类,也就是jpaRepository的实现类simpleJpaRepository,继续debug,发现,当进入使用dao层方法的时候,会进入到这个代理类,然后经过一些拦截器,最终进入到QueryExecutorMethodInterceptor.doInvoke这个方法中,这个拦截器主要做的事情就是判断方法类型,然后执行对应的操作,我们定义的findByUserPhone属于自定义查询,继续debug会发现进入了getExecution()获取查询策略方法,在执行execute()会先选择一个对应的策略,

    protected JpaQueryExecution getExecution() {
        if (this.method.isStreamQuery()) {
            return new StreamExecution();
        } else if (this.method.isProcedureQuery()) {
            return new ProcedureExecution();
        } else if (this.method.isCollectionQuery()) {
            return new CollectionExecution();
        } else if (this.method.isSliceQuery()) {
            return new SlicedExecution(this.method.getParameters());
        } else if (this.method.isPageQuery()) {
            return new PagedExecution(this.method.getParameters());
        } else {
            return (JpaQueryExecution)(this.method.isModifyingQuery() ? new ModifyingExecution(this.method, this.em) : new SingleEntityExecution());
        }
    }

如上述代码所示,根据method变量实例化时的查询设置方式,实例化不同的JpaQueryExecution子类实例去运行。我们的findByUserPhone最终落入了SingleEntityExecution返回单个实例的Execution, 继续debug会发现,进入了createQuery()方法,正是在这个方法里进行了Sql的拼装。
仔细观看这个类的代码时,会发现在构造方法中,有JpaQueryMethod类,这其实就是接口中带有@Query注解方法的全部信息,包括注解,类名,实参等的存储类。前面提到的QueryExecutorMethodInterceptor类,里面出现了一个private final Map<Method, RepositoryQuery> queries;,查看RepositoryQuery会发现里面有个QueryMethod,由此可以得出,一个RepositoryQuery代表了Repository接口中的一个方法,根据方法头上注解不同的形态,将每个Repository接口中的方法分别映射成相对应的RepositoryQuery实例。
实例所有类型

  • NamedQuery:使用javax.persistence.NamedQuery注解访问数据库的形式,内部就会根据此注解选择创建一个NamedQuery实例;
  • NativeJpaQuery:方法头上@Query注解的nativeQuery属性如果显式的设置为true,也就是使用原生SQL,此时就会创建NativeJpaQuery实例;
  • PartTreeJpaQuery:方法头上未进行@Query注解,就会使用JPA识别的方式进行sql语句拼接,此时内部就会创建一个PartTreeJpaQuery实例;
  • SimpleJpaQuery:方法头上@Query注解的nativeQuery属性缺省值为false,也就是使用JPQL,此时会创建SimpleJpaQuery实例;
  • StoredProcedureJpaQuery:在Repository接口的方法头上使用org.springframework.data.jpa.repository.query.Procedure注解,也就是调用存储过程的方式访问数据库,此时在jpa内部就会根据@Procedure注解而选择创建一个StoredProcedureJpaQuery实例;

2、启动流程

在启动的时候会实例化一个Repositories,它会去扫描所有的class,然后找出由我们定义的、继承自org.springframework.data.repository.Repositor的接口,然后遍历这些接口,针对每个接口依次创建如下几个实例:

  • SimpleJpaRepository:进行默认的 DAO 操作,是所有 Repository 的默认实现;
  • JpaRepositoryFactoryBean:装配 bean,装载了动态代理Proxy,会以对应的DAO的beanName为key注册到DefaultListableBeanFactory中,在需要被注入的时候从这个bean中取出对应的动态代理Proxy注入给DAO;
  • JdkDynamicAopProxy:动态代理对应的InvocationHandler,负责拦截DAO接口的所有的方法调用,然后做相应处理,比如findByUserPhone()被调用的时候会先经过这个类的invoke方法;

JpaRepositoryFactoryBean.getRepository()方法被调用的过程中,还是在实例化QueryExecutorMethodInterceptor这个拦截器的时候,spring 会去为我们的方法创建一个PartTreeJpaQuery,在它的构造方法中同时会实例化一个PartTree对象。PartTree定义了一系列的正则表达式,全部用于截取方法名,通过方法名来分解查询的条件,排序方式,查询结果等等,这个分解的步骤是在进程启动时加载 Bean 的过程中进行的,当执行查询的时候直接取方法对应的PartTree用来进行sql的拼装,然后进行DB的查询,返回结果。

简要概括就是

在启动的时候扫描所有继承自 Repository 接口的 DAO 接口,然后为其实例化一个动态代理,同时根据它的方法名、参数等为其装配一系列DB操作组件,在需要注入的时候为对应的接口注入这个动态代理,在 DAO 方法被调用的时会走这个动态代理,然后经过一系列的方法拦截路由到最终的 DB 操作执行器JpaQueryExecution,然后拼装 sql,执行相关操作,返回结果。

三、JPA相关知识点

1、基本查询

JPA查询主要有两种方式,继承JpaRepository后使用默认提供的,也可以自己自定义。以下列举了一部分

  • 默认的
    List<T> findAll();

    List<T> findAll(Sort var1);

    List<T> findAllById(Iterable<ID> var1);

    <S extends T> List<S> saveAll(Iterable<S> var1);

    void flush();

    <S extends T> S saveAndFlush(S var1);

    void deleteInBatch(Iterable<T> var1);

    void deleteAllInBatch();

    T getOne(ID var1);

    <S extends T> List<S> findAll(Example<S> var1);

    <S extends T> List<S> findAll(Example<S> var1, Sort var2);
  • 自定义
    User findByUserPhone(String userPhone);

自定义的关键字如下

关键字 例子 JPQL
And findByLastnameAndFirstname … where x.lastname = ?1 and x.firstname = ?2
Or findByLastnameOrFirstname … where x.lastname = ?1 or x.firstname = ?2
Is,Equals findByFirstnameIs,findByFirstnameEquals … where x.firstname = ?1
Between findByStartDateBetween … where x.startDate between ?1 and ?2
LessThan findByAgeLessThan … where x.age < ?1
LessThanEqual findByAgeLessThanEqual … where x.age ⇐ ?1
GreaterThan findByAgeGreaterThan … where x.age > ?1
GreaterThanEqual findByAgeGreaterThanEqual … where x.age >= ?1
After findByStartDateAfter … where x.startDate > ?1
Before findByStartDateBefore … where x.startDate < ?1
IsNull findByAgeIsNull … where x.age is null
IsNotNull,NotNull findByAge(Is)NotNull … where x.age not null
Like findByFirstnameLike … where x.firstname like ?1
NotLike findByFirstnameNotLike … where x.firstname not like ?1
StartingWith findByFirstnameStartingWith … where x.firstname like ?1 (parameter bound with appended %)
EndingWith findByFirstnameEndingWith … where x.firstname like ?1 (parameter bound with prepended %)
Containing findByFirstnameContaining … where x.firstname like ?1 (parameter bound wrapped in %)
OrderBy findByAgeOrderByLastnameDesc … where x.age = ?1 order by x.lastname desc
Not findByLastnameNot … where x.lastname <> ?1
In findByAgeIn(Collection ages) … where x.age in ?1
NotIn findByAgeNotIn(Collection age) … where x.age not in ?1
TRUE findByActiveTrue() … where x.active = true
FALSE findByActiveFalse() … where x.active = false
IgnoreCase findByFirstnameIgnoreCase … where UPPER(x.firstame) = UPPER(?1)

2、分页查询

分页查询在实际使用中非常普遍了,jpa已经帮我们实现了分页的功能,在查询的方法中,需要传入参数Pageable,当查询中有多个参数的时候Pageable建议做为最后一个参数传入。Pageable是Spring封装的分页实现类,使用的时候需要传入页数、每页条数和排序规则,以之前编写的为例:

/**
 * @author Gjing
 **/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
    /**
     * 分页查询
     * @param pageable 分页对象
     * @return Page<User>
     */
    @Override
    Page<User> findAll(Pageable pageable);

    /**
     * 根据用户名分页查询
     * @param userName 用户名
     * @param pageable 分页对象
     * @return page
     */
    Page<User> findByUserName(String userName, Pageable pageable);
}

接口

    @GetMapping("/list")
    @ApiOperation(value = "分页查询用户列表", httpMethod = "GET")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "page", value = "页数(0开始)", required = true, dataType = "int", paramType = "query", defaultValue = "0"),
            @ApiImplicitParam(name = "size", value = "条数", required = true, dataType = "int", paramType = "query", defaultValue = "5")
    })
    @NotNull
    public ResponseEntity<PageResult<List<User>>> listForUser(Integer page, Integer size) {
        return ResponseEntity.ok(userService.listForUser(PageRequest.of(page, size, Sort.Direction.DESC,"id")));
    }

由上可看出,我们使用了PageRequest.of()构建了pageable对象,并指定了按id进行排序

3、自定义SQL

大部分的SQL都可以根据方法名定义的方式来实现,但是由于某些原因我们想使用自定义的SQL来查询,JPA也是完美支持的;在SQL的查询方法上面使用@Query注解,如涉及到删除和修改在需要加上@Modifying。也可以根据需要添加@Transactional对事务的支持,查询超时的设置等

/**
 * @author Gjing
 **/
@Repository
public interface UserRepository extends JpaRepository<User,Long> {
    /**
     * 通过手机号查询
     * @param userPhone 手机号
     * @return user
     */
    User findByUserPhone(String userPhone);

    /**
     * 分页查询
     * @param pageable 分页对象
     * @return Page<User>
     */
    @Override
    Page<User> findAll(Pageable pageable);

    /**
     * 根据用户名分页查询
     * @param userName 用户名
     * @param pageable 分页对象
     * @return page
     */
    Page<User> findByUserName(String userName, Pageable pageable);

    /**
     * 根据用户id更新用户
     * @param userId 用户id
     * @param userName 用户名
     * @return int
     */
    @Query("update User u set u.userName = ?1 where u.id = ?2")
    @Modifying
    @Transactional(rollbackFor = Exception.class)
    int updateById(String userName, Long userId);

    /**
     * 查询指定用户
     * @param userPhone 用户号码
     * @return user
     */
    @Query("select u from User u where u.userPhone = ?1")
    User findUserByUserPhone(String userPhone);
}

4、动态查询

JPA极大的帮助了我们更方便的操作数据库,但是,在实际场景中,往往会碰到复杂查询的场景,前端会动态传一些参数请求接口,这时候就需要使用到动态查询了。
首先需要在继承一个接口JpaSpecificationExecutor,需要传入一个泛型,填写你的具体实体对象即可,接下来在service层实现一个动态的查询方法

    /**
     * 动态查询
     * @param age 岁数
     * @param userName 用户名
     * @return list
     */
    public List<User> dynamicFind(Integer age, String userName) {
        Specification<User> specification = (Specification<User>) (root, criteriaQuery, criteriaBuilder) -> {
            List<Predicate> predicateList = new ArrayList<>();
            if (ParamUtil.isNotEmpty(age)) {
                predicateList.add(criteriaBuilder.equal(root.get("userAge"), age));
            }
            if (ParamUtil.isNotEmpty(userName)) {
                predicateList.add(criteriaBuilder.equal(root.get("userName"), userName));
            }
            return criteriaBuilder.and(predicateList.toArray(new Predicate[0]));
        };
        return userRepository.findAll(specification);
    }

这里定义了根据年龄和名称动态查询用户列表,这里只举了个栗子,更多玩法需要自己去拓展研究

5、相关注解

注解 作用
@Entity 表明这是个实体bean
@Table 指定实体对应的数据表,里面可配置数据表名和索引,该索引可以不使用,默认表名为实体类名
@Column 字段,可设置字段的相关属性
@Id 指明这个字段为id
@GeneratedValue ID生成策略
@Transient 指定该字段不用映射到数据库
@Temporal 指定时间精度
@JoinColumn 一对一:本表中指向另一个表的外键。一对多:另一个表指向本表的外键
@OneToOne、@OneToMany、@ManyToOne 对应hibernate配置文件中的一对一,一对多,多对一
@Modifying 搭配@Query使用,查询操作除外
@Query 自定义SQL注解,默认JPQL,如果要使用原生sql,可以指定nativeQuery==true

6、进阶用法

1、返回指定的VO

很多时候,我们有些字段并不需要,所有我们可以定义一个VO去接JPA查询回来的结果

定义个VO

/**
 * @author Gjing
 **/
@Getter
@Setter
@ToString
@AllArgsConstructor
public class UserVO {
    private Long id;
    private String userName;
    private Integer userAge;
    private String userPhone;
}

在Repository接口增加一个接口

/**
 * @author Gjing
 **/
@Repository
public interface UserRepository extends JpaRepository<User,Long> , JpaSpecificationExecutor<User> {
    /**
     * 分页查询用户
     * @param pageable 分页条件
     * @return Page<UserVO>
     */
    @Query("select new com.gj.domain.vo.UserVO(u.id,u.userName,u.userAge,u.userPhone) from User as u")
    Page<UserVO> findAllUser(Pageable pageable);
}

这里可以看到,我们采用了自定义HQL,语句中我们new了一个我们定义的VO去获取结果,这里有个注意的地方就是,你的VO一定要有构造方法与之对应,这样我们就能返回个VO啦,pageable参数JPA也会帮我们自动去分页查询,是不是很方便呢,那就快用起来吧

7、知识点扩展

1、如果想在往数据库插入数据时候,自动带上添加时间可以在对应字段标注@CreatedDate,想在更新的时候自动添加更新时间可以在对应字段使用@LastModifiedDate,另外必须在启动类使用@EnableJpaAuditing注解以及在对应的实体类使用@EntityListeners(AuditingEntityListener.class),否则无效
2、在@Column中定义字段的默认值,在默认情况下JPA是不会进行默认插值的,这时候,可以在实体类上加个注解@DynamicInsert


本文到此结束啦,篇幅比较长,如果哪里写的有误可以在评论区留言哈,也希望各位可以关注我哦,我会持续发布更多新的文章。本Demo的源代码地址:SprignBoot-Demo

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