2018年6月

前言

又是久违的一篇文章。前段时间“不务正业”,因为要做课程设计,去学习了一下SpringBoot。期间也了解了一些数据库查询的东西。这当中也让我体会到原来一条sql语句优化的好,可以剩下很多java代码的处理。去看了一下郭霖的csdn博客,看到他有关litepal系列的文章中有关于查询的教程。原博客地址:请输入链接描述


一对多的关系在LitePal实体类的写法

首先来讲讲一对多关系的实体映射写法。表关联可以说是数据库中的一个基础结构,表和表之间通过相同的字段来建立联系。这里就要提到外键的概念了。以下是一个类似于聊天信息简单表结构。

  • 有用户User表,表中字段为

    private String userId; //主键
    private String phone; //手机
    private String sex; //性别
    private String number; //账号

  • 用户消息表Info表,字段有

    private String userId; //外键
    private String title; //标题
    private String content; //内容
    private String time; //时间

以上只是简单提供一个例子,仅作参考。
显然用户的消息与用户需要建立联系,它们之间的桥梁是userId,通过该字段可以建立两个表一对多的对应关系。

那如何在类中表示他们的对应关系呢?一般所说的一对多关系在java中这个多可以转换为List、Set这些集合。即一个User对象里可以包含一个用Info对象作为泛型的集合。
然后User表中的属性就可以添加一条:

private List<Info> infoList=new ArrayList<>();            //消息列表

LitePal的激进查询

LitePal默认是懒查询的。也就是说即使我们像上面那样通过设置一对多关系后,在仅仅只查询一个User实体的情况下是无法获取到消息列表的。我们必须通过获取到的userId,即外键来再次查询。
当然我们也可以通过调用查询方法的重载方法来进行激进查询:

User user = DataSupport.find(User.class, 1, true);
List<Info> infos = user.getInfoList();

在find方法后面添加一个参数,这个参数的意思就是是否使用激进查询。如果使用了激进的话就相当于联表查询的意思。查询的结果返回User实体和所对应消息Info列表。

当然,其实官方并不推荐这种查询方案。因为如果数据量过大的时候这种查询会大大地降低响应的速度。毕竟有些情况是自己想查询这个User实体但并不需要里面的消息列表。那如果我想封装查询的步骤可以怎么写呢?郭霖给出的方案是把Info表的查询放在User类的get方法中。通过调用get方法查询数据库。

public List<Info> getInfoList() {
    return LitePal.where("userId = ?",userId)        //按照userId查询消息列表按time 降序排序
            .order("time desc")
            .find(Info.class);
}

这种方法可以延迟查询,即有需要是在进行查询操作。


利用原生SQL查询

最后还有一种方法实现查询的是输入原生的SQL语句进行查询操作,这个就不用多说了,就是直接将sql当作字符串作为参数传输查询即可。

DataSupport.findBySQL("select * from User where userId = ?",userId);

数据格式校验的重要性

关于数据格式完整的重要性相信各位都有足够的认识,很多时候我们被要求这个类的这个字段不可为空,这个框应该填入的是数字,这个框应该填入的是一个邮箱格式/手机号码格式...,面对一连串的格式要求传统的办法是自行写一个类对传入的这个类进行一一判断审查,但是作为一名程序员必定不能容忍这种方法,故而在这里给各位介绍一种方便的检验方法。

框架&导入包

SSM、SSH框架
基本校验器:javax.validation
hibernate校验器:org.hibernate.validator
以上两个包版本最好选择比较新的,否则一些注解不支持

格式校验配置顺序

1.新建错误信息文件
2.绑定校验器
3.编写pojo类
4.根据要求给pojo类属性添加格式注解
以下是详解

第一步:新建错误信息文件

该文件是非必须的
该文件存在的意义仅仅是为了不要暴露设计者的设计思想罢了,并不是一个硬需求,到后面的注解编写就会理解这里说的是什么。
一般建议使用properties文件方便XML的读入。

第二步:绑定校验器

