1.综述

总的来说,jeesite中hibernate的应用主要有2个方面,annotation和查询语句。前者主要是指定实体类与数据库表的各种关系,而后者则包括criteria,它以面向对象对方式来实现各种查询逻辑,以及HQL语句,hibernate自定的查询语句。

2.annotation

先说annotation,就拿User来举例,首先对与User类,有如下的实现:

@Entity
@Table(name = "sys_user")
@DynamicInsert @DynamicUpdate
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class User extends IdEntity<User> {
...
}

@Entity用来告诉系统,这个类是一个实体类,提醒系统将它们跟普通POJO类分开。
@Table指定该实体类所对应的数据库表名
@DynamicInsert和@DynamicUpdate两个注释主要目的是提升操作数据的效率。@DynamicUpdate说明在更新时只修改有改动的字段,否则数据库会修改该列的所有字段。@DynamicInsert则针对插入时。
@Cache指定一个数据库表的缓存,也是效率相关。

以上这部分是针对实体类本身的,比较简单,而且直接拓下来完全没问题。

@Id  
@GeneratedValue(strategy = GenerationType.IDENTITY) 
private Integer sys_id;

这句话在user类里没有用到,可是也特别常用。

@Id功能是指定主键。表示这个数据库表的主键是sys_id;
@GeneratedValue指定主键的生成方式,比如自增和手动等。

顺便一提,针对属性的注释可以选择在属性上或者在属性的get方法上。get方法会在把内容存入数据库这个过程中产生作用,而set方法会在把内容取出数据库这个过程中产生作用。

@ManyToOne
@JoinColumn(name="company_id")
@NotFound(action = NotFoundAction.IGNORE)
@JsonIgnore
@NotNull(message="归属公司不能为空")
@ExcelField(title="归属公司", align=2, sort=20)
public Office getCompany() {
    return company;
}

现在开始在难一点的annotation上,主要是在对@ManyToOne,@OneToMany以及@ManyToMany的理解上。

上述代码是User类的getCompany函数及其注释。对与User来说,一个User对应一个Company,而一个Company对应多个User。所以User和Company是多对一的关系。在数据库表中,这种关系的实现形式是在多的那方加入一的那方的外键。

例如User(id,company_id,...),Company(id,...)这种形式。

@ManyToOne就是告诉系统实体属性在数据库表里是以这种形式来组织的。

@JoinColum则是指定这个外键字段在数据库表里的名称。

@NotFound顾名思义,在找不到引用外键多时候采取什么行为,默认是抛出异常,这里采取的是忽略,意思就是允许User没有对应的Company。

@JsonIgnore标明该属性不会在Json进行序列化的时候包含在内。但是我还没有弄懂为什么User类会需要转换为Json。

@NotNull看名字大家都懂的。
@ExcelField自定义的注释,导出excel用的。

这样处理之后,当需要将User中的company存入数据库时,hibernate就会将company所属的另一个实体类office的属性存入office类所对应的数据库表中,并且与User表进行外键关联。

接下来跳过OneToMany,咱们直接来看ManyToMany:

@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role", joinColumns = { @JoinColumn(name = "user_id") }, inverseJoinColumns = { @JoinColumn(name = "role_id") })
@Where(clause="del_flag='"+DEL_FLAG_NORMAL+"'")
@OrderBy("id") @Fetch(FetchMode.SUBSELECT)
@NotFound(action = NotFoundAction.IGNORE)
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
@JsonIgnore
@ExcelField(title="拥有角色", align=1, sort=800, fieldType=RoleListType.class)
public List<Role> getRoleList() {
    return roleList;
}

@ManyToMany表示的是多对多的关系,一个用户可以有多个角色,一个角色也可以被多个用户所有。这种关系在数据库中一般是用一个关系数据库表来实现。

@JoinTable就是用来指定这个表的,在我们的系统中,user表和role表的关联表就是sys_user_role表,就在JoinTable的name字段里指定。其中joinColumns与inverseJoinColumns字段也是必须的字段,前者指定本类,也就是这里的User类在关联表中的字段名,既user_id,后者指的是对方类的字段名,这里便是role_id。

两个annotation组合起来,就构成了一个三张表的查询,当需要通过user查出rolelist的时候,先得到user_id,再在关系表中查询处多个role_id,最后取出多个role对象,返回List

以上便是数据库存取实体类的配置注释部分,通过这些注释,等同于构建了一个普通的java实体类,然后利用配置文件将之于某个数据库表关联起来,当然数据库多个表之间的关系也会反应在这个配置文件中。

3.查询方法

