zoukankan      html  css  js  c++  java
  • JPA 使用 Specification 复杂查询和 Criteria 查询

    转自:https://blog.wuwii.com/jpa-specification.html

    前言

    JPA 给我们提供了基础的 CURD 的功能,并且用起来也是特别的方便,基本都是一行代码完成各种数据库操作,但是在复杂的多表查询的时候,我总是遇到各种问题,以前一般都是用原生 SQL 就行查询,原来它自带了复杂查询的 JpaSpecificationExecutor 接口,可以完成各种复杂查询,而且配合 JAVA 8的新特性,使用起来也是特别的方便。

    环境:

    使用 JpaSpecificationExecutor 复杂查询

    了解 JpaSpecificationExecutor

    JPA 提供动态接口,利用类型检查的方式,进行复杂的条件查询,这个比自己写 SQL 更加安全。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    public interface JpaSpecificationExecutor<T> {
     
    /**
    * Returns a single entity matching the given {@link Specification}.
    *
    * @param spec
    * @return
    */
    T findOne(Specification<T> spec);
     
    /**
    * Returns all entities matching the given {@link Specification}.
    *
    * @param spec
    * @return
    */
    List<T> findAll(Specification<T> spec);
     
    /**
    * Returns a {@link Page} of entities matching the given {@link Specification}.
    *
    * @param spec
    * @param pageable
    * @return
    */
    Page<T> findAll(Specification<T> spec, Pageable pageable);
     
    /**
    * Returns all entities matching the given {@link Specification} and {@link Sort}.
    *
    * @param spec
    * @param sort
    * @return
    */
    List<T> findAll(Specification<T> spec, Sort sort);
     
    /**
    * Returns the number of instances that the given {@link Specification} will return.
    *
    * @param spec the {@link Specification} to count instances for
    * @return the number of instances
    */
    long count(Specification<T> spec);
    }

    Specification 是我们传入进去的查询参数,实际上它是一个接口,并且只有一个方法:

    1
    2
    3
    4
    public interface Specification<T> {
     
    Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb);
    }

    使用

    创建实体类

    1. 我现在有三个实体类是关联关系:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      @Entity
      @Data
      public class Employee implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
       
      @OneToOne(cascade = CascadeType.ALL) // 拥有级联维护的一方,参考http://westerly-lzh.github.io/cn/2014/12/JPA-CascadeType-Explaining/
      @JoinColumn(foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
      private EmployeeDetail detail;
       
      @ManyToOne(fetch = FetchType.LAZY) // 默认 lazy ,通过懒加载,知道需要使用级联的数据,才去数据库查询这个数据,提高查询效率。
      // 设置外键的问题,参考http://mario1412.github.io/2016/06/27/JPA%E4%B8%AD%E5%B1%8F%E8%94%BD%E5%AE%9E%E4%BD%93%E9%97%B4%E5%A4%96%E9%94%AE/
      @JoinColumn(name = "jobId", foreignKey = @ForeignKey(name = "none", value = ConstraintMode.NO_CONSTRAINT))
      private Job job;
      }
       
      @Entity
      @Data
      public class EmployeeDetail implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
       
      private String name;
       
      private String phone;
       
      private Integer age;
      }
       
      @Entity
      @Data
      public class Job implements Serializable {
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
       
      private String name;
       
      @OneToMany(targetEntity = Employee.class, mappedBy = "job") // mappedBy 只有在双向关联的时候设置,表示关系维护的一端,否则会生成中间表A_B
      @org.hibernate.annotations.ForeignKey(name = "none") // 注意这里不能使用 @JoinColumn 中的 @ForeignKey 不然会生成外键
      private Set<Employee> employees;
      }

    创建持久化元模型

    这个可以不实现,但是在后面实现复杂查询的时候,只能手动输入相关的实体类的属性字段的字符串,然后进行强制转换类型,我认为这样相对来说好维护一些,关于持久化元模型,将在这篇文章最后部分再详细介绍下。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /**
    * 不知道什么原因
    * 这个持久性单元模型需要与实体类相同的包中,否则相关的值不会注入到 spring 容器中
    *
    *
    * @author Zhang Kai
    * @version 1.0
    * @since <pre>2018/3/8 10:16</pre>
    */
    @StaticMetamodel(Employee.class)
    public class Employee_ {
    public static volatile SingularAttribute<Employee, Long> id;
    public static volatile SingularAttribute<Employee, EmployeeDetail> detail;
    public static volatile SingularAttribute<Employee, Job> job;
    }
     
    @StaticMetamodel(EmployeeDetail.class)
    public class EmployeeDetail_ {
    public static volatile SingularAttribute<EmployeeDetail, Long> id;
    public static volatile SingularAttribute<EmployeeDetail, String> name;
    public static volatile SingularAttribute<EmployeeDetail, String> phone;
    public static volatile SingularAttribute<EmployeeDetail, Integer> age;
    }
     
    @StaticMetamodel(EmployeeDetail.class)
    public class EmployeeDetail_ {
    public static volatile SingularAttribute<EmployeeDetail, Long> id;
    public static volatile SingularAttribute<EmployeeDetail, String> name;
    public static volatile SingularAttribute<EmployeeDetail, String> phone;
    public static volatile SingularAttribute<EmployeeDetail, Integer> age;
    }

    创建 dao 接口,继承 JpaSpecificationExecutor<T>:

    1
    2
    3
    @Repository
    public interface EmployeeDao extends JpaSpecificationExecutor<Employee>, PagingAndSortingRepository<Employee, Id> {
    }

    实现复杂动态查询:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    /**
    * 多条件动态分页查询,
    * <p>
    * 如果后期还需要加入其他的查询的条件,可以直接添加代码逻辑就好了。
    * <p>
    * 分割,需要注意的是 spring data 还提供了<code>Specification</code> 这个类直接提供了 eq | gt | equal 等等 Specification
    * 接口的方法,但是它的方法已经过时了,不推荐使用,如果需要使用记录下
    * 来自 网站 https://www.tianmaying.com/tutorial/spring-jpa-page-sort
    * <code>
    * public Page<Person> findAll(SearchRequest request) {
    * Specification<Person> specification = new Specifications<Person>()
    * .eq(StringUtils.isNotBlank(request.getName()), "name", request.getName())
    * .gt(Objects.nonNull(request.getAge()), "age", 18)
    * .between("birthday", new Range<>(new Date(), new Date()))
    * .like("nickName", "%og%", "%me")
    * .build();
    * return personRepository.findAll(specification, new PageRequest(0, 15));
    * }
    * </code>
    *
    * @param search 查询属性
    * @param pageable 分页和排序
    * @return 分页数据
    */
    @Override
    public Page<Employee> pageBySearch(EmployeeSearch search, Pageable pageable) {
    return employeeDao.findAll((Specification<Employee>) (root, criteriaQuery, criteriaBuilder) -> {
    List<Predicate> predicates = new LinkedList<>();
    Optional<EmployeeSearch> optional = Optional.ofNullable(search);
    // 根据 employee id 查询
    optional.map(EmployeeSearch::getEmployeeId).ifPresent(id -> {
    predicates.add(criteriaBuilder.equal(root.get(Employee_.id), id));
    });
    // 根据 employee detail name 模糊查询
    optional.map(EmployeeSearch::getEmployeeName).ifPresent(name -> {
    // 使用左联接,如果直接 get(Employee_.detail).get(EmployeeDetail_.name) 就是无条件内联,
    // 相当于 cross join,会产生 笛卡尔积
    Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
    predicates.add(criteriaBuilder.like(join.get(EmployeeDetail_.name),
    "%" + name + "%"));
    });
    // 根据职位名查询
    optional.map(EmployeeSearch::getJobName).ifPresent(name -> {
    Join<Employee, Job> join = root.join(Employee_.job, JoinType.LEFT);
    predicates.add(criteriaBuilder.equal(join.get(Job_.name), name));
    });
    Predicate[] array = new Predicate[predicates.size()];
    return criteriaBuilder.and(predicates.toArray(array));
    }, pageable);
    }

    CriteriaBuilder 有各种操作方法完成查询操作。

    进行测试

    1
    2
    3
    4
    5
    6
    7
    @Test
    public void testPageBySearch() throws Exception {
    EmployeeSearch employeeSearch = new EmployeeSearch(null, null, "程序猿");
    Sort sort = new Sort(Sort.Direction.ASC, "id");
    Page<Employee> employees = employeeService.pageBySearch(employeeSearch, new PageRequest(0, 5, sort));
    Assert.assertThat("18772383543", Matchers.equalTo(employees.getContent().get(0).getDetail().getPhone()));
    }

    其实实现起来不是很复杂,但是用起来很舒坦,再完全不用手动拼接字符串,使用面向对象的类型检测,写起来 BUG 也少些。

    使用 CriteriaQuery 查询和类型安全检测

    使用criteria 查询

    例如我现在要实现一个接口,查询在大于或等于某个年纪的员工:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    @PersistenceContext
    private EntityManager em;
     
    /**
    * Search age gt or eq the parameter
    *
    * @param age
    * @return
    */
    @Override
    public List<Employee> listByAge(Integer age) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
    Root<Employee> root = query.from(Employee.class); // 设置查询根,可以根据查询的类型设置不同的 就是 Form 语句 后面的 entity
    List<Predicate> predicates = new LinkedList<>();
    // 连表查询使用左连接
    Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
    predicates.add(cb.gt(join.get(EmployeeDetail_.age), age));
    predicates.add(cb.equal(join.get(EmployeeDetail_.age), age));
    // 设置排序规则
    Order order = cb.asc(root.get(Employee_.id));
    query.orderBy(order);
    query.where(cb.or(predicates.toArray(new Predicate[predicates.size()])));
    TypedQuery typedQuery = em.createQuery(query); // TypedQuery执行查询与获取元模型实例
    return typedQuery.getResultList();
    }

    构建CriteriaQuery 实例API说明

    CriteriaBuilder 安全查询创建工厂

    CriteriaBuilder 安全查询创建工厂,创建CriteriaQuery,创建查询具体具体条件Predicate 等
    CriteriaBuilder 是一个工厂对象,安全查询的开始.用于构建JPA安全查询.可以从EntityManager 或EntityManagerFactory类中获得CriteriaBuilder.
    CriteriaBuilder 工厂类是调用EntityManager.getCriteriaBuilder 或 EntityManagerFactory.getCriteriaBuilder而得。

    CriteriaQuery 安全查询主语句

    CriteriaQuery对象必须在实体类型或嵌入式类型上的Criteria 查询上起作用。
    它通过调用 CriteriaBuilder, createQuery 或CriteriaBuilder.createTupleQuery 获得。

    Root 定义查询的From子句中能出现的类型

    AbstractQuery是CriteriaQuery 接口的父类。它提供得到查询根的方法。
    Criteria查询的查询根定义了实体类型,能为将来导航获得想要的结果,它与SQL查询中的FROM子句类似。
    Root实例也是类型化的,且定义了查询的FROM子句中能够出现的类型。
    查询根实例能通过传入一个实体类型给 AbstractQuery.from方法获得。
    Criteria查询,可以有多个查询根。

    1
    Root<Employee> employee = criteriaQuery.from(Employee.class);
    Predicate 过滤条件

    过滤条件应用到SQL语句的FROM子句中。因此它是 root 创建的。
    在criteria 查询中,查询条件通过Predicate 或Expression 实例应用到CriteriaQuery 对象上。
    这些条件使用 CriteriaQuery .where 方法应用到CriteriaQuery 对象上。
    CriteriaBuilder 也是作为Predicate 实例的工厂,Predicate 对象通过调用CriteriaBuilder 的条件方法( equal,notEqual, gt, ge,lt, le,between,like等)创建。
    Predicate 实例也可以用Expression 实例的 isNull, isNotNull 和 in方法获得,复合的Predicate 语句可以使用CriteriaBuilder的and, or andnot 方法构建。
    下面的代码片段展示了Predicate 实例检查年龄大于24岁的员工实例:

    1
    2
    Predicate condition = criteriaBuilder.gt(employee.get(Employee_.age), 24);
    criteriaQuery.where(condition);

    Employee_元模型类age属性,称之为路径表达式。若age属性与String文本比较,编译器会抛出错误,这在JPQL中是不可能的。

    Predicate[] 多个过滤条件

    支持复杂的 条件拼接, or 语句

    1
    predicatesList.add(criteriaBuilder.or(criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.repairing),criteriaBuilder.equal(root.get(RepairOrder_.localRepairStatus), LocalRepairStatus.diagnos)));

    最后查询的时候

    1
    2
    query.where(cb.or(predicates.toArray(new Predicate[predicates.size()])));
    TypedQuery typedQuery = em.createQuery(query); // TypedQuery执行查询与获取元模型实例
    TypedQuery执行查询与获取元模型实例

    注意,你使用EntityManager创建查询时,可以在输入中指定一个CriteriaQuery对象,它返回一个TypedQuery,它是JPA 2.0引入javax.persistence.Query接口的一个扩展,TypedQuery接口知道它返回的类型。

    所以使用中,先创建查询得到TypedQuery,然后通过typeQuery得到结果.

    当EntityManager.createQuery(CriteriaQuery)方法调用时,一个可执行的查询实例会创建,该方法返回指定从 criteria 查询返回的实际类型的TypedQuery 对象。

    TypedQuery 接口是javax.persistence.Queryinterface.的子类型。在该片段中, TypedQuery 中指定的类型信息是Employee,调用getResultList时,查询就会得到执行 :

    1
    2
    TypedQuery<Employee> typedQuery = em.createQuery(criteriaQuery);
    List<Employee> result = typedQuery.getResultList();

    元模型实例通过调用 EntityManager.getMetamodel 方法获得,EntityType<Employee>的元模型实例通过调用Metamodel.entity(Employee.class)而获得,其被传入 CriteriaQuery.from 获得查询根。

    1
    2
    3
    Metamodel metamodel = em.getMetamodel();
    EntityType<Employee> Employee_ = metamodel.entity(Employee.class);
    Root<Employee> empRoot = criteriaQuery.from(Employee_);
    Expression 用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法

    Expression对象用在查询语句的select,where和having子句中,该接口有 isNull, isNotNull 和 in方法,下面的代码片段展示了Expression.in的用法,employye的年龄检查在20或24的。

    1
    2
    3
    4
    CriteriaQuery<Employee> criteriaQuery = criteriaBuilder .createQuery(Employee.class);
    Root<Employee> employee = criteriaQuery.from(Employee.class);
    criteriaQuery.where(employee.get(Employee_.age).in(20, 24));
    em.createQuery(criteriaQuery).getResultList();

    下面也是一个更贴切的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //定义一个Expression
    Expression<String> exp = root.get(Employee.id);
    //
    List<String> strList=new ArrayList<>();
    strList.add("20");
    strList.add("24");
    predicatesList.add(exp.in(strList));
     
    criteriaQuery.where(predicatesList.toArray(new Predicate[predicatesList.size()]));
    复合谓词

    Criteria Query也允许开发者编写复合谓词,通过该查询可以为多条件测试下面的查询检查两个条件。首先,name属性是否以M开头,其次,employee的age属性是否是25。逻辑操作符and执行获得结果记录。

    1
    2
    3
    4
    5
    6
    criteriaQuery.where(
    criteriaBuilder.and(
    criteriaBuilder.like(employee.get(Employee_.name), "M%"),
    criteriaBuilder.equal(employee.get(Employee_.age), 25)
    ));
    em.createQuery(criteriaQuery).getResultList();
    路径表达式

    Root实例,Join实例或者从另一个Path对象的get方法获得的对象使用get方法可以得到Path对象,当查询需要导航到实体的属性时,路径表达式是必要的。
    Get方法接收的参数是在实体元模型类中指定的属性。

    参数化表达式

    在JPQL中,查询参数是在运行时通过使用命名参数语法(冒号加变量,如 :age传入的。在Criteria查询中,查询参数是在运行时创建ParameterExpression对象并为在查询前调用TypeQuery,setParameter方法设置而传入的。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ParameterExpression<Integer> ageParameter = cb.parameter(Integer.class);
    List<Predicate> predicates = new LinkedList<>();
    // 连表查询使用左连接
    Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
    predicates.add(cb.gt(join.get(EmployeeDetail_.age), ageParameter));
    predicates.add(cb.equal(join.get(EmployeeDetail_.age), ageParameter));
    query.where(cb.or(predicates.toArray(new Predicate[predicates.size()])));
    TypedQuery typedQuery = em.createQuery(query); // TypedQuery执行查询与获取元模型实例
    return typedQuery.setParameter(ageParameter, age).getResultList();
    排序结果
    1
    2
    3
    // 设置排序规则
    Order order = cb.asc(root.get(Employee_.id));
    query.orderBy(order);

    可以设置多个 order。

    分组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /**
    * 分组统计重名数量
    * @param name
    * @return
    */
    @Override
    public List<Tuple> groupByName(String name) {
    CriteriaBuilder cb = em.getCriteriaBuilder();
    CriteriaQuery<Tuple> query = cb.createTupleQuery();
    Root<Employee> root = query.from(Employee.class);
    Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
    query.groupBy(join.get(EmployeeDetail_.name));
    if (name != null) {
    query.having(cb.like(join.get(EmployeeDetail_.name), "%" + name + "%"));
    }
    query.select(cb.tuple(join.get(EmployeeDetail_.name), cb.count(root)));
    TypedQuery<Tuple> typedQuery = em.createQuery(query);
    return typedQuery.getResultList();
    // print sql :
    //select employeede1_.name as col_0_0_, count(employee0_.id) as col_1_0_ from employee employee0_
    // left outer join employee_detail employeede1_ on employee0_.detail_id=employeede1_.id
    // group by employeede1_.name having employeede1_.name like ?
    }
    返回元组(Tuple)的查询

    查询的时候需要查询 单列 的记录可以使用元组,

    1
    2
    3
    4
    CriteriaQuery<Tuple> criteriaQuery = criteriaBuilder.createTupleQuery();
    Root<Employee> employee = criteriaQuery.from(Employee.class);
    criteriaQuery.multiselect(employee.get(Employee_.name).alias("name"), employee.get(Employee_.age).alias("age"));
    em.createQuery(criteriaQuery).getResultList();
    使用 construct()

    使用一个不是实体类来装载 查询出来的数据,但是必须要的是实体类必须有相应的构造函数才行,还需要注意的是,该装载类必须继承实体类

    1. 首先实现一个装载数据的类,我只用来装载 name, age 两个属性即可,当然需要更多也可以设计的,毕竟我们继承了实体类,如果实体类有的字段我们可以不需要再制造了。

      1
      2
      3
      4
      5
      6
      @Data
      @AllArgsConstructor
      public class EmployeeResult extends Employee {
      private String name;
      private Integer age;
      }
    2. 编写查询的业务代码,很方便:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      /**
      * 使用 构造函数 装载查询出来的数据
      *
      * @return
      */
      @Override
      public List<EmployeeResult> findEmployee() {
      CriteriaBuilder cb = em.getCriteriaBuilder();
      CriteriaQuery<Employee> query = cb.createQuery(Employee.class);
      Root<Employee> root = query.from(Employee.class); // 设置查询根,可以根据查询的类型设置不同的
      Join<Employee, EmployeeDetail> join = root.join(Employee_.detail, JoinType.LEFT);
      // 使用构造函数 CriteriaBuilder.construct 来完成装载数据
      query.select(cb.construct(EmployeeResult.class, join.get(EmployeeDetail_.name), join.get(EmployeeDetail_.age)));
      // 设置排序规则
      Order order = cb.asc(root.get(Employee_.id));
      query.orderBy(order);
      TypedQuery typedQuery = em.createQuery(query); // TypedQuery执行查询与获取元模型实例
      return typedQuery.getResultList();
      }
    返回 Object[]

    Criteria查询也能通过设置值给CriteriaBuilder.array方法返回 Object[]的结果。

    1
    criteriaQuery.select(criteriaBuilder.array(root.get(xxx),join.get(xxx)));

    关于持久化元模型

    在JPA中,标准查询是以元模型的概念为基础的.元模型是为具体持久化单元的受管实体定义的.这些实体可以是实体类,嵌入类或者映射的父类.提供受管实体元信息的类就是元模型类.
    描述受管类的状态和他们之间的关系的静态元模型类可以

    1. 从注解处理器产生
    2. 从程序产生
    3. 用EntityManager访问.

    元模型类描述持久化类的元数据。如果一个类安装 JPA 2.0 规范精确地描述持久化实体的元数据,那么该元模型类就是 规范的。规范的元模型类是 静态的,因此它的所有成员变量都被声明为 静态的(也是 public的)。
    Employee类的标准元模型类的名字将是使用 javax.persistence.StaticMetamodel注解的Employee_。元模型类的属性全部是static和public的。Employee的每一个属性都会使用在JPA2规范中描述的以下规则在相应的元模型类中映射:

    • 诸如id,name和age的非集合类型,会定义静态属性SingularAttribute b,这里b是定义在类A中的类型为B的一个对象。
    • 对于Addess这样的集合类型,会定义静态属性ListAttribute<A, B> b,这里List对象b是定义在类A中类型B的对象。其它集合类型可以是SetAttributeMapAttribute 或 CollectionAttribute类型。

    简单总结下

      1. 这次搭建的 jpa 框架是自动生成的数据库表,中间出了很多叉子,例如,自动生成了中间表,不想要外键,给你自己生成外键,而且还不好解决,最终通过谷歌终于找到了解决办法,相应的网页,也在代码上标注出来了,还是基础不行。
      2. 主要理解怎么从 CriteriaBuilder 一步步的在下面创建查询的条件,例如 查询类型 root,查询语句 CriteriaQuery ,查询条件 Predicate ,这样就很容易构建一个 criteria 查询。
      3. 了解了 JPA 复杂查询中 Specification 接口,给人第一体验就是完全面向对象,包括类型检查,基本上代码写了一遍就能编译运行它通过。再多条件动态条件查询的时候,利用 java 8 的 Optional 的空指针检查,比较方便,不用再被长长的 SQL 拼接发麻了。
  • 相关阅读:
    Ubuntu深度学习环境搭建 tensorflow+pytorch
    FineReport调用存储过程
    简单实现固定表格的上表头、左表头
    为什么vertical-align不起作用
    纪录我的iOS学习之路
    【原】使用Bmob作为iOS后台开发心得——云端代码添加其他User的Relation关系
    【原】使用Bmob作为iOS后台开发心得——查询关联关系(BmobRelation)
    【译】Unity3D Shader 新手教程(6/6) —— 更好的卡通Shader
    【译】Unity3D Shader 新手教程(5/6) —— Bumped Diffuse Shader
    【译】Unity3D Shader 新手教程(4/6) —— 卡通shader(入门版)
  • 原文地址:https://www.cnblogs.com/furenjian/p/9609637.html
Copyright © 2011-2022 走看看