Mybatis(三)

三 使用XML配置SQL映射器(映射文件)
关系型数据库和SQL是经受时间考验和验证的数据存储机制。和其他的ORM 框架如Hibernate不同,【MyBatis鼓励】开发者可以直接【使用数据库】,而不是将其对开发者隐藏,因为这样可以充分发挥数据库服务器所提供的SQL语句的巨大威力。
与此同时,MyBaits【消除】了书写大量【冗余代码】的痛苦,它让使用SQL更容易。在代码里直接嵌套
很差的编码实践,并且维护起来困难。MyBaits使用了映射文件或注解来配置SQL语句。

3.1 映射器文件和映射器接口
我们已经看见了一些在映射器配置文件中配置基本的映射语句,以及怎样使用SqlSession对象调用它们的例子。现在让我们看一下在com.briup.mappers包中的StudentMapper.xml 配置文件内,是如何配置id为”findStudentById”的SQL语句的,代码如下:
<?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="com.briup.mappers.StudentMapper">
<select id="findStudentById" parameterType="int" resultType="Student">
select stud_id as studId, name, email, dob
from students where stud_id=#{studId}
</select>
</mapper>

我们可以通过下列代码调用findStudentById映射的SQL语句:
public Student findStudentById(Integer studId) {
SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();
try{
Student student = sqlSession.selectOne("com.briup.mappers.StudentMapper.findStudentById", studId);
return student;
}
finally {
sql Session.close();
}
}

调用映射方法的方式一:
我们可以通过字符串(字符串形式为:映射器配置文件所在的包名的【namespace + sql语句id值】,如上,即包名com.briup.mappers.StudentMapper和语句id的值findStudentById组成)调用映射的SQL语句。
但是这种方式【容易出错】,我们【不推荐】使用。你需要检查映射器配置文件中的定义,以保证你的输入参数类型和结果返回类型是有效的。

调用映射方法的方式二:
也就是我们一直在使用的方式,通过session获取mapper对象,然后由mapper对象直接调用映射方法实现功能。
【重点部分:】
MyBatis通过使用映射器Mapper接口提供了更好的调用映射语句的方法。一旦我们通过映射器配置文件配置了映射语句,我们可以创建一个完全对应的一个映射器接口,xml映射文件中的【namespace属性值】和映射接口的【全限定名】需要保持【一致】。
映射器接口中的【方法名】也跟映射器配置文件中【id值】完全对应;
映射方法的【参数类型】和【parameterType属性值】对应;
映射方法【返回值类型】和【returnType属性值】一致。

上述的StudentMapper.xml文件,我们可以创建一个映射器接口StudentMapper.java如下:
     
    package com.briup.mappers; 
    public interface StudentMapper{ 
        Student findStudentById(Integer id); 
    } 
在Student Mapper.xml映射器配置文件中,其名空间namespace应该跟StudentMapper接口的全限定名保持一致。另外,StudentMapper.xml中语句id, parameterType,returnType 应该分别和StudentMapper接口中的方法名,参数类型,返回值相对应。

使用映射器接口我们可以以类型安全的形式调用调用映射语句。如下所示:
     
    public Student findStudentById(Integer studId){ 
        SqlSession sqlSession = MyBatisSqlSessionFactory.openSession();  
        try { 
            StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); 

            return student Mapper.findStudentById(studId); 
        } 
        finally { 
            sqlSession.close(); 
        } 
    } 

结论:
namespace关键字的用法
情形1: 任何值都可以
使用session对象执行sql语句;

情形2:  必须映射接口的全包名
    使用映射接口对象 执行sql语句。
    
推荐第2种。

3.2 映射语句
MyBatis提供了多种元素来配置不同类型的语句,如SELECT,INSERT,UPDATE,DELETE。让我们看看如何具体配置映射语句