数据库配好了,我们可以轻松查询里面的数据了。不过hibernate提供了多种方法来进行查询,这些查询方法各有优劣。在jeesite里常用的查询方法有HQL,criteria,和普通SQL,criteria又分为普通criteria和DetachedCriteria。SQL的优点是学习成本低,反正大家多少懂一点,缺点是不能跨平台,换个数据库就不行了。HQL跟SQL差不多,但是hibernate进行了一部分封装,使用HQL就能够直接支持跨平台,缺点是查询语句要事先写好,动态性不强。criteria是面向对象的查询方法,能够动态的添加和删除条件。SQL基本不需要讲了,HQL其实也很简单,我会简单的列举jeesite中HQL的使用,criteria学习成本相对较高,接下来我主要会介绍这个部分。

首先看看HQL吧,jeesite的大部分DAO里,都有一个有一个具体的查询语句,调用find或者update函数。这些函数里通常有一段stirng字符串,这字符串是jeesite的作者自己加工的,最终这段字符串会被加工为HQL语句。

public List<Article> findByIdIn(String[] ids){
    return find("from Article where id in (:p1)", new Parameter(new Object[]{ids}));
}

这段代码的作用是根据Id查询出具体的Article实体,其中的string在find函数中被装配为hibernate.Query,然后通过query.list()函数返回查询结果。这里之所以用HQL的原因是这一查询要求使用频繁并且要求不会变化,所以直接写HQL语句,直观且高效。

criteria被实现在BaseDao类中,它是所有Dao类的基类。

public Page<T> find(Page<T> page, DetachedCriteria detachedCriteria)

上述函数就是使用critera的find函数,它以一个DetachedCriteria作为参数,之后引导查询。这样做的好处是当一个具体的service想执行特定的查询时,将会自己构建一个查询criteria,然后传给DAO里的find,就能进行想要的查询。

下面会以jeesite前台页面的主页面所用查询为例的梳理整个过程,该页面内容写在frontindex.jsp文件中。

<div class="span4">
    <h4>
        <small>
            <a href="${ctx}/list-2${urlSuffix}" class="pull-right">
            更多&gt;&gt;
            </a>
        </small>
        组织机构
    </h4>
    <ul>
        <c:forEach items="${fnc:getArticleList(site.id, 2, 8, '')}" var="article">
            <li>
                <span class="pull-right">
                    <fmt:formatDate value="${article.updateDate}" pattern="yyyy.MM.dd"/>
                </span>
                <a href="${article.url}" style="color:${article.color}">${fns:abbr(article.title,28)}
                </a>
            </li>
        </c:forEach>
    </ul>
</div>

这段代码中最重要的就是fnc:getArticleList(site.id, 2, 8, '')这个函数,整个函数的java实现在CmsUtils里。而这整个代码段的作用是从数据库中取出相应的8条记录,显示到登陆主页的展示框中。

public static List<Article> getArticleList(String siteId, String categoryId, int number, String param){
    Page<Article> page = new Page<Article>(1, number, -1);
    Article article = new Article(new Category(categoryId, new Site(siteId)));
    if (StringUtils.isNotBlank(param)){
        @SuppressWarnings({ "rawtypes" })
        Map map = JsonMapper.getInstance().fromJson("{"+param+"}", Map.class);
        if (new Integer(1).equals(map.get("posid")) || new Integer(2).equals(map.get("posid"))){
            article.setPosid(String.valueOf(map.get("posid")));
        }
        if (new Integer(1).equals(map.get("image"))){
            article.setImage(Article.YES);
        }
        if (StringUtils.isNotBlank((String)map.get("orderBy"))){
            page.setOrderBy((String)map.get("orderBy"));
        }
    }
    article.setDelFlag(Article.DEL_FLAG_NORMAL);
    page = articleService.find(page, article, false);
    return page.getList();
}

先看参数,第一个参数site.id参数不用关注,它的主页分为主网站和子网站,目前只有两种,这个ID是区分这两种网站的。第二个参数2是文章的分类id,从jsp文件可以看出这个div框所展现的是所有与组织机构相关的文章,数据库里也显示这一分类的分类ID是2。第三个参数是取出的文章个数,根据框的大小来设定,这里用的是8,也就是8条list。最后是预留参数,目前还没发现哪里在用。代码段里if之内的字段就是解析这一param之用,由于暂时没有使用所以不必关注。简单来讲,这个过程传入分类ID构建了一个新article类,传入数目构成了一个新的page类,然后将这两个类传到page = articleService.find(page, article, false)中。我们再来看articleService.find的代码。