这一步一定要做,否则配置出来的校验器将无法工作。那么绑定是怎么个绑定法?以SSM框架为例,必然有一配置文件spring-mvc.xml,虽然每个人自己搭的SSM框架可能名字起的不是很一致,但是大概都是这种格式的名称,在其中添加进去如下代码:

<mvc:annotation-driven  validator="validator"/>
<!-- 校验器 -->
<bean id="validator"
    class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <!-- hibernate校验器 -->
    <property name="providerClass" value="org.hibernate.validator.HibernateValidator" />
    <!-- 指定校验使用的资源文件,在文件中配置校验错误信息,如果不指定则默认使用classpath下的 
            ValidationMessages.properties -->
    <property name="validationMessageSource" ref="messageSource" />
</bean>

这个配置是什么意思呢?也就是定义一数据校验器,class为:org.hibernate.validator.HibernateValidator
而下面的这个validationMessageSource是我们写的那个错误信息文件,之前说了这个是非必须的所以这个配置也是可以省略的。
假如你新建了错误信息文件,那么需要新添加这个配置:

    <!-- 校验错误信息配置文件   xxx.properties -->
<bean id="messageSource"
    class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
    <!-- 资源文件名 基名-->
    <property name="basenames">
        <list>
            <value>classpath:你的文件名称,后缀可以省略</value>
        </list>
    </property>
    <!-- 资源文件编码格式 -->
    <property name="fileEncodings" value="utf-8" />
    <!-- 对资源文件内容缓存时间,单位秒 -->
    <property name="cacheSeconds" value="120" />
</bean>

这样配置就完成了。

第三,四步:编写pojo类,根据要求编写错误信息

这是本是非常简单的一件事,但是由于我们的校验器的存在使得这个类承担了更多的责任,他需要在属性上面加上注解,如下:

import javax.validation.constraints.Pattern;

import javax.validation.constraints.Size;

public class Auctionuser {
private Integer userid;

@Size(min=3,max=6,message="{register.username.length.error}")
private String username;
@Size(min=6,message="{register.password.length.error}")
private String userpassword;
@Pattern(regexp="[0-9]{18}",message="{register.usercardno.length.error}")
private String usercardno;
@Size(min=7,max=8,message="{register.usertel.length.error}")
private String usertel;

private String useraddress;

private String userpostnumber;

private Integer userisadmin;

private String userquestion;

private String useranswer;

public Integer getUserid() {
    return userid;
}

public void setUserid(Integer userid) {
    this.userid = userid;
}

public String getUsername() {
    return username;
}

public void setUsername(String username) {
    this.username = username == null ? null : username.trim();
}

public String getUserpassword() {
    return userpassword;
}

public void setUserpassword(String userpassword) {
    this.userpassword = userpassword == null ? null : userpassword.trim();
}

public String getUsercardno() {
    return usercardno;
}

public void setUsercardno(String usercardno) {
    this.usercardno = usercardno == null ? null : usercardno.trim();
}

public String getUsertel() {
    return usertel;
}

public void setUsertel(String usertel) {
    this.usertel = usertel == null ? null : usertel.trim();
}

public String getUseraddress() {
    return useraddress;
}

public void setUseraddress(String useraddress) {
    this.useraddress = useraddress == null ? null : useraddress.trim();
}

public String getUserpostnumber() {
    return userpostnumber;
}

public void setUserpostnumber(String userpostnumber) {
    this.userpostnumber = userpostnumber == null ? null : userpostnumber.trim();
}

public Integer getUserisadmin() {
    return userisadmin;
}

public void setUserisadmin(Integer userisadmin) {
    this.userisadmin = userisadmin;
}

public String getUserquestion() {
    return userquestion;
}

public void setUserquestion(String userquestion) {
    this.userquestion = userquestion == null ? null : userquestion.trim();
}

public String getUseranswer() {
    return useranswer;
}

public void setUseranswer(String useranswer) {
    this.useranswer = useranswer == null ? null : useranswer.trim();
}
}