3.2.1 INSERT 插入语句
一个INSERT语句可以在<insert>标签元素在映射器XML配置文件中配置,如下所示:
<insert id="insertStudent" parameterType="Student">
INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE) VALUES(#{studId},#{name},#{email},#{phone})
</insert>
这里我们设置一个ID属性为insertStudent,可以在命名空间 com.briup.mappers.StudentMapper.insertStudent中唯一标识该sql语句。
parameterType 属性是一个完全限定类名或者是一个类型别名(alias)。

我们可以如下调用这个语句:
    int count =  sqlSession.insert("com.briup.mappers.StudentMapper.insertStudent", student); 
    sqlSession.insert() 方法返回执行 INSERT 语句后所影响的行数。

如果不使用命名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器Mapper 接口,并以类型安全的方式调用方法,如下所示:
    package com.briup.mappers; 
    public interface StudentMapper{ 
        int insertStudent(Student student); 
    } 
你可以如下调用insertStudent映射语句:
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    int count = mapper.insertStudent(student); 

【自动生成主键】
情形1:
数据库默认支持自动生成主键列的值,例如mysql。

在上述的INSERT语句中,我们为可以自动生成(auto-generated)主键的列 STUD_ID 插入值。我们可以使用useGeneratedKeys和keyProperty属性让数据库生成auto_increment列的值,并将生成的值设置到其中一个输入对象属性内,如下所示:
 
    <insert id="insertStudent" parameterType="Student" useGeneratedKeys="true" keyProperty="studId"> 
        INSERT INTO STUDENTS(NAME, EMAIL, PHONE) VALUES(#{name},#{email},#{phone}) 
    </insert> 
这里STUD_ID列值将会被数据库自动生成(如mysql),并且生成的值会被设置到student对象的studId属性上。

情形2:
数据库不支持自动生成主键列的值,例如oracle。
但是有些数据库如Oracle并不支持AUTO_INCREMENT列,其使用序列【SEQUENCE】来生成主键值。假设我们有一个名为my_seq的序列来生成SUTD_ID主键值。使用如下代码来生成主键:
drop sequence my_seq;
create sequence my_seq;

    <insert id="insertStudent" parameterType="Student"> 
        <selectKey keyProperty="studId" resultType="int" order="BEFORE"> 
            SELECT my_seq.nextval FROM DUAL 
        </selectKey> 
        INSERT INTO STUDENTS(STUD_ID,NAME,EMAIL, PHONE) 
            VALUES(#{studId},#{name},#{email},#{phone}) 
    </insert> 
这里我们使用了<selectKey>子元素来生成主键值,并将值保存到Student对象的studId 属性上。属性order=“before”表示MyBatis将取得序列的下一个值作为主键值,并且在执行INSERT语句之前将值设置到studId属性上。

【注意】
    SelectKey需要注意order属性,像MySQL、SQLServer等一类支持自动增长类型的数据库中,order需要设置为after才会取到正确的值。
    像Oracle这样取序列的情况,需要设置为before,否则会报错。

3.2.2 UPDATE 更新语句
一个UPDATE SQL语句可以在<update>元素在映射器XML配置文件中配置,如下所示:
<update id="updateStudent" parameterType="Student">
UPDATE STUDENTS SET NAME=#{name}, EMAIL=#{email}, PHONE=#{phone}
WHERE STUD_ID=#{studId}
</update>

我们可以如下调用此语句:
    int noOfRowsUpdated = sqlSession.update("com.briup.mappers.StudentMapper.updateStudent", student); 
    sqlSession.update()方法返回执行UPDATE语句之后影响的行数。

如果不使用名空间(namespace)和语句id来调用映射语句,你可以通过创建一个映射器Mapper接口,并以类型安全的方式调用方法,如下所示:
     
    package com.briup.mappers; 
    public interface StudentMapper{ 
        int updateStudent(Student student); 
    } 

你可以使用映射器Mapper接口来调用updateStudent语句,如下所示:
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    int noOfRowsUpdated = mapper.updateStudent(student); 

3.2.3 DELETE 删除语句
一个UPDATE SQL语句可以在<update>元素在映射器XML配置文件中配置,如下所示
<delete id="deleteStudent" parameterType="int">
DELETE FROM STUDENTS WHERE STUD_ID=#{id}
</delete>

我们可以如下调用此语句:
    int studId = 1; 
    //          delete("命名空间名.方法名",参数);
    int noOfRowsDeleted = sqlSession.delete("com.briup.mappers.StudentMapper.deleteStudent", studId); 
    sqlSession.delete()方法返回 delete 语句执行后影响的行数。

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
    package com.briup.mappers; 
    public interface StudentMapper{ 
      int deleteStudent(int studId); 
    } 

你可以使用映射器Mapper接口来调用deleteStudent语句,如下所示:
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    int noOfRowsDeleted = mapper.deleteStudent(studId);

3.2.4 SELECT 查询语句
MyBatis真正【强大】的功能,在于【映射SELECT】查询结果到java的【各种类型】。
让我们看看一个简单的select查询是如何(在MyBatis中)配置的,如下所示:
<select id="findStudentById" parameterType="int"
resultType="Student">
SELECT STUD_ID, NAME, EMAIL, PHONE
FROM STUDENTS
WHERE STUD_ID=#{studId}
</select>
我们可以如下调用此语句:
int studId = 1;
Student student = sqlSession.selectOne("com.briup.mappers. StudentMapper.findStudentById", studId);

如果不使用名空间(namespace)和语句 id 来调用映射语句,你可以通过创建一个映射器 Mapper 接口,并以类型安全的方式调用方法,如下所示:
    package com.briup.mappers; 
    public interface StudentMapper{ 
        Student findStudentById(Integer studId); 
    }
你可以使用映射器Mapper接口来调用 findStudentById 语句,如下所示:
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    Student student = mapper.findStudentById(studId); 

如果你检查Student对象的属性值,你会发现studId属性值并没有被stud_id列值填充。这是因为MyBatis自动对java对象中和列名匹配的属性进行填充。这就是为什么name,email和 phone属性被填充而studId属性没有被填充。
解决这一问题,我们可以为列名起一个可以与JavaBean中属性名匹配的别名,如下所示:
    <select id="findStudentById" parameterType="int"  
    resultType="Student"> 
        SELECT STUD_ID AS studId, NAME,EMAIL, PHONE  
            FROM STUDENTS  
        WHERE STUD_ID=#{studId} 
    </select> 

    
MyBatis执行返回多条结果的SELECT语句查询,如下所示:
    <select id="findAllStudents" resultType="Student"> 
        SELECT STUD_ID AS studId, NAME, EMAIL, PHONE  
        FROM STUDENTS 
    </select>

    List<Student> students =  
    sqlSession.selectList("com.briup.mappers.StudentMapper.findAllStudents"); 

映射器 Mapper 接口 StudentMapper 可以如下定义:
    package com.briup.mappers; 
    public interface StudentMapper{ 
        List<Student> findAllStudents(); 
    } 

使用上述代码,我们可以如下调用
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    List<Student> students = mapper.findAllStudents(); 

如果你注意到上述的SELECT映射定义,你可以看到,我们为所有的映射语句中的stud_id 起了别名。我们可以使用ResultMaps,来避免上述的到处重复别名。我们稍后会继续讨论。

除了java.util.List,你也可以使用其他类型的集合类,如Set,Map,以及(SortedSet)。MyBatis 根据集合的类型,会采用适当的集合实现,如下所示:
    对于List,Collection,Iterable类型,MyBatis将返回java.util.ArrayList 
    对于Map类型,MyBatis 将返回java.util.HashMap  
    对于Set类型,MyBatis 将返回java.util.HashSet 
    对于SortedSet类型,MyBatis将返回java.util.TreeSet 

3.3 结果集映射 ResultMaps
ResultMaps被用来将SELECT语句的结果集映射到java对象的属性中。我们可以定义结果集映射ResultMaps并且在一些SELECT语句上引用resultMap。MyBatis的结果集映射 ResultMaps特性非常强大,你可以使用它将简单的SELECT语句映射到复杂的一对一、一对多关系的SELECT语句上。

3.3.1 简单ResultMap
一个映射了查询结果为Student类型的resultMap定义如下:
<resultMap id="StudentResult" type="com.briup.pojo.Student">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>

    <select id="findAllStudents" resultMap="StudentResult"> 
        SELECT * FROM STUDENTS 
    </select> 
    <select id="findStudentById" parameterType="int" resultMap="StudentResult"> 
        SELECT * FROM STUDENTS WHERE STUD_ID=#{studId} 
    </select> 

resultMap的id值应该在此名空间内是唯一的,并且type属性是完全限定类名或者是返回类型的别名。
<result>子元素被用来将一个resultset列映射到对象的一个属性中。
<id>元素和<result>元素功能相同,不过<id>它被用来映射到唯一标识属性,用来区分和比较对象(一般和主键列相对应)。
在<select>语句中,我们使用了resultMap属性,而不是resultType属性。当<select>语句中配置了resutlMap属性,MyBatis会使用表中的列名与对象属性 【映射关系】 来填充对象中的属性值。
    
【注意】:
    【resultType和resultMap】二者只能用其一,】不能同时使用】。

【问题升级1】:
<select>映射语句中如何将查询【一条】数据填充到Map中?
<select id="findStudentById" parameterType="int" resultType="map">
SELECT * FROM STUDENTS WHERE STUD_ID=#{studId}
</select>
在上述的<select>语句中,我们将resultType配置成【map,即java.util.HashMap】的别名。在这种情况下,结果集的列名将会作为Map中的key值,而列值将作为Map的value值。

    HashMap<String,Object> studentMap = sqlSession.selectOne("com.briup.mappers.StudentMapper.findStudentById", studId); 
    System.out.println("stud_id :"+studentMap.get("stud_id")); 
    System.out.println("name :"+studentMap.get("name")); 
    System.out.println("email :"+studentMap.get("email")); 
    System.out.println("phone :"+studentMap.get("phone"));

【问题升级2】:
<select>映射语句中如何将查询【多条】数据填充到Map中?
<select id="findAllStudents" resultType="map">
SELECT STUD_ID, NAME, EMAIL, PHONE FROM STUDENTS
</select>

由于resultType=”map”和语句返回多行,则最终返回的数据类型应该是List<Map<String,Object>>,如下所示:

    List<Map<String, Object>> studentMapList = sqlSession.selectList("com.briup.mappers.StudentMapper.findAllStudents"); 
    for(Map<String, Object> studentMap : studentMapList) { 
    System.out.println("studId :" + studentMap.get("stud_id")); 
        System.out.println("name :" + studentMap.get("name")); 
        System.out.println("email :" + studentMap.get("email")); 
        System.out.println("phone :" + studentMap.get("phone")); 
    } 


其他实例1:【结果为对象】
    <select id="findAllStudents_student" resultType="Student">
        SELECT STUD_ID AS STUDID,NAME,EMAIL,DOB
        FROM STUDENTS
    </select>
对应的接口中的方法,你写什么类型的集合,Mybatis就给你返回什么类型的集合,但是要注意使用SortedSet的时候,Student类需要实现Comparable接口,否则是不能进行排序的。
例如:
    public List<Student> findAllStudents_List();
    或者
    public Set<Student> findAllStudents_Set();
    或者
    public SortedSet<Student> findAllStudents_SortedSet();
    SortedSet是Set的子接口,TreeSet是其实现子类【需要实现比较接口才可以成功】。

其他实例2:【查询指定列】
    <select id="findAllName_list" resultType="String">
        SELECT NAME
        FROM STUDENTS
    </select>
    对应的接口中的方法: 把查询到所有名字都放到List集合中并返回
    public List<String> findAllName_list();

    
其他实例3:【查询组函数】
    <select id="findCount_int" resultType="int">
        SELECT count(*)
        FROM STUDENTS
    </select>
    对应的接口中的方法: 把查询到的这个值直接返回
    public int findCount_int();

3.3.2 拓展/继承 ResultMap 【后面知识点,暂不介绍】
(注:这个例子在下面的一对一映射的知识点中进行测试,因为这里需要建立一对一关系的表结构)
我们可以从从另外一个<resultMap>,【拓展】出一个新的<resultMap>,这样,原先的属性映射可以【继承】过来,以实现:
<resultMap type="Student" id="StudentResult">
<id property="studId" column="stud_id" />
<result property="name" column="name" />
<result property="email" column="email" />
<result property="phone" column="phone" />
</resultMap>

    <!-- Student类中又新增加了一个属性,该属性的类型是Address -->
    <!-- 自定义类Address,类中也有多个属性,同时数据库中ADDRESSES表与其对应 -->
    <resultMap type="Student" id="StudentWithAddressResult" extends="StudentResult"> 
        <result property="address.addrId" column="addr_id" /> 
        <result property="address.street" column="street" /> 
        <result property="address.city" column="city" /> 
        <result property="address.state" column="state" /> 
        <result property="address.zip" column="zip" /> 
        <result property="address.country" column="country" /> 
    </resultMap> 

    其中id为StudentWithAddressResult的resultMap拓展了id为StudentResult的resultMap

    如果你只想映射Student数据,你可以使用id为StudentResult的resultMap,如下所示:
     
    <select id="findStudentById" parameterType="int"  
    resultMap="StudentResult"> 
        SELECT * FROM STUDENTS WHERE STUD_ID=#{stud Id} 
    </select> 

    如果你想将映射Student数据和Address数据,你可以使用id为StudentWithAddressResult的 resultMap:
    <select id="selectStudentWithAddress" parameterType="int"  
    resultMap="StudentWithAddressResult"> 
        SELECT STUD_ID, NAME, EMAIL, PHONE, A.ADDR_ID, STREET, CITY, STATE, ZIP, COUNTRY 
        FROM STUDENTS S LEFT OUTER JOIN ADDRESSES A ON  
                S.ADDR_ID=A.ADDR_ID 
        WHERE STUD_ID=#{studId} 
    </select> 
    注:该sql语句使用了连接查询中的左外连接,也可以使用等值连接

3.4 一对一映射
Student和Address是一个【一对一】关系
建表语言:
drop table students;
drop table addresses;
如果需要可以使用 cascade constraints;

    create table addresses(
        addr_id number primary key,
        street varchar2(50) not null,
        city varchar2(50) not null,
        state varchar2(50) not null,
        zip varchar2(10),
        country varchar2(50)
    );

    create table students(
        stud_id number primary key,
        name varchar2(50) not null,
        email varchar2(50),
        phone varchar2(15),  
        dob date ,
        addr_id number references addresses(addr_id)
    );

java类:
    public class PhoneNumber {
        private String countryCode;
        private String stateCode;
        private String number;
        get/set
    }
    public class Address{
        private Integer addrId;
        private String street;
        private String city;
        private String state;
        private String zip;
        private String country;
        get/set
    }
    public class Student {
        private Integer studId; 
        private String name; 
        private String email; 
        private Date dob;
        private PhoneNumber phone;
        private Address address;
        get/set
    }


    addresses 表的样例输入如下所示:
    addr_id  street     city     state  zip   country 
        1    redSt      kunshan   W     12345  china 
        2    blueST     kunshan   W     12345  china 

    insert into addresses(addr_id,street,city,state,zip,country) values(1,'redSt','kunshan','W','12345','china');
    insert into addresses(addr_id,street,city,state,zip,country) values(2,'blueST','kunshan','W','12345','china');


    students 表的样例数据如下所示:
    stud_id  name    email          phone       addr_id 
       1    John  john@gmail.com  123-456-7890   1 
       2    Paul  paul@gmail.com  111-222-3333   2 
    
    insert into students(stud_id,name,email,phone,addr_id) values(1,'John','john@gmail.com','123-456-7890',1);
    insert into students(stud_id,name,email,phone,addr_id) values(2,'Paul','paul@gmail.com','111-222-3333',2);

【执行select操作】
mapper XML:

    <resultMap type="Student" id="StudentWithAddressResult"> 
        <id property="studId" column="stud_id" /> 
        <result property="name" column="name" /> 
        <result property="email" column="email" /> 
        <result property="dob" column="dob" /> 
        <result property="phone" column="phone" /> 
        <result property="address.addrId" column="addr_id" /> 
        <result property="address.street" column="street" /> 
        <result property="address.city" column="city" /> 
        <result property="address.state" column="state" /> 
        <result property="address.zip" column="zip" /> 
        <result property="address.country" column="country" /> 
    </resultMap>
    
    //【多表查询】实现功能
    <select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddressResult"> 
        select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country 
        from students s left outer join addresses a on  
            s.addr_id=a.addr_id 
        where stud_id=#{id} 
    </select>  
    //注意,有歧义的一定要有【别名】,两个表中都有【addr_id】

我们可以使用(【对象.属性名】)的方式为【内嵌的对象】的属性赋值。在上述的resultMap中,Student的address属性使用该方式被赋上了 address 对应列的值。同样地,我们可以访问【任意深度】的内嵌对象的属性。

//接口定义 
    public interface StudentMapper{ 
        Student selectStudentWithAddress(int studId); 
    } 

//方法调用
    int studId = 1; 
    StudentMapper studentMapper = sqlSession.getMapper(StudentMapper.class); 
    Student student = studentMapper.selectStudentWithAddress(studId); 
    System.out.println("Student :" + student); 
    System.out.println("Address :" + student.getAddress()); 

【执行insert操作】
1.映射文件:
<insert id="insertAddress" parameterType="Address">
<selectKey keyProperty="addrId" resultType="int" order="BEFORE">
select my_seq.nextval from dual
</selectKey>
insert into addresses(addr_id,street,city,state,zip,country)
values(#{addrId},#{street},#{city},#{state},#{zip},#{country})
</insert>

    <insert id="insertStudent" parameterType="Student">
        <selectKey keyProperty="studId" resultType="int" order="BEFORE">
            select my_seq.nextval from dual
        </selectKey>
        insert into students(stud_id,name,email,phone,dob,addr_id) 
        values(#{studId},#{name},#{email},#{phone},#{dob},#{address.addrId})
    </insert>
    
2.映射接口:
    public void insertAddress(Address addr);

    public void insertStudent(Student stu);

3.测试代码:
    One2OneMapper mapper = session.getMapper(One2OneMapper.class);
    Address addr = new Address("xyl", "ks", "sz", "js", "china");
    mapper.insertAddress(addr);
    
    Student stu = new Student("zs", "email", new Date(), new PhoneNumber("110-120-119"), addr);
    mapper.insertStudent(stu);
    
    session.commit();
        
上面展示了一对一关联映射的一种方法。然而,使用这种方式映射,如果address结果需要在其他的SELECT映射语句中映射成Address对象,我们需要为每一个语句重复这种映射关系。
MyBatis提供了更好地实现一对一关联映射的方法:【嵌套结果】ResultMap和【嵌套查询】select语句。接下来,我们将讨论这两种方式。

3.4.1 使用【嵌套结果ResultMap】实现一对一关系映射
我们可以使用一个嵌套结果ResultMap方式来获取Student及其Address信息。

映射文件 【映射接口和测试代码 使用之前的即可】
    <resultMap type="Address" id="AddressResult"> 
        <id property="addrId" column="addr_id" /> 
        <result property="street" column="street" /> 
        <result property="city" column="city" /> 
        <result property="state" column="state" /> 
        <result property="zip" column="zip" /> 
        <result property="country" column="country" /> 
    </resultMap> 
    
    <resultMap type="Student" id="StudentWithAddressResult"> 
        <id property="studId" column="stud_id" /> 
        <result property="name" column="name" /> 
        <result property="email" column="email" /> 
        <result property="dob" column="dob" /> 
        <result property="phone" column="phone" /> 
        <!-- 【关联】的意思 -->
        <association property="address" resultMap="AddressResult" /> 
    </resultMap>
    
    <select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddressResult"> 
        select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country 
        from students s left outer join addresses a 
        on s.addr_id=a.addr_id 
        where stud_id=#{studid}  
        <!-- 也可以不使用左外连接,而使用多表查询【等值连接】实现功能 -->
    </select>
    
    【等值连接】
        select stud_id, name, email, dob, phone, a.addr_id, street, city, state, zip, country 
        from students s, addresses a 
        where s.addr_id=a.addr_id 
        and stud_id=#{studid}
    
注:【association是关联】的意思
    元素<association>被用来导入“有一个”(has-one)类型的关联。在上述的例子中,我们使用了<association>元素引用了另外的在同一个XML文件中定义的<resultMap>。     
    
    
同时我们也可以使用<association> 定义【内联的resultMap】,代码如下所示:
    <resultMap type="Student" id="StudentWithAddressResult"> 
        <id property="studId" column="stud_id" /> 
        <result property="name" column="name" /> 
        <result property="email" column="email" /> 
        <result property="dob" column="dob" /> 
        <result property="phone" column="phone" /> 
        <association property="address" javaType="Address"> 
            <id property="addrId" column="addr_id" /> 
            <result property="street" column="street" /> 
            <result property="city" column="city" /> 
            <result property="state" column="state" /> 
            <result property="zip" column="zip" /> 
            <result property="country" column="country" /> 
        </association> 
    </resultMap> 

3.4.2 使用【嵌套查询select】实现一对一关系映射
嵌套查询本质:
一个select语句 转化成 多条select语句去实现功能。
我们可以通过使用嵌套select查询来获取Student及其Address信息,代码如下:
1.【先根据addr_id去查找 地址对象】
<resultMap type="Address" id="AddressResult">
<id property="addrId" column="addr_id" />
<result property="street" column="street" />
<result property="city" column="city" />
<result property="state" column="state" />
<result property="zip" column="zip" />
<result property="country" column="country" />
</resultMap>

    <select id="findAddressById" parameterType="int" resultMap="AddressResult"> 
        select * from addresses where addr_id=#{id} 
    </select> 

2.【根据学号 查询 学生对象】
    <resultMap type="Student" id="StudentWithAddress"> 
        <id property="studId" column="stud_id" /> 
        <result property="name" column="name" /> 
        <result property="email" column="email" /> 
        <result property="dob" column="dob" /> 
        <result property="phone" column="phone" />
        <!-- 【嵌套查询实现】 -->
        <association property="address" column="addr_id" select="findAddressById" /> 
    </resultMap>
    
    <select id="findStudentByIdWithAddress" parameterType="int" resultMap="StudentWithAddress"> 
        select * from students where stud_id=#{id} 
    </select> 

在此方式中,<association>元素的select属性被设置成了id为findAddressById的语句。这里,两个分开的SQL语句将会在数据库中分别执行,第一个调用findStudentById加载student信息,而第二个调用findAddressById来加载address信息。

addr_id列的值将会被作为输入参数传递给selectAddressById语句。

3.【映射接口】
    public Address findAddressById(int id);
    另外一个映射方法 之前已经实现,不需要再次定义。
    
4.【测试代码】
    测试代码同上。

我们可以如下调用findStudentWithAddress映射语句:
    StudentMapper mapper = sqlSession.getMapper(StudentMapper.class); 
    Student student = mapper.selectStudentWithAddress(studId); 
    System.out.println(student); 
    System.out.println(student.getAddress());

总结:
嵌套结果映射 本质上是一条sql语句查询多张表;
嵌套查询 本质上是每条sql语句查询一张表,组合在一起查询多张表。
效率上,嵌套结果更快。

结论:
如果是 嵌套结果,通过 association标签中的 resultMap属性 实现;
如果是 嵌套查询,通过 association标签中的 select属性 实现。


3.5 一对多映射
【一个讲师】tutors可以教授一个或者【多个课程】course。这意味着讲师和课程之间存在一对多的映射关系。

第一步:建立表结构,并插入数据
注意:在一对多关系中,数据库建表的时候外键一定是在多的那一方建立.
建表语句:
drop table tutors;
drop table courses;
如果需要可以使用 cascade constraints;

    create table tutors(
        tutor_id number primary key,
        name varchar2(50) not null,
        email varchar2(50) ,
        phone varchar2(15) ,  
        addr_id number(11) references addresses (addr_id)
    );

    create table courses(
        course_id number primary key,
        name varchar2(100) not null,
        description varchar2(512),
        start_date date ,
        end_date date ,
        tutor_id number references tutors (tutor_id)
    );

tutors 表的样例数据如下:
    tutor_id   name     email         phone     addr_id 
        1       zs  zs@briup.com   123-456-7890    1 
        2       ls  ls@briup.com   111-222-3333    2 

插入数据:
    insert into tutors(tutor_id,name,email,phone,addr_id)
    values(1,'zs','zs@briup.com','123-456-7890',1);
    insert into tutors(tutor_id,name,email,phone,addr_id)
    values(2,'ls','ls@briup.com','111-222-3333',2);

course 表的样例数据如下:
    course_id  name  description  start_date   end_date  tutor_id 
        1    JavaSE    JavaSE      2015-09-10  2016-02-10   1 
        2    JavaEE    JavaEE      2015-09-10  2016-03-10   2 
        3    MyBatis   MyBatis     2015-09-10  2016-02-20   2 

插入数据:
    insert into courses(course_id,name,description,start_date,end_date,tutor_id) 
    values(1,'JavaSE','JavaSE',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-02-10','yyyy-mm-dd'),1);

    insert into     courses(course_id,name,description,start_date,end_date,tutor_id)
    values(2,'JavaEE','JavaEE',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-03-10','yyyy-mm-dd'),2);

    insert into         courses(course_id,name,description,start_date,end_date,tutor_id)
    values(3,'MyBatis','MyBatis',to_date('2015-09-10','yyyy-mm-dd'),to_date('2016-02-20','yyyy-mm-dd'),1);

在上述的表数据中,zs 讲师教授一个课程,而 ls 讲师教授两个课程

第二步:建立对应Java类
java代码:
public class Tutor{
private Integer tutorId;
private String name;
private String email;
private PhoneNumber phone;
private Address address;
//【此处注意,一个老师可以教多门课,所以用集合】
private List<Course> courses;

    get/set
}

public class Course{
    private Integer courseId; 
    private String name; 
    private String description; 
    private Date startDate; 
    private Date endDate; 

    get/set
}

【<collection>元素】被用来将多行课程结果映射成一个课程Course对象的一个集合。和一对一映射一样,我们可以使用【嵌套结果ResultMap】和【嵌套查询Select】语句两种方式映射实现一对多映射。

第三步
建立One2MoreMapper.xml映射文件
建立One2MoreMapper.java映射接口

在mybatis-config.xml文件中配置mapper标签,使映射文件生效。

3.5.1 使用嵌套结果 ResultMap 实现一对多映射

第四步:在One2MoreMapper.xml文件中添加以下配置
使用嵌套结果resultMap方式获得讲师及其课程信息,
注意,一个讲师类Tutor 中包含一个地址address 和多门课程 courses。
所以表示讲师的ResultMap里面可以包含一个address和多个course;

代码如下:
    <!-- 只要遇到Address对象,就按照AddressResult封装 -->
    <resultMap type="Address" id="AddressResult"> 
        <id property="addrId" column="addr_id" /> 
        <result property="street" column="street" /> 
        <result property="city" column="city" /> 
        <result property="state" column="state" /> 
        <result property="zip" column="zip" /> 
        <result property="country" column="country" /> 
    </resultMap>
    
    <!-- 只要遇到Course对象,就按照CourseResult封装 -->
    <resultMap type="Course" id="CourseResult"> 
        <id column="course_id" property="courseId" /> 
        <result column="name" property="name" /> 
        <result column="description" property="description" /> 
        <result column="start_date" property="startDate" /> 
        <result column="end_date" property="endDate" /> 
    </resultMap> 
    
    <!-- 只要遇到Tutor类型对象,就按照TutorResult封装 -->
    <resultMap type="Tutor" id="TutorResult"> 
        <id column="tutor_id" property="tutorId" /> 
        <result column="name" property="name" /> 
        <result column="email" property="email" /> 
        <result column="phone" property="phone" /> 
        <!-- association表一对一关系,address属性用AddressResult处理 -->
        <association property="address" resultMap="AddressResult" />
        <!-- collection表一对多关系,遇到courses集合成员 -->
        <collection property="courses" resultMap="CourseResult" /> 
    </resultMap> 

<!-- 以下为【select语句】通过【左外连接】实现 -->
    <select id="findTutorById" parameterType="int" resultMap="TutorResult"> 
        select t.tutor_id, t.name, t.email,t.phone, c.course_id, c.name, description, start_date, end_date , a.addr_id, a.street, a.state, a.zip, a.country
        from tutors t left outer join addresses a on t.addr_id=a.addr_id 
        left outer join courses c on t.tutor_id=c.tutor_id 
        where t.tutor_id=#{tutorid} 
    </select> 
    
select语句也可以通过【多表查询】 【等值连接】实现
    <select id="findTutorById" parameterType="int" resultMap="TutorResult">
        select t.tutor_id,t.name,t.email,t.phone,
        a.addr_id,a.street,a.city,a.state,a.zip,a.country,
        c.course_id,c.name,c.description,c.start_date,c.end_date
        from tutors t , addresses a, courses c
        where t.addr_id=a.addr_id
        and c.tutor_id=t.tutor_id
        and t.tutor_id=#{id}
    </select>
    
【注意】
    此时查询出来的结果中,课程name值和讲师name值相同,原因是select中有多个name,产生了歧义,可以使用别名解决,即c.name as 【cname】。
    
    这里我们使用了一个简单的使用了JOINS连接的Select语句获取讲师及其所教课程信息。<collection>元素的resultMap属性设置成了CourseResult,CourseResult包含了Course对象属性与表列名之间的映射。
    要查询到Address相关信息,按照上面一对一的方式,在配置中加入<association>。

第五步:添加测试文件,获得session对象,再有session获得mapper对象,然后调用findTutorById(1)方法去获得1号讲师,然后输出。

3.5.2 使用嵌套Select语句实现一对多映射
也可以使用【嵌套Select语句】方式获得讲师及其课程信息
嵌套查询可以理解成子查询,但稍微不同,子查询是先查子句然后再查主句;但此处是先查主句讲师 tutor表信息,得到讲师id后,再据此去查找课程course表信息,同时得到讲师地址id后,再据此查找讲师地址address信息。

第四步:在映射文件One2MoreMapper.xml文件中添加如下代码
    <resultMap type="Address" id="AddressResult"> 
        <id property="addrId" column="addr_id" /> 
        <result property="street" column="street" /> 
        <result property="city" column="city" /> 
        <result property="state" column="state" /> 
        <result property="zip" column="zip" /> 
        <result property="country" column="country" /> 
    </resultMap>
    <resultMap type="Course" id="CourseResult"> 
        <id column="course_id" property="courseId" /> 
        <result column="name" property="name" /> 
        <result column="description" property="description" /> 
        <result column="start_date" property="startDate" /> 
        <result column="end_date" property="endDate" /> 
    </resultMap>

    
    <resultMap type="Tutor" id="TutorResult"> 
        <id column="tutor_id" property="tutorId" /> 
        <result column="tutor_name" property="name" /> 
        <result column="email" property="email" /> 
        <result column="phone" property="phone" />
        <!--根据表中addr_id去findAddressById,获得address成员信息 -->
        <association property="address" column="addr_id" select="findAddressById"></association>
        <!-- 【第3执行子句】,把当前tutor_id表中列的值当做参数传递给findCoursesByTutor去查询,得到的结果封装到Tutor类中的courses属性中 -->
        <collection property="courses" column="tutor_id" select="findCoursesByTutor" /> 
    </resultMap> 
    
    <!-- 此处是select主句,【第1执行】 -->
    <select id="findTutorById" parameterType="int" resultMap="TutorResult"> 
        select *  
        from tutors
        where tutor_id=#{tutor_id} 
    </select>
    <!-- 此处是select子句,【第2执行】-->
    <select id="findAddressById" parameterType="int" resultMap="AddressResult">
        select *
        from addresses
        where addr_id = #{addr_id}
    </select>
    <!-- 此处是select子句,【第3执行】-->
    <select id="findCoursesByTutor" parameterType="int" resultMap="CourseResult">
       select * 
       from courses 
       where tutor_id=#{tutor_id} 
    </select> 


注意:     
    在这种方式中,<aossication>元素的select属性被设置为id为findCourseByTutor的语句,用来触发单独的SQL查询加载课程信息。tutor_id这一列值将会作为输入参数传递给 findCouresByTutor语句。

第五步:去One2MoreMapper.java接口中添加成员方法
    public interface TutorMapper{ 
        Tutor findTutorById(int tutorId); 
    } 

第六步:在测试文件中调用方法
    TutorMapper mapper = sqlSession.getMapper(TutorMapper.class); 
    Tutor tutor = mapper.findTutorById(tutor Id); 
    System.out.println(tutor); 
    List<Course> courses = tutor.getCourses(); 
    for (Course course : courses){ 
        System.out.println(course); 
    } 
【注意】嵌套查询Select语句查询会导致1+N选择问题。首先,主查询将会执行(1 次),对于主查询返回的每一行,另外一个查询将会被执行(主查询 N 行,则此查询 N 次)。对于大量数据而言,这会导致很差的性能问题。

总之,我们还是推荐【嵌套结果】方式。

3.5 多对多映射
对于在mybatis中的多对多的处理,其实我们可以参照一对多来解决
【注意】在这个例子中有三个字段都是一样的:id,这种情况一定要小心,要给列起别名的(上面的一对一和一对多中如果出现这种情况也是一样的处理方式)

多对多的关系,需要建立第三张桥表 来帮助实现功能。

第一步:建立表结构【此处不用插入数据,通过mybatis编码实现】
    建表语句:
    drop table student_course;
    drop table course;
    drop table student;
    如果需要可以使用 cascade constraints;

    create table course (
        id number primary key,
        course_code varchar2(30) not null,
        course_name varchar2(30) not null 
    );
    create table student (
        id number primary key,
        name varchar2(10) not null,
        gender varchar2(10) ,
        major varchar2(10) ,
        grade varchar2(10) 
    );
    //学生课程表  就是 桥表
    create table student_course (
        id number primary key,
        student_id number references student(id),
        course_id number references course(id)
    );

第二步:建立对应Java类 com.briup.many2many
    java代码:
    public class Course {
        private Integer id;
        private String courseCode; // 课程编号
        private String courseName;// 课程名称
        private List<Student> students;// 选课学生
        get/set
    }
    public class Student {
        private Integer id;
        private String name; // 姓名
        private String gender; // 性别
        private String major; // 专业
        private String grade; // 年级
        private List<Course> courses;// 所选的课程
        get/set
    }
    
第三步:建立Many2ManyMapper.java接口
    public interface Many2ManyMapper {
        //插入student数据
        public void insertStudent(Student student);
        //插入course数据
        public void insertCourse(Course course);
        //通过id查询学生
        public Student getStudentById(Integer id);
        //通过id查询课程
        public Course getCourseById(Integer id);
        
        //学生x选课y
        public void studentSelectCourse(Student student, Course course);
        //查询比指定id值小的学生信息
        public List<Student> getStudentByIdOnCondition(Integer id);
        //查询student级联查询出所选的course并且组装成完整的对象
        public Student getStudentByIdWithCourses(Integer id);
    }

第四步:在映射文件Many2ManyMapper.xml中进行配置
a.插入学生、课程信息【基本操作】
    <insert id="insertStudent" parameterType="Student">
        <selectKey keyProperty="id" resultType="int" order="BEFORE">
            select my_seq.nextval from dual
        </selectKey>
        insert into student(id,name,gender,major,grade)
        values(#{id},#{name},#{gender},#{major},#{grade})
    </insert>
    
    <insert id="insertCourse" parameterType="Course">
        <selectKey keyProperty="id" resultType="int" order="BEFORE">
            select my_seq.nextval from dual
        </selectKey>
        insert into course(id,course_code,course_name)
        values(#{id},#{courseCode},#{courseName})
    </insert>

b.Many2ManyMapperTest.java文件中添加测试代码
【插入基本信息到数据库,注意 两个表先不相互包含】
    //插入学生 
    Student s = new Student("lisi", "男", "计算机", "大四");
    mapper.insertStudent(s);
    session.commit();
    
    //插入课程
    mapper.insertCourse(new Course("002","Oracle"));
    session.commit();

c.在Many2ManyMapper.xml中配置查询操作
    <select id="getStudentById" parameterType="int" resultType="Student">
        select id,name,gender,major,grade
        from student
        where id=#{id}
    </select>
    
    <select id="getCourseById" parameterType="int" resultType="Course">
        select id,course_code as courseCode,course_name as courseName
        from course
        where id=#{id}
    </select>

d.Many2ManyMapperTest.java文件中添加测试代码
    //查学生
    Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
    Student stu = mapper.getStudentById(22);
    System.out.println(stu);

    //查课程
    Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
    Course course = mapper.getCourseById(24);
    System.out.println(course);

在前面所有功能实现完成以后,再做下面的工作

【核心功能】,实现学生选课功能
    
第五步:映射文件中配置【学生选课】 多对多映射
a.添加xml配置文件
    <!-- param1代表方法中第一个参数 以此类推 -->
    <insert id="studentSelectCourse">
        insert into student_course(id,student_id,course_id)
        values(my_seq.nextval,#{param1.id},#{param2.id})
    </insert>

b.添加测试代码
    Student student = mapper.getStudentById(22);
    Course course = mapper.getCourseById(24);
    mapper.studentSelectCourse(student,course);
    session.commit();

c. 根据条件查找学生信息
    <!-- 【注意】如果有特殊符号的话 需要用 <![CDATA[ 特殊符号 ]]>  例如 < & 等等 -->
    <select id="getStudentByIdOnCondition" parameterType="int" resultType="Student">
        select *
        from student
        where id <![CDATA[ < ]]> #{id}
    </select>
d. 添加测试代码
    Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
    List<Student> list = mapper.getStudentByIdOnCondition(30);
    for (Student student : list) {
        System.out.println(student);
    }

第六步:核心功能,查询student级联查询出所选的course并且组装成完整的对象
a.封装基本Student查询结果,不包含Course
    <!-- 
         这里使用了嵌套结果ResultMap的方式进行级联查询 
         当然也可以使用嵌套查询select 
    -->
    <!-- 映射一个基本的Student查询结果 -->
    <resultMap id="StudentResult" type="Student">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="gender" column="gender"/>
        <result property="major" column="major"/>
        <result property="grade" column="grade"/>
    </resultMap>
b.在以上ResultMap基础上,封装完整Student查询结果
    <!-- 继承上面那个基本的映射,再扩展出级联查询 -->
    <resultMap id="StudentResultWithCourses" type="Student" extends="StudentResult">
        <collection property="courses" resultMap="CourseResult"></collection>
    </resultMap>
c.单独再封装遇到的Course对象为CourseResult
    <!-- 这里特别要是的是column="cid" 这是和select语句中的 c.id as cid对应的 一定一定一定要对应起来 -->
    <resultMap id="CourseResult" type="Course">
        <id property="id" column="cid"/>
        <result property="courseCode" column="course_code"/>
        <result property="courseName" column="course_name"/>
    </resultMap>
d.书写select查询语句
    <!-- 
        注意:查询语句的中的c.id as cid这个地方,避免名字相同出现查询结果不正确的情况
        同时在id="CourseResult"的resultMap中也有与这里对应的设置要特别特别注意
    -->
    <select id="getStudentByIdWithCourses" parameterType="int" resultMap="StudentResultWithCourses">
        select s.id,s.name,s.gender,s.major,s.grade,c.id as cid,c.course_code,c.course_name,sc.id,sc.student_id,sc.course_id
        from student s,course c,student_course sc
        where s.id=#{id}
        and s.id=sc.student_id 
        and sc.course_id=c.id
    </select>
e.添加测试代码
    Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
    Student stu = mapper.getStudentByIdWithCourses(22);
    System.out.println(stu);

Many2ManyMapperTest.java中完整测试代码如下:
@Test
public void test_insertStudent(){

    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
            
        mapper.insertStudent(new Student("张三","男","计算机","大四"));
        
        session.commit();
        
    } catch (Exception e) {
        e.printStackTrace();
        session.rollback();
    }finally {
        if(session!=null)session.close();
    }
    
}

@Test
public void test_insertCourse(){
    
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
            
        mapper.insertCourse(new Course("001","corejava"));
        mapper.insertCourse(new Course("002","oracle"));
        
        session.commit();
        
    } catch (Exception e) {
        e.printStackTrace();
        session.rollback();
    }finally {
        if(session!=null)session.close();
    }
    
}

@Test
public void test_getStudentById() {
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
        
        Student stu = mapper.getStudentById(61);
        System.out.println(stu);
        
        session.commit();
        
    } catch (Exception e) {
        e.printStackTrace();
        session.rollback();
    }finally {
        if(session!=null)session.close();
    }
}

@Test
public void test_getCourseById() {
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
        
        Course c = mapper.getCourseById(62);
        System.out.println(c);
        
        session.commit();
        
    } catch (Exception e) {
        e.printStackTrace();
        session.rollback();
    }finally {
        if(session!=null)session.close();
    }
    
}

@Test
public void test_studentSelectCourse(){
    
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
        
        //【注意】一定是 获取已经插入表中的学生 和 课程,然后进行选课
        Student student = mapper.getStudentById(58);
        Course course = mapper.getCourseById(59);
        
        mapper.studentSelectCourse(student, course);
        
        session.commit();
        
    } catch (Exception e) {
        e.printStackTrace();
        session.rollback();
    }finally {
        if(session!=null)session.close();
    }
    
}

@Test
public void test_getStudentByIdOnCondition(){
    
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
        
        List<Student> list = mapper.getStudentByIdOnCondition(100);
        
        for(Student s:list){
            System.out.println(s);
        }
        
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        if(session!=null)session.close();
    }
    
}

@Test
public void test_getStudentByIdWithCourses(){
    
    SqlSession session = null;
    try {
        session = MyBatisSqlSessionFactory.openSession();
        
        Many2ManyMapper mapper = session.getMapper(Many2ManyMapper.class);
        
        Student student = mapper.getStudentByIdWithCourses(58);
        
        System.out.println(student);
        
    } catch (Exception e) {
        e.printStackTrace();
    }finally {
        if(session!=null)session.close();
    }
    
}


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

推荐阅读更多精彩内容