public Page<Article> find(Page<Article> page, Article article, boolean isDataScopeFilter) {
    // 更新过期的权重,间隔为“6”个小时
    // 这部分我暂时未找到权重在哪里使用的,并且运行到现在所有权重都为0,不用更新,因此这部分可以不用关注。
    //目前看来应该是文章排序相关
    Date updateExpiredWeightDate =  (Date) CacheUtils.get ("updateExpiredWeightDateByArticle");
    if (updateExpiredWeightDate == null || (updateExpiredWeightDate != null && updateExpiredWeightDate.getTime() < new Date().getTime())){
        articleDao.updateExpiredWeight();
        CacheUtils.put("updateExpiredWeightDateByArticle", DateUtils.addHours(new Date(), 6));
    }
    // 通过articleDao创建了一个新的criteria
    // criteria的类别是Article,表示该criteria的返回值是Article
    DetachedCriteria dc = articleDao.createDetachedCriteria();

    // 在dc当中添加两个别名,所谓别名是在查询关联数据表的时候使用
    // hibernate会在Article实体类中寻找名为catergory的属性
    // 并在查询的时候将两个实体类关联到一起
    // Article与category的关系是一对多的关系
    // 他们在数据库里的关联字段是category_id
    // 这些都写在Article中get函数的annotation中
    dc.createAlias("category", "category");
    dc.createAlias("category.site", "category.site");

    //如果category存在并且合法
    if (article.getCategory()!=null && StringUtils.isNotBlank(article.getCategory().getId()) && !Category.isRoot(article.getCategory().getId())){
        
        //首先通过传入的id取出category的实体
        Category category = categoryDao.get(article.getCategory().getId());
        
        //接着再进行判断,如果成功取出,则开始构建存取逻辑
        if (category!=null){

            //最外层是一个或逻辑,逻辑1与逻辑2满足一个条件即可
            //接着是逻辑1:article中的category.id要与传入的分类id相等
            //接着是逻辑2:article中category的父类包含传入分类id
            //这一逻辑可以如下总结,对于我们传入的分类id,找到满足以下条件的文章
            //文章的分类与传入分类相同,或者文章的分类是传入分类的子类
            dc.add(Restrictions.or(
                    Restrictions.eq("category.id", category.getId()),
                    Restrictions.like("category.parentIds", "%,"+category.getId()+",%")));
        
            //然后添加第二个逻辑
            //文章所对应的网站id要与分类中的网站id相同
            //这一网站id也是由前台传入,保留在catergory中   
            dc.add(Restrictions.eq("category.site.id", category.getSite().getId()));

            //接着把已经取出的category存入article中
            article.setCategory(category);

        //如果不能取出category,说明数据库中不存在此分类?
        //我也不知道为何category合法却不能取出
        }else{
            //则只比较site,不比较category
            dc.add(Restrictions.eq("category.site.id", Site.getCurrentSiteId()));
        }

    //如果category不合法或者压根就不存在 
    }else{
        //还是只比较site就行了
        //我擦,category一旦有问题,解决方案就是把所有分类都取出来
       dc.add(Restrictions.eq("category.site.id", Site.getCurrentSiteId()));
    }

    //之后就是根据具体设置来添加逻辑
    //如果article的title存在
    //在我这条逻辑线里这东西不可能存在
    //在别的查询逻辑里可能存在title
    if (StringUtils.isNotEmpty(article.getTitle())){
        //那么找出包含此title的文章
        dc.add(Restrictions.like("title", "%"+article.getTitle()+"%"));
    }
    //如果posid存在
    //不知道是干什么的
    if (StringUtils.isNotEmpty(article.getPosid())){
        dc.add(Restrictions.like("posid", "%,"+article.getPosid()+",%"));
    }
    //如果图片地址存在
    if (StringUtils.isNotEmpty(article.getImage()) && Article.YES.equals(article.getImage())){

    //...
    dc.add (Restrictions.and (Restrictions.isNotNull ("image"), Restrictions.ne ("image","")));
    }
    //如果createby有记录
    if (article.getCreateBy()!=null && StringUtils.isNotBlank(article.getCreateBy().getId())){
        //...
        dc.add(Restrictions.eq("createBy.id", article.getCreateBy().getId()));
    }

    //如果需要进行范围过滤
    if (isDataScopeFilter){
        
        //再由dc创造两个别名,之前提到过别名是多表查询时使用的
        //第一个是别名涉及的是article->category->office
        //第二个就是article->createBy
        //注意offic实际上对应的是Office实体类
        //createBy对应的是User实体类
        dc.createAlias("category.office", "categoryOffice").createAlias("createBy", "createBy");
        
        //这里之后涉及office的查询实际上是三张表的查询了
        //文章表(1)->(N)分类表(N)->(1)部门表
        //各种关联由hibernate处理,我们不用处理细节
        //dataScopeFilter是权限过滤函数
        //它取出当前用户的所有角色
        //合并这些角色对应的权限
        //然后按照权限规则添加关于文章归属部门的查询条件
        dc.add(dataScopeFilter(UserUtils.getUser(), "categoryOffice", "createBy"));
    }
    dc.add(Restrictions.eq(Article.FIELD_DEL_FLAG, article.getDelFlag()));
    if (StringUtils.isBlank(page.getOrderBy())){
        //为dc添加排序规则
        dc.addOrder(Order.desc("weight"));
        dc.addOrder(Order.desc("updateDate"));
    }
    //最终交给DAO进行查询
    return articleDao.find(page, dc);
}

详细解析些在注释里了,其中包含了jeesite中criteria的最常见用法。