可以看到其实它与一般的bean差不多但是多了一些注解例如@Size,@Pattern等,英文水平稍微好点的直接就理解这是什么了,这两个注解一个是属于javax.validation.constraints.Size另外一个属于javax.validation.constraints.Pattern,答案很明显了这两个就是承担校验的注解。以下是大部分注解的名称与使用方法:
827161-20161022150209967-1863141332.png
图片来源于:https://www.cnblogs.com/xiaogangfan/p/5987659.html
如@Size(min=3,max=6,message="{register.username.length.error}")这句注释的意思就是这个属性的最低长度为3,最大长度为6,一旦错误后就会产生register.username.length.error这个错误,但是我们有个问题,register.username.length.error到底是个什么?还记得我们前面新建的错误信息文件吗,properties文件是一个以K-V形式存储信息的一个文件,下面是这个文件的截图:
TIM截图20180626155112.png
我们可以在其中找到
register.username.length.error=u7528u6237u540Du957Fu5EA6u5FC5u987Bu4E3A3u52306u4F4D
后面的那个unicode编码是中文字符串,把鼠标移动到这串unicode编码中就能查看当前信息:
TIM截图20180626155243.png
当然这个方法不是唯一的,你也可以直接在message中直接写入中文也是支持的,但是使用错误信息文件的方法一方面不会导致我们的错误信息暴露,另外一方面也易于维护,本人强烈推荐。

测试

做测试以前需要我们在我们的controller加入一些注解:

@RequestMapping("/register")
public String register(Model model,@Validated Auctionuser user,BindingResult bindResult) {
    if(bindResult.hasErrors()) {
        List<FieldError> errors = bindResult.getFieldErrors();
        for(FieldError error:errors) {
            model.addAttribute(error.getField(), error.getDefaultMessage());
        }
        model.addAttribute("registerUser", user);
        return "register";
    }
    userservice.insertAuctionUser(user);
    System.out.println("--------------->添加用户成功");
    return "login";
}

这个controller很简单,用户在前端页面填写信息提交到这个方法,但是我们还是出现了一些陌生的东西例如:@Validated,这个注解就是告诉容器在传入这个方法前先对这个Auctionuser类的这个user对象进行一次数据格式校验,校验的就是刚刚我们写的那些@Size和@Pattern,假如出现了错误信息那么就把错误信息填充到BindingResult类中,一次校验对应一次BindingResult。
error.getField()获取属性名称(username,password等)
error.getDefaultMessage()获取错误信息message
这代码也不是很难所以也就不怎么解释了。
运行结果截图:
before:
before.png
after:
after.png
是不是挺有趣?


Jeesite数据格式校验

Jeesite是一个高度封装的框架,很多东西thinkgem老哥已经给我们封装好了,但是Jeesite的官方文档只能说内容太少太少了,很多地方只能看他的源码才能知道它的流程,最近更新了一下项目,把以前的校验方法都删除变成了上面这种格式,下面说一下Jeesite是如何使用数据校验的:
首先pojo类还是需要我们写注解,错误信息文件操作照常,接下来是重点:在controller中不要使用@Validated对bean进行校验!!!,Jeesite有自己的错误检查框架,每一个controller都会继承了一个名为BaseController的类,一旦出错后他会拦截异常直接跳转到对应的方法,代码如下,你可以通过ctrl进去basecontroller进行查看:

/**
 * 参数绑定异常
 */
@ExceptionHandler({BindException.class, ConstraintViolationException.class, 
ValidationException.class})
public String bindException() {  
    return "error/400";
}

而这个拦截后就直接跳转到了400页面我们就无法和上面那样对错误信息进行操作了,所以我们应该采用thinkgem老哥给我们留下的另外一条路:

/**
 * 服务端参数有效性验证
 * @param object 验证的实体对象
 * @param groups 验证组
 * @return 验证成功:返回true;严重失败:将错误信息添加到 message 中
 */
protected boolean beanValidator(Model model, Object object, Class<?>... groups) {
    try{
        BeanValidators.validateWithException(validator, object, groups);
    }catch(ConstraintViolationException ex){
        List<String> list = BeanValidators.extractPropertyAndMessageAsList(ex, ": ");
        list.add(0, "数据验证失败:");
        addMessage(model, list.toArray(new String[]{}));
        return false;
    }
    return true;
}

