zoukankan      html  css  js  c++  java
  • Mybatis基本用法--中

    Mybatis基本用法--中

    第四部分 动态 SQL

      动态 SQL 元素和使用 JSTL 或其他类似基于 XML 的文本处理器相似。MyBatis 采用功能强大的基于 OGNL 的表达式来消除其他元素。

    if
    choose (when, otherwise)
    trim (where, set)
    foreach
    

    4.1 if

    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG WHERE state = ‘ACTIVE’ 
      <if test="title != null">
        AND title like #{title}
      </if>
      <if test="author != null and author.name != null">
        AND author_name like #{author.name}
      </if>
    </select>
    

      注意这种情况传入的title中应该包含%或者_,否则,可以利用下面的bind元素。like模糊查询中,%通配任意字符,_通配一个字符。

    4.2 choose, when, otherwise

    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG WHERE state = ‘ACTIVE’
      <choose>
        <when test="title != null">
          AND title like #{title}
        </when>
        <when test="author != null and author.name != null">
          AND author_name like #{author.name}
        </when>
        <otherwise>
          AND featured = 1
        </otherwise>
      </choose>
    </select>
    

      有点类似于Java 中的 switch 语句,但它不需要break也只会执行一种情况或者不执行(没有匹配的)。

    4.3 trim, where, set

      对于“if”示例,这次我们将“ACTIVE = 1”也设置成动态的条件,看看会发生什么。

    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG 
      WHERE 
      <if test="state != null">
        state = #{state}
      </if> 
      <if test="title != null">
        AND title like #{title}
      </if>
      <if test="author != null and author.name != null">
        AND author_name like #{author.name}
      </if>
    </select>
    

      此时,若一个都没匹配上

    SELECT * FROM BLOG
    WHERE
    

      若只匹配第二个条件,则会多一个AND。此时可以利用<where> 元素。where 元素知道只有在一个以上的if条件有值的情况下才去插入“WHERE”子句。而且,若最后的内容是“AND”或“OR”开头的,where 元素也知道如何将他们去除。

    <select id="findActiveBlogLike"
         resultType="Blog">
      SELECT * FROM BLOG 
      <where> 
        <if test="state != null">
             state = #{state}
        </if> 
        <if test="title != null">
            AND title like #{title}
        </if>
        <if test="author != null and author.name != null">
            AND author_name like #{author.name}
        </if>
      </where>
    </select>
    

      类似的用于动态更新语句的解决方案叫做 set。set 元素可以被用于动态包含需要更新的列,而舍去其他的。比如:

    <update id="updateAuthorIfNecessary">
      update Author
        <set>
          <if test="username != null">username=#{username},</if>
          <if test="password != null">password=#{password},</if>
          <if test="email != null">email=#{email},</if>
          <if test="bio != null">bio=#{bio}</if>
        </set>
      where id=#{id}
    </update>
    

      这里,set 元素会动态前置 SET 关键字,同时也会消除无关的逗号,因为用了条件语句之后很可能就会在生成的赋值语句的后面留下这些逗号。
      其实我们可以自定义 trim 元素来定制我们想要的功能,比如,和 where 元素等价的自定义 trim 元素为:

    <trim prefix="WHERE" prefixOverrides="AND |OR ">
      ... 
    </trim>
    

      prefixOverrides 属性会忽略通过管道分隔的文本序列(注意此例中的空格也是必要的)。它带来的结果就是所有在 prefixOverrides 属性中指定的内容将被移除,并且插入 prefix 属性中指定的内容。

      set 元素等价的自定义 trim 元素,注意这里我们忽略的是后缀中的值:

    <trim prefix="SET" suffixOverrides=",">
      ...
    </trim>
    

    4.4 foreach

      动态 SQL 的另外一个常用的必要操作是需要对一个集合进行遍历,通常是在构建 IN 条件语句的时候。比如:

    <select id="selectPostIn" resultType="domain.blog.Post">
      SELECT *
      FROM POST P
      WHERE ID in
      <foreach item="item" index="index" collection="list"
          open="(" separator="," close=")">
            #{item}
      </foreach>
    </select>
    

      它也允许你指定开闭匹配的字符串以及在迭代中间放置分隔符。你可以将任何可迭代对象(如列表、集合等)和任何的字典或者数组对象传递给foreach作为集合参数。当使用可迭代对象或者数组时,index是当前迭代的次数,item的值是本次迭代获取的元素。当使用字典(或者Map.Entry对象的集合)时,index是键,item是值。

    4.5 bind

      bind 元素可以从 OGNL 表达式中创建一个变量并将其绑定到上下文。比如:

    <select id="selectBlogsLike" resultType="Blog">
      <bind name="pattern" value="'%' + _parameter.getTitle() + '%'" />
      SELECT * FROM BLOG
      WHERE title LIKE #{pattern}
    </select>
    

    第五部分 Java API

    5.1 语句执行方法(不推荐,推荐使用5.6和5.7

      这些方法被用来执行定义在 SQL 映射的 XML 文件中的 SELECT,INSERT,UPDA E T 和 DELETE 语句。它们都会自行解释,每一句都使用语句的 ID 属性和参数对象,参数可以 是原生类型(自动装箱或包装类) ,JavaBean,POJO 或 Map。

    <T> T selectOne(String statement, Object parameter)
    <E> List<E> selectList(String statement, Object parameter)
    <K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey)
    int insert(String statement, Object parameter)
    int update(String statement, Object parameter)
    int delete(String statement, Object parameter)
    

      selectOne 和 selectList 的不同仅仅是 selectOne 必须返回一个对象。 如果多于一个, 或者没有返回 (或返回了 null) 那么就会抛出异常。 如果你不知道需要多少对象, 使用 selectList。
      如果你想检查一个对象是否存在,那么最好返回统计数(0 或 1) 。因为并不是所有语句都需 要参数,这些方法都是有不同重载版本的,它们可以不需要参数对象。

    <T> T selectOne(String statement)
    <E> List<E> selectList(String statement)
    <K,V> Map<K,V> selectMap(String statement, String mapKey)
    int insert(String statement)
    int update(String statement)
    int delete(String statement)
    

      最后,还有查询方法的三个高级版本,它们允许你限制返回行数的范围,或者提供自定 义结果控制逻辑,这通常用于大量的数据集合。

    <E> List<E> selectList (String statement, Object parameter, RowBounds rowBounds)
    <K,V> Map<K,V> selectMap(String statement, Object parameter, String mapKey, RowBounds rowbounds)
    void select (String statement, Object parameter, ResultHandler<T> handler)
    void select (String statement, Object parameter, RowBounds rowBounds, ResultHandler<T> handler)
    

      RowBounds 参数会告诉 MyBatis 略过指定数量的记录,还有限制返回结果的数量。 RowBounds 类有一个构造方法来接收 offset 和 limit,否则是不可改变的。

    int offset = 100;
    int limit = 25;
    RowBounds rowBounds = new RowBounds(offset, limit);
    

      ResultHandler 参数允许你按你喜欢的方式处理每一行。ResultHandler接口很简单

    package org.apache.ibatis.session;
    public interface ResultHandler<T> {
      void handleResult(ResultContext<? extends T> context);
    }
    

    5.2 批量立即更新方法(Flush Method)

      刷新(执行)存储在JDBC驱动类中的批量更新语句

    List<BatchResult> flushStatements()
    

    5.3 事务控制方法

      控制事务作用域有四个方法。 当然, 如果你已经选择了自动提交或你正在使用外部事务管理器,这就没有任何效果了。然而,如果你正在使用 JDBC 事务管理器,由 Connection 实例来控制,那么这四个方法就会派上用场:

    void commit()
    void commit(boolean force)
    void rollback()
    void rollback(boolean force)
    

      注意MyBatis-Spring和MyBatis-Guice提供了声明事务处理,所以如果你在使用Mybatis的同时使用了Spring或者Guice,那么请参考它们的手册以获取更多的内容。

    5.4 清理 Session 级的缓存

    void clearCache()
    

    5.5 确保 SqlSession 被关闭

    SqlSession session = sqlSessionFactory.openSession();
    try {
        // following 3 lines pseudocod for "doing some work"
        session.insert(...);
        session.update(...);
        session.delete(...);
        session.commit();
    } finally {
        session.close();
    }
    

    5.6 使用映射器

      上述的各个 insert,update,delete 和 select 方法都很强大,但也有些繁琐,没有类型安全,对于你的 IDE 和可能的单元测试也没有帮助。因此, 一个更通用的方式来执行映射语句是使用映射器类。推荐使用5.6中的映射器和5.7的映射器注解
      一个映射器类就是一个简单 的接口,其中的方法定义匹配于 SqlSession 方法。

    //注释中的语句对应SqlSession 方法,不推荐!
    public interface AuthorMapper {
      // (Author) selectOne("selectAuthor",5);
      Author selectAuthor(int id); 
      // (List<Author>) selectList(“selectAuthors”)
      List<Author> selectAuthors();
      // (Map<Integer,Author>) selectMap("selectAuthors", "id")
      @MapKey("id")
      Map<Integer, Author> selectAuthors();
      // insert("insertAuthor", author)
      int insertAuthor(Author author);
      // updateAuthor("updateAuthor", author)
      int updateAuthor(Author author);
      // delete("deleteAuthor",5)
      int deleteAuthor(int id);
    }
    
    • 方法名必须匹配映射语句的 ID。返回类型必须匹配期望的结果类型。所有常用的类型都是支持的,包括:原生类型,Map,POJO 和 JavaBean。
    • 映射器接口不需要去实现任何接口或扩展任何类。
    • 你可以传递多个参数给一个映射器方法。 如果你这样做了, 默认情况下它们将会以它们 在参数列表中的位置来命名,比如:#{param1},#{param2}等。如果你想改变参数的名称(只在多参数情况下) ,可以在参数上使用@Param("paramName")注解。
    • 你也可以给方法传递一个 RowBounds 实例来限制查询结果。

    5.7 映射器注解

    注解有下面这些:

    注解 目标 相对应的 XML 描述
    @CacheNamespace <cache> 为给定的命名空间 (比如类) 配置缓存。 属性:implemetation,eviction, flushInterval,size,readWrite,blocking 和 properties。
    @Property N/A <property> 属性名和属性位置
    @ConstructorArgs 方法 <constructor> 收集一组结果传递给一个对象的 构造方法。属性:value,是数组形式的参数。
    @Case N/A <case> 值和它对应的映射的简单情况。属性: value,type,results。Results 属性是结 果数组,因此这个注解和实际的 ResultMap 很相似,由下面的 Results 注解指定。
    @Results 方法 <resultMap> Result映射的列表, 包含列如何被映射到属性或字段的详情。 属 性:value, id。value 属性是 Result 注解的数组。这个id的属性是结果映射的名称。
    @Result N/A <result><id> 在列和属性或字段之间的单独结果映射。属 性:id,column, property, javaType ,jdbcType ,type Handler, one,many。id 属性是一个布尔值,表 示了应该被用于比较(和在 XML 映射 中的相似)的属性。one 属性是单 独 的 联 系, 和 <association> 相 似 , 而 many 属 性 是 对 集 合 而 言 的 , 和 <collection>相似。 它们这样命名是为了 避免名称冲突。
    @One N/A <association> 复杂类型的单独属性值映射。属性: select,已映射语句(也就是映射器方 法)的完全限定名,它可以加载合适类 型的实例。
    @Many N/A <collection> 映射到复杂类型的集合属性。属性:select,已映射语句(也就是映射器方法)的全限定名, 它可以加载合适类型的实例的集合
    @Options 方法 映射语句的属性 它们通常在映射语句上作为 属性出现。属性:useCache=true , flushCache=FlushCachePolicy.DEFAULT , resultSetType=FORWARD_ONLY , statementType=PREPARED , fetchSize=-1 , timeout=-1 useGeneratedKeys=false , keyProperty=”id” , keyColumn=”” , resultSets=””。
    @Insert
    @Update
    @Delete
    @Select
    方法 <insert><update><delete><select> 这些注解中的每一个代表了执行的真实 SQL。
    @Param Parameter N/A 映射器的方法参数命名,默认:#{param1} , #{param2} 等 。 使 用 @Param(“person”),参数应该被命名为 #{person}。
    @ResultMap 方法 N/A 给@Select或者@SelectProvider提供在XML映射中的<resultMap>的id。
    @InsertProvider
    @UpdateProvider
    @DeleteProvider
    @SelectProvider
    Method <insert>``<update>``<delete>``<select> 允许你指定一个 类名和一个方法在运行时动态创建SQL。

    但其实:Java 注解限制了它们的表现和灵活,所以简单的方法使用注解,复杂的方法使用xml配置。
    例子:

    @Results(id = "userResult", value = {
      @Result(property = "id", column = "uid", id = true),
      @Result(property = "firstName", column = "first_name"),
      @Result(property = "lastName", column = "last_name")
    })
    @Select("select * from users where id = #{id}")
    User getUserById(Integer id);
    
    @Results(id = "companyResults")
    @ConstructorArgs({
      @Arg(property = "id", column = "cid", id = true),
      @Arg(property = "name", column = "name")
    })
    @Select("select * from company where id = #{id}")
    Company getCompanyById(Integer id);
    
    
    @SelectProvider(type = UserSqlBuilder.class, method = "buildGetUsersByName")
    List<User> getUsersByName(
        @Param("name") String name, @Param("orderByColumn") String orderByColumn);
    
    class UserSqlBuilder {
    
      // If not use @Param, you should be define same arguments with mapper method
      public String buildGetUsersByName(
          final String name, final String orderByColumn) {
        return new SQL(){{
          SELECT("*");
          FROM("users");
          WHERE("name like #{name} || '%'");
          ORDER_BY(orderByColumn);
        }}.toString();
      }
    
      // If use @Param, you can define only arguments to be used
      public String buildGetUsersByName(@Param("orderByColumn") final String orderByColumn) {
        return new SQL(){{
          SELECT("*");
          FROM("users");
          WHERE("name like #{name} || '%'");
          ORDER_BY(orderByColumn);
        }}.toString();
      }
    }
    

    注意上面的||是字符串连接符,在MySQL中,一般用concat函数。

    第六部分 SQL语句构建器类

    过去我们在Java中写sql语句:

    String sql = "SELECT P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME, "
    "P.LAST_NAME,P.CREATED_ON, P.UPDATED_ON " +
    "FROM PERSON P, ACCOUNT A " +
    "INNER JOIN DEPARTMENT D on D.ID = P.DEPARTMENT_ID " +
    "INNER JOIN COMPANY C on D.COMPANY_ID = C.ID " +
    "WHERE (P.ID = A.ID AND P.FIRST_NAME like ?) " +
    "OR (P.LAST_NAME like ?) " +
    "GROUP BY P.ID " +
    "HAVING (P.LAST_NAME like ?) " +
    "OR (P.FIRST_NAME like ?) " +
    "ORDER BY P.ID, P.FULL_NAME";
    

    现在(优先用下面的变长参数方法):

    private String selectPersonSql() {
      return new SQL() {{
        SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FULL_NAME");
        SELECT("P.LAST_NAME, P.CREATED_ON, P.UPDATED_ON");
        FROM("PERSON P");
        FROM("ACCOUNT A");
        INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID");
        INNER_JOIN("COMPANY C on D.COMPANY_ID = C.ID");
        WHERE("P.ID = A.ID");
        WHERE("P.FIRST_NAME like ?");
        OR();
        WHERE("P.LAST_NAME like ?");
        GROUP_BY("P.ID");
        HAVING("P.LAST_NAME like ?");
        OR();
        HAVING("P.FIRST_NAME like ?");
        ORDER_BY("P.ID");
        ORDER_BY("P.FULL_NAME");
      }}.toString();
    }
    

    给出一些例子:

    // With conditionals (note the final parameters, required for the anonymous inner class to access them)
    public String selectPersonLike(final String id, final String firstName, final String lastName) {
      return new SQL() {{
        SELECT("P.ID, P.USERNAME, P.PASSWORD, P.FIRST_NAME, P.LAST_NAME");
        FROM("PERSON P");
        if (id != null) {
          WHERE("P.ID like #{id}");
        }
        if (firstName != null) {
          WHERE("P.FIRST_NAME like #{firstName}");
        }
        if (lastName != null) {
          WHERE("P.LAST_NAME like #{lastName}");
        }
        ORDER_BY("P.LAST_NAME");
      }}.toString();
    }
    
    public String updatePersonSql() {
      return new SQL() {{
        UPDATE("PERSON");
        SET("FIRST_NAME = #{firstName}");
        WHERE("ID = #{id}");
      }}.toString();
    }
    
    public String deletePersonSql() {
      return new SQL() {{
        DELETE_FROM("PERSON");
        WHERE("ID = #{id}");
      }}.toString();
    }
    
    public String insertPersonSql() {
      return new SQL() {{
        INSERT_INTO("PERSON");
        VALUES("ID, FIRST_NAME", "#{id}, #{firstName}");
        VALUES("LAST_NAME", "#{lastName}");
      }}.toString();
    }
    
    方法 描述
    SELECT(String...) 参数是使用逗号分隔的列名和别名列表,但也可以是数据库驱动程序接受的任意类型。
    FROM(String...) from
    JOIN(String...)
    INNER_JOIN(String...)
    LEFT_OUTER_JOIN(String)
    LEFT_OUTER_JOIN(String...)
    RIGHT_OUTER_JOIN(String)
    RIGHT_OUTER_JOIN(String...)
    参数可以包含由列命和join on条件组合成标准的join。
    WHERE(String...) 插入新的 WHERE子句条件,由AND链接。可以多次被调用,每次都由AND来链接新条件。使用 OR() 来分隔OR。
    OR() 使用OR来分隔当前的 WHERE子句条件。 可以被多次调用,但在一行中多次调用或生成不稳定的SQL。
    AND() 使用AND来分隔当前的 WHERE子句条件。 可以被多次调用,但在一行中多次调用可能生成不稳定的SQL。因为 WHERE 和 HAVING 二者都会自动链接 AND,只是为了完整性才被使用,一般不用主动用。
    GROUP_BY(String...) 插入新的 GROUP BY子句元素,由逗号连接。 可以被多次调用,每次都由逗号连接新的条件。
    HAVING(String...) 插入新的 HAVING子句条件。 由AND连接。可以被多次调用,每次都由AND来连接新的条件。使用 OR() 来分隔OR.
    ORDER_BY(String...) 插入新的 ORDER BY子句元素, 由逗号连接。可以多次被调用,每次由逗号连接新的条件。
    DELETE_FROM(String) 开始一个delete语句并指定需要从哪个表删除的表名。通常它后面都会跟着WHERE语句!
    INSERT_INTO(String) 开始一个insert语句并指定需要插入数据的表名。后面都会跟着一个或者多个VALUES() or INTO_COLUMNS() and INTO_VALUES()。
    SET(String...) 针对update语句,插入到"set"列表中
    UPDATE(String) 开始一个update语句并指定需要更新的表名。后面都会跟着一个或者多个SET(),通常也会有一个WHERE()。
    VALUES(String, String) 插入到insert语句中。第一个参数是要插入的列名,第二个参数则是该列的值。
    INTO_COLUMNS(String...) Appends columns phrase to an insert statement. This should be call INTO_VALUES() with together.
    INTO_VALUES(String...) Appends values phrase to an insert statement. This should be call INTO_COLUMNS() with together.

    从版本3.4.2,可以使用变长参数:

    public String selectPersonSql() {
      return new SQL()
        .SELECT("P.ID", "A.USERNAME", "A.PASSWORD", "P.FULL_NAME", "D.DEPARTMENT_NAME", "C.COMPANY_NAME")
        .FROM("PERSON P", "ACCOUNT A")
        .INNER_JOIN("DEPARTMENT D on D.ID = P.DEPARTMENT_ID", "COMPANY C on D.COMPANY_ID = C.ID")
        .WHERE("P.ID = A.ID", "P.FULL_NAME like #{name}")
        .ORDER_BY("P.ID", "P.FULL_NAME")
        .toString();
    }
    
    public String insertPersonSql() {
      return new SQL()
        .INSERT_INTO("PERSON")
        .INTO_COLUMNS("ID", "FULL_NAME")
        .INTO_VALUES("#{id}", "#{fullName}")
        .toString();
    }
    
    public String updatePersonSql() {
      return new SQL()
        .UPDATE("PERSON")
        .SET("FULL_NAME = #{fullName}", "DATE_OF_BIRTH = #{dateOfBirth}")
        .WHERE("ID = #{id}")
        .toString();
    }
    

    第七部分 Logging

    Mybatis内置的日志工厂提供日志功能,具体的日志实现有以下几种工具:

    SLF4J
    Apache Commons Logging
    Log4j 2
    Log4j
    JDK logging
    

    具体选择哪个日志实现工具由MyBatis的内置日志工厂确定。它会使用最先找到的(按上文列举的顺序查找)。 如果一个都未找到,日志功能就会被禁用。
    你可以通过在MyBatis的配置文件mybatis-config.xml里面添加一项setting(配置)来选择一个不同的日志实现,logImpl可选的值有:SLF4J、LOG4J、LOG4J2、JDK_LOGGING、COMMONS_LOGGING、STDOUT_LOGGING、NO_LOGGING 或者是实现了接口org.apache.ibatis.logging.Log的类的完全限定类名。

    <configuration>
      <settings>
        ...
        <setting name="logImpl" value="LOG4J"/>
        ...
      </settings>
    </configuration>
    

    Logging Configuration

    步骤1: 添加 Log4J 的 jar 包
    步骤2:配置Log4J
    在应用的classpath中创建一个名称为log4j.properties的文件, 文件的具体内容如下:

    # Global logging configuration
    log4j.rootLogger=ERROR, stdout
    # MyBatis logging configuration...
    log4j.logger.org.mybatis.example.BlogMapper=TRACE
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n
    

    添加以上配置后,Log4J就会把 org.mybatis.example.BlogMapper 的详细执行日志记录下来,对于应用中的其它类则仅仅记录错误信息。

    也可以将日志从整个mapper接口级别调整到到语句级别,从而实现更细粒度的控制。如下配置只记录 selectBlog 语句的日志:

    log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
    

    与此相对,可以对一组mapper接口记录日志,只要对mapper接口所在的包开启日志功能即可:

    log4j.logger.org.mybatis.example=TRACE
    

    某些查询可能会返回大量的数据,只想记录其执行的SQL语句该怎么办?为此,Mybatis中SQL语 句的日志级别被设为DEBUG(JDK Logging中为FINE),结果日志的级别为TRACE(JDK Logging中为FINER)。所以,只要将日志级别调整为DEBUG即可达到目的:

    log4j.logger.org.mybatis.example=DEBUG
    

    要记录日志的是类似下面的mapper文件而不是mapper接口又该怎么呢?

    <?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="org.mybatis.example.BlogMapper">
      <select id="selectBlog" resultType="Blog">
        select * from Blog where id = #{id}
      </select>
    </mapper>
    

    对这个文件记录日志,只要对命名空间增加日志记录功能即可:

    log4j.logger.org.mybatis.example.BlogMapper=TRACE
    

    进一步,要记录具体语句的日志可以这样做:

    log4j.logger.org.mybatis.example.BlogMapper.selectBlog=TRACE
    
  • 相关阅读:
    Direct3D轮回:游戏场景之天空
    Direct3D轮回:游戏特效之晴天光晕
    Direct3D轮回:基于.X文件的网格加载及渲染
    Direct3D轮回:游戏特效之风动、雾化
    Direct3D轮回:游戏场景之陆地
    Direct3D轮回:基于ID3DXSprite的2D元素绘制
    Direct3D轮回:基于HLSL实现D3D中的光照特效
    Direct3D轮回:构建基于Direct3D的通用摄影机类
    Direct3D轮回:构建基于DirectInput机制的键盘输入设备
    剪切上传图片源码
  • 原文地址:https://www.cnblogs.com/xzwblog/p/6796846.html
Copyright © 2011-2022 走看看