/**
 * 服务端参数有效性验证
 * @param object 验证的实体对象
 * @param groups 验证组
 * @return 验证成功:返回true;严重失败:将错误信息添加到 flash message 中
 */
protected boolean beanValidator(RedirectAttributes redirectAttributes, Object object, Class<?>... 
groups) {
    try{
        BeanValidators.validateWithException(validator, object, groups);
    }catch(ConstraintViolationException ex){
        List<String> list = BeanValidators.extractPropertyAndMessageAsList(ex, ": ");
        list.add(0, "数据验证失败:");
        addMessage(redirectAttributes, list.toArray(new String[]{}));
        return false;
    }
    return true;
}

这两个方法都是可以进行数据校验的方法,只要在controller中调用然后根据需要选择使用哪一个就好:
需要跳转到另外一个页面的使用上面的那个,需要重定向到原来页面的使用下面的那个。
但是thinkgem老哥还是留了点小瑕疵,他的错误信息把我们的属性暴露出来了,所以我把他的信息方法改成了:

/**
 * 辅助方法, 转换Set<ConstraintViolation>为List<propertyPath +separator+ message>.
 */
@SuppressWarnings("rawtypes")
public static List<String> extractPropertyAndMessageAsList(Set<? extends ConstraintViolation> 
constraintViolations,
        String separator) {
    List<String> errorMessages = Lists.newArrayList();
    for (ConstraintViolation violation : constraintViolations) {
        //errorMessages.add(violation.getPropertyPath() + separator + 
violation.getMessage());
        errorMessages.add(violation.getMessage());
    }
    return errorMessages;
}

注释的那句是原来的,而我们只需要错误信息不需要属性名称,除非你知道你在做什么而且你觉得有必要你可以不改。
先码到这,后面有更多研究会进行修改。

什么是Supplier

supplier接口是JAVA8以后配合lambda表达式和函数式接口编程(FunctionInterface,以下简称FI)组合使用的一个接口,对外表现为双冒号"::",顺便说下"->"符号对应的是Function接口中的Reply方法例如:

Supplier<Person> persionSupplier = Person::new;
Arrays.asList("a","b","c").forEach(e->System.out.println(e));

其中的双冒号点击进去就是这个Supplier接口:

package java.util.function;

/**
 * Represents a supplier of results.
 *
 * <p>There is no requirement that a new or distinct result be returned each
 * time the supplier is invoked.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #get()}.
 *
 * @param <T> the type of results supplied by this supplier
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Supplier<T> {

/**
 * Gets a result.
 *
 * @return a result
 */
T get();
}

可以看到这份代码中,有一个比较奇特的注解@FunctionalInterface,这是一个函数式接口的声明。该注解不是必须的,如果一个接口符合"函数式接口"定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错。

Supplier的使用

我们可以从源代码中看出来,Supplier接口中只有一个方法,就是一个get方法且无需参数,返回的是泛型T。一般的类强制转换为一个接口编译器(eclipse)不会报错但是运行时会报错。假设现在我们新建一个普通类Person:

public class Person{
    Integer id;
    String name;
    public Integer getId() {
        return id;
    }
    public void setId(Integer id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    
    public Person() {
        this.name="mike";
                    System.out.println("创建了一个对象");
    }
    public Person(Integer x,String y) {
        // TODO Auto-generated constructor stub
        this.id = x;
        this.name=y;
    }
//        public String toString() {
//            return this.name;
//        }
    
}

Person类中只有基本的属性与get/set方法,最最常见的对象创建方法就是直接一个new,这是正确的我们也恰好使用的是这个特性

Person::new

在编译器中这句代码相当于声明一个Person类型的Supplier,返回一个Supplier<Person>接口,接下来是重点:使用get方法会执行Person类的无参构造方法,每一次使用get方法都会新建一个对象,所以不是Supplier保存了Person对象,而是需要使用时创建一个,在main中运行以下代码:

    Supplier<Person> persionSupplier = Person::new;
    System.out.println("-------------分割线-------------");
    System.out.println("supplier中的实体类地址为:"+persionSupplier.get().toString());
    persionSupplier.get().setName("coda");
    System.out.println("更改后supplier中的实体类地址为:"+persionSupplier.get().toString());

而输出的结果为:
-------------分割线-------------
创建了一个对象
supplier中的实体类地址为:Person@87aac27
创建了一个对象
创建了一个对象
更改后supplier中的实体类地址为:Person@3e3abc88

可以得知:
第一句代码并没有调用构造方法
第三句代码调用了构造方法所以给出了地址 87aac27
第四句代码也调用了构造方法所以他其实已经不是上面的那个地址为87aac27的Person对象而是一个新的对象了
第五句代码也调用了构造方法所以他是一个全新的对象,地址为:3e3abc88

lambda表达式与Supplier的组合使用

看了以上的同学可能会问,至于吗,我只是想新建一个对象而已,我一个new就能解决的事需要搞这么多花里花俏的?
冷静老哥。JAVA8说了是扩展对象的用法,而不是替代传统的new方式,以下贴代码,注释不需要管,但是也是个学习的东西所以顺便贴上去了,可以解除注释试一下输出:

List<Person> list = new ArrayList();  
list.add(new Person(1, "haha"));  
list.add(new Person(2, "rere"));  
list.add(new Person(3, "fefe"));   
//Map<Integer, Person> mapp = list.stream().collect(Collectors.toMap(Person::getId, Function.identity())); 
//Map<Integer, Person> mapp = list.stream().collect(Collectors.toMap(x -> x.getId(), x->x));
//System.out.println(mapp);
Map<Integer, String> map = list.stream().collect(Collectors.toMap(Person::getId, Person::getName)); 
//Map<Integer, String> map = list.stream().collect(Collectors.toMap(Person::getId, Person::getName,(x1,x2)->x1));  

以下的功能很简单,把list中的三个Person对象变成一个Map。Map是一个K-V形式的集合,他要求key不可以重复,所以当我们把Person中的构造方法的123改成相同的那就会出现报错,注意下面那句注释(x1,x2)->x1意味着当第一个和第二个参数的key一致时选用第一个元素,这样即算有两个相同的KEY也不会影响到代码的运行,->是什么我会在另外一篇博文中详细介绍,现在我们只关心::,也就是Supplier在这段代码中起什么作用。Person::getId, Person::getName这句话到底是什么意思,这个我们直接看map的输出就好:
{1=haha, 2=rere, 3=fefe}
看到这个结果我们就知道了Person::getId指的是使用Person对象中的ID作为键,使用Person::getName作为Map中元素的值,getId,getName指的是list中的元素使用的方法名字。虽然可能这段代码还是太过于复杂但是这是我觉得比较适合用来诠释JAVA8的操作拓展改变的所以想了一下还是用的这个例子。

总结

Supplier单独的使用并没有什么意义,但是如果要使用JAVA8的其他新特性例如lambda表达式,Function,stream,Collectors等则需要花点时间看一下,否则会发现同样是JAVA为啥他写的像外星文,我写的像是幼儿园的这种闹剧。

前言

ORM数据库,即对象关系映射数据库。ORM将对象模型表示的对象映射到基于SQL的关系模型数据库结构中。简单来说就是我们可以在java代码中新建一个个实体类并规定他们的关联关系,然后ORM就会帮我们映射到数据库中。Android中本身自带有MySQL,实际开发的时候其实也可以通过Android自带的SQLiteDatabase等接口来操作数据库。但是这样的操作在我看来对于移动端开发而言并不算友好,
因为操作比较繁琐。今天谈的LitePal框架是一个ORM框架,它是基于MySQL的Android数据库框架,也是Android大神郭霖开发的框架。学习Android的人一般会看《第一行代码》,这个框架在这本书的第二版也有提及。


集成大小

在说这个框架的使用方法之前,我觉得有必要提一下它的优点——轻量级。LitePal的jar只有100多KB,可以说是绝对的轻量级了。在使用这个框架之前我也有使用过realm框架。它也是一个ORM,但和LitePal的区别是它有自己独立的数据库结构,而没有使用MySQL。但是集成之后往往包大小会多出4MB左右,虽然官方也给出了这个问题的优化方案,就是区分cpu架构打包,但是我个人觉得这不是一个比较方便的做法。或许是我对这方面经验不足,有知道的可以交流一下。


集成

添加依赖:implementation 'org.litepal.android:core:1.6.1'


数据库配置文件和创建实体类

首先需要在app/src/main目录下新建一个assets目录,然后新建一个xml文件叫"litepal.xml",里面的结构如下

<?xml version="1.0" encoding="utf-8"?>
<litepal>
<dbname value = "WeChat"></dbname>
<version    value="1"></version>
<list>
    <mapping class="com.xcyoung.wechat.litepal.bean.User"></mapping>
    <mapping class="com.xcyoung.wechat.litepal.bean.Info"></mapping>
</list>
</litepal>

  • dbname标签:数据库名称
  • version标签:数据库版本号
  • list标签:里面声明的是我们要配置的映射模型类

映射模型类即实体类,也就相当于我们常说的javaBean,这个应该有和后台对接过的都会明白,这里就不详细说明了。但有一点需要注意的是实体类需要继承框架提供的父类DataSupport(在1.6及以下版本)。
这里配置文件的意思就是当我们每新建一个实体类然后在配置文件声明就相当于在自己的数据库当中新建了一个表。


数据库简单操作,增删改查

先新建一个实体类譬如User类:

public class User extends DataSupport {
      private String name;
      private int age;

      //省略get、set方法
}

增:

User user=new User();
user.setName("xcyoung");
user.setAge(20);
user.save();

因为User类继承了DataSupport,在保存的时候只需要调用其父类的save方法即可保存到数据库的User表当中。

删:
对于已存在的对象可以直接调用delete()的方法进行删除。也可以通过条件进行删除。

DataSupport.deleteAll(User.class,"age > ?",30);

查:
select()方法,对应SQL的select语句,指定查询哪一列或哪几列。
where()方法,对应where语句,为查询添加约束条件
order()方法,对应order by语句,为查询结果排序,desc:降序,asc:升序,默认为asc可不写。
以下是查询相关条件的user例子:

List<User> users=DataSupport.select("name","age").where("age > ?",30).find(User.class);

查询User表中年龄大于30岁的name和age这两列的数据。

改:
updateAll方法可根据指定的条件按照给定的实体对象更新数据库,如:

User user=new User();
user.setAge(10);
user.updateAll("name = ?","xcyoung");

将姓名叫xcyoung的所有数据的age字段更新为10
除此之外,LitePal也提供了一个saveOrUpdate()方法。这个方法的操作过程和上面的写操作类似,通过在该方法参数中添加条件约束,若按照条件该对象已存在,则不新建新的数据,而更新已有的数据。

User user=new User();
user.setAge(1);
user.saveOrUpdate("name = ?","xcyoung");

数据恢复初始化

java中String的初始值为null,int的初始值为0。如果在设置实体对象的值为这个类型的默认值是不可行的,LitePal提供了恢复初始值的方法setToDefault。调用此方法就可以将指定的字段恢复初始值了。

user.setToDefault("age");

其他

以上是LitePal操作的同步方法,其实它也为我们提供了异步方法,在方法名后添加Async即可。由于是异步操作,所以它提供了onFinish的方法作为操作完成后的回调。
LitePal还有一些关于表关联的操作,这些操作可以帮助我们简单的查询数据。有兴趣的可以去查阅文档。


最后

最后要说的是,就在发这篇文章的今天,郭霖的公众号发布了一篇文章,LitePal 2.0版本发布了。但是由于2.0对于实际开发的编写改动不大,主要是实体类继承的父类有所不同,所以我上述的是按照我最近使用的1.6版本的。按照发布文章的说法是他们将整个框架的大部分都重构了。以下是公众号的原文:
请输入链接描述