JFinal4.3 框架总结(三)

7 持久层——ActiveRecord

ActiveRecord 模式的核心是:一个 Model 对象唯一对应数据库表中的一条记录,而对应关系依靠的是数据库表的主键值。

因此,ActiveRecord 模式要求数据库表必须要有主键。当数据库表没有主键时,只能使用 Db + Record 模式来操作数据库。

JFinal的前端提交的formBean与数据库查询的JavaBean可以使用的是同一个Model对象,Model对象是业务对象,业务对象对应数据库的表,代码可以使用自带的代码生成器来生成;


7.1 常规配置

Model与数据库表的映射需要自己配置,这块可以使用代码生成器生成;这样就不需要XML来映射,也不需要注解来映射;

public class DemoConfig extends JFinalConfig {

  public void configPlugin(Plugins me) {

  DruidPlugin dp = new DruidPlugin("jdbc:mysql://localhost/db_name", "userName", "password");

    me.add(dp);

    ActiveRecordPlugin arp = new ActiveRecordPlugin(dp); // 设置数据源

    me.add(arp);

    arp.addMapping("user", User.class); // 绑定数据库Table与Model对象的映射

    arp.addMapping("article", "article_id", Article.class); // 绑定可以指定主键ID,默认是id;

  }

}


7.2 Model

Model是ActiveRecord中最重要的组件之一,它充当MVC模式中的Model部分,即是JavaBean又是DAO;

常见用法:

public class User extends Model<User> {

    public static final User dao = new User().dao();

}


// 创建name属性为James,age属性为25的User对象并添加到数据库

new User().set("name", "James").set("age", 25).save();

// 删除id值为25的User

User.dao.deleteById(25);

// 查询id值为25的User将其name属性改为James并更新到数据库

User.dao.findById(25).set("name", "James").update();

// 查询id值为25的user, 且仅仅取name与age两个字段的值

User user = User.dao.findByIdLoadColumns(25, "name, age");

// 获取user的name属性

String userName = user.getStr("name");

// 获取user的age属性

Integer userAge = user.getInt("age");

// 查询所有年龄大于18岁的user

List<User> users = User.dao.find("select * from user where age>18");

// 分页查询年龄大于18的user,当前页号为1,每页10个user

Page<User> userPage = User.dao.paginate(1, 10, "select *", "from user where age > ?", 18);


注意:User中定义的 public static final User dao对象是全局共享的,只能用于数据库查询,不能用于数据承载对象。数据承载需要使用new User().set(…)来实现。


7.3 代码生成

ActiveRecord 模块的 com.jfinal.plugin.activerecord.generator 包下,提供了一个 Generator 工具类,可自动生成 Model、BaseModel、MappingKit、DataDictionary 四类文件。

生成后的 Model 与 java bean 合体,立即拥有了 getter、setter 方法,使之遵守传统的 java bean 规范,立即拥有了传统 JavaBean 所有的优势,开发过程中不再需要记忆字段名。


Model与Bean合体后注意事项

合体后JSP模板输出Bean中的数据将依赖其getter方法,输出的变量名即为getter方法去掉”get”前缀字符后剩下的字符首字母变小写,如果希望JSP仍然使用之前的输出方式,可以在系统启动时调用一下ModelRecordElResolver. setResolveBeanAsModel(true);

Controller之中的getModel()需要表单域名称对应于数据表字段名,而getBean()则依赖于setter方法,表单域名对应于setter方法去掉”set”前缀字符后剩下的字符串字母变小写。

许多类似于jackson、fastjson的第三方工具依赖于Bean的getter方法进行操作,所以只有合体后才可以使用jackson、fastjson

JFinalJson将Model转换为json数据时,json的keyName是原始的数据表字段名,而jackson、fastjson这类依赖于getter方法转化成的json的keyName是数据表字段名转换而成的驼峰命名

建议mysql数据表的字段名直接使用驼峰命名,这样可以令json的keyName完全一致,也可以使JSP在页面中取值时使用完全一致的属性名。注意:mysql数据表的名称仍然使用下划线命名方式并使用小写字母,方便在linux与windows系统之间移植。

总之,合体后的Bean在使用时要清楚使用的是其BaseModel中的getter、setter方法还是其Model中的get(String attrName)方法


7.4 Db+Record模式

使用Db与Record类时,无需对数据库表进行映射,Record相当于一个通用的Model;Db+Record可以填补Model方式无法满足的功能;

以下为Db + Record模式的一些常见用法:

// 创建name属性为James,age属性为25的record对象并添加到数据库

Record user = new Record().set("name", "James").set("age", 25);

Db.save("user", user);

 

// 删除id值为25的user表中的记录

Db.deleteById("user", 25);

 

// 查询id值为25的Record将其name属性改为James并更新到数据库

user = Db.findById("user", 25).set("name", "James");

Db.update("user", user);

 

// 获取user的name属性

String userName = user.getStr("name");

// 获取user的age属性

Integer userAge = user.getInt("age");

 

// 查询所有年龄大于18岁的user

List<Record> users = Db.find("select * from user where age > 18");

 

// 分页查询年龄大于18的user,当前页号为1,每页10个user

Page<Record> userPage = Db.paginate(1, 10, "select *", "from user where age > ?", 18);


 以下为事务处理示例:

boolean succeed = Db.tx(new IAtom(){

    public boolean run() throws SQLException {

       int count = Db.update("update account set cash = cash - ? where id = ?", 100, 123);

       int count2 = Db.update("update account set cash = cash + ? where id = ?", 100, 456);

       return count == 1 && count2 == 1;

}});


Db.find(...) 系与 Db.query(...)/Db.queryXxx(...) 系的区别

前者将返回值一律封装到一个 Record 对象中,而后者不封装,只将数据原样返回。查询所使用的 sql 与参数用法完全一样。


7.5 分页查找

接口

paginate(int pageNumber, int pageSize, String select, String sqlExceptSelect, Object... paras)

paginate(int pageNumber, int pageSize, boolean isGroupBySql, String select, String sqlExceptSelect, Object... paras)

paginateByFullSql(int pageNumber, int pageSize, String totalRowSql, String findSql, Object... paras)


将sql语句的select部分、sql语句除了select以外的部分分开,是为了生成select count(*) ...语句;

区分groupby是为了在生成select count(*) ...的时候不会出错,当传入的是一个统计的SQL,去生成count的sql容易出错,所以,要区分;


重点:paginateByFullSql 最关键的地方是 totalRwoSql、findSql 这两条 sql 要能够共用最后一个参数 Object... paras,相当于 dao.find(totalRwoSql, paras) 与 dao.find(findSql, paras) 都要能正确执行,否则断然不能使用 paginateByFullSql。


7.6 事务管理

7.6.1 Db.tx

Java 8 的 lambda 语法使用示例:

Db.tx(() -> {

  Db.update("update t1 set f1 = ?", 123);

  Db.update("update t2 set f2 = ?", 456);

  return true; // return true 提交事务,return false 则回滚事务

});

Db.tx 方法 默认针对主数据源 进行事务处理,如果希望对其它数据源开启事务,使用 Db.use(configName).tx(...) 即可。此外,Db.tx(...) 还支持指定事务级别:

Db.tx(Connection.TRANSACTION_SERIALIZABLE, () -> {

  Db.update(...);

  new User().setNickName("james").save();

  return true;

});

 以上代码中的 Db.tx(...) 第一个参数传入了事务级别参数  Connection.TRANSACTION_SERIALIZABLE,该方法对于需要灵活控制事务级的场景十分方便实用。

 注意:MySql数据库表必须设置为InnoDB引擎时才支持事务,MyISAM并不支持事务。


7.6.2 声明式事务

ActiveRecord支持声明式事务,声明式事务需要使用ActiveRecordPlugin提供的拦截器来实现;以下代码是声明式事务示例:

// 本例仅为示例, 并未严格考虑账户状态等业务逻辑

@Before(Tx.class)

public void trans_demo() {

    // 获取转账金额

    Integer transAmount = getParaToInt("transAmount");

    // 获取转出账户id

    Integer fromAccountId = getParaToInt("fromAccountId");

    // 获取转入账户id

    Integer toAccountId = getParaToInt("toAccountId");

    // 转出操作

    Db.update("update account set cash = cash - ? where id = ?",

 transAmount, fromAccountId);

    // 转入操作

    Db.update("update account set cash = cash + ? where id = ?",

 transAmount, toAccountId);

}


特别注意:声明式事务默认只针对主数据源进行回滚,如果希望针对 “非主数据源” 进行回滚,需要使用注解进行配置,以下是示例:

@TxConfig("otherConfigName")

@Before(Tx.class)

public void doIt() {

   ...

}

Tx 拦截器是通过捕捉到异常以后才回滚事务的,所以上述代码中的 doIt() 方法中如果有 try catch 块捕获到异常,必须再次抛出,才能让 Tx 拦截器感知并回滚事务。


JDBC 默认的事务级别为:Connection.TRANSACTION_READ_COMMITTED。为了避免某些同学的应用场景下对事务级别要求较高,jfinal 的 ActiveRecordPlugin 默认使用的是 Connection.TRANSACTION_REPEATABLE_READ,但这在对某个表存在高并发锁争用时性能会下降,这时可以通过配置事务级别来提升性能:

public void configPlugin(Plugins me) {

    ActiveRecordPlugin arp = new ActiveRecordPlugin(...);

    arp.setTransactionLevel(Connection.TRANSACTION_REPEATABLE_READ);

    me.add(arp);

}


7.6.3 Cache 缓存

ActiveRecord 可以使用缓存以大大提高性能,默认的缓存实现是 ehcache,使用时需要引入 ehcache 的 jar 包及其配置文件,以下代码是Cache使用示例:

public void list() {

    List<Blog> blogList = Blog.dao.findByCache("cacheName", "key", "select * from blog");

    setAttr("blogList", blogList).render("list.html");

}

上例findByCache方法中的cacheName需要在ehcache.xml中配置如:<cache name="cacheName" …>。此外Model.paginateByCache(…)、Db.findByCache(…)、Db.paginateByCache(…)方法都提供了cache支持。在使用时,只需传入cacheName、key以及在ehccache.xml中配置相对应的cacheName就可以了。


说明:除了要把使用默认的 ehcache 实现以外,还可以通过实现 ICache 接口切换到任意的缓存实现上去,调用ActiveRecordPlugin的setCache方法设置自定义缓存;


7.6.4 方言Dialect

目前ActiveRecordPlugin提供了MysqlDialect、OracleDialect、PostgresqlDialect、SqlServerDialect、Sqlite3Dialect、AnsiSqlDialect实现类。调用ActiveRecordPlugin的setDialect方法进行设置;


7.6.5 表关联查询

以前使用JavaBean的时候,通常需要添加扩展字段来保存另一个表的字段,这样数据库结果集合才能映射到JavaBean;

JFinal不需要这样处理,JFinal的Model、Record都是Map的实现,直接会放进去,有对应的getXxx来获取;

对Model来说,还有另一种方式:

public class Blog extends Model<Blog>{

    public static final Blog dao = new Blog().dao();

    

    public User getUser() {

       return User.dao.findById(get("user_id"));

    }

}

 

public class User extends Model<User>{

    public static final User dao = new User().dao();

    

    public List<Blog> getBlogs() {

       return Blog.dao.find("select * from blog where user_id=?", get("id"));

    }

}

这种方式类似Hibernate与Mybatis,对象关联查询;


7.6.6 复合主键

JFinal ActiveRecord 从 2.0 版本开始,采用极简设计支持复合主键,对于 Model 来说需要在映射时指定复合主键名称,以下是具体例子:

ActiveRecordPlugin arp = new ActiveRecordPlugin(druidPlugin);

// 多数据源的配置仅仅是如下第二个参数指定一次复合主键名称

arp.addMapping("user_role", "userId, roleId", UserRole.class);

 

//同时指定复合主键值即可查找记录

UserRole.dao.findByIds(123, 456);

 

//同时指定复合主键值即可删除记录

UserRole.dao.deleteByIds(123, 456);


对于 Db + Record 模式来说,复合主键的使用不需要配置,直接用即可:

Db.findByIds("user_role", "roleId, userId", 123, 456);

Db.deleteByIds("user_role", "roleId, userId", 123, 456);


7.6.7 Oracle支持

public class DemoConfig extends JFinalConfig {

  public void configPlugin(Plugins me) {

    DruidPlugin dp = new DruidPlugin(……);

    me.add(dp);

    //配置Oracle驱动

    dp.setDriverClass("oracle.jdbc.driver.OracleDriver");

    

    ActiveRecordPlugin arp = new ActiveRecordPlugin(dp);

    me.add(arp);

    // 配置Oracle方言

    arp.setDialect(new OracleDialect());

    // 配置属性名(字段名)大小写不敏感容器工厂

    arp.setContainerFactory(new CaseInsensitiveContainerFactory());

    arp.addMapping("user", "user_id", User.class);

  }

}


由于Oracle数据库会自动将属性名(字段名)转换成大写,所以需要手动指定主键名为大写,如:arp.addMaping(“user”, “ID”, User.class)。如果想让ActiveRecord对属性名(字段名)的大小写不敏感可以通过设置CaseInsensitiveContainerFactory来达到,有了这个设置,则arp.addMaping(“user”, “ID”, User.class)不再需要了。


7.6.8 SQL 模板与查询

JFinal利用自带的 Enjoy Template Engine 极为简洁的实现了 Sql 模板管理功能。一如既往的极简设计,仅有 #sql、#para、#namespace 三个指令,学习成本依然低到极致。


7.6.9 调用存储过程

使用工具类 Db 可以很方便调用存储过程,以下是代码示例:

Db.execute((connection) -> {

    CallableStatement cs = connection.prepareCall(...);

    cs.setObject(1, ...);

    cs.setObject(2, ...);

    cs.execute();

    cs.close();

    return cs.getObject(1);

});

如上所示,使用 Db.execute(...) 可以很方便去调用存储过程,其中的 connection 是一个 Connection 类型的对象,该对象在使用完以后,不必 close(),因为 jfinal 在上层会默认帮你 close() 掉。


此外,MySQL 之下还可以使用更简单的方式调用存储过程:

// 调用存储过程,查询 salary 表

Record first = Db.findFirst("CALL FindSalary (1,\"201901\")");

 

// 调用存储过程,插入 salary 表

int update2 = Db.update("CALL InsertSalary (3, 123)");

 

// 调用存储过程,更新 salary 表

int update = Db.update("CALL UpdateSalary (3, 99999)");

 

// 调用存储过程,删除 salary 表

int delete = Db.delete("CALL DeleteSalary(3)");


7.6.10 其他问题

ResultSet转Record

RecordBuilder类负责进行转换,将ResultSet以key是字段名,value是JDBC类型放入Map中,这个Map放在Record对象内;

对特殊的类型CLOB、BLOB、NCLOB做了处理,CLOB对应String,BLOB对应byte[],NCLOB对应String;


ResultSet转Model

转换方式与ResultSet转Record是一样的,也是把JDBC类型放入Map中;Model里面保存的就是Map;


多数据源的处理

多数据源有多个ActiveRecordPlugin,对应多个DataSource,对应多个DbPro对象,每个数据库的Table与Model的映射是独立配置的;

DbKit维护了configName与Config的映射关系;

DbKit维护了Model与Config的映射关系;

TableMapping单例对象管理这个系统的Table与Model的映射(key是Model,value是Table);

问题:

1)Table与Model的映射是否有问题?

Table与Model是一一映射,是系统启动时候配置进去的;

由于TableMapping目前设计是单例,所以,不同数据库映射同一个Model,要求不同数据库的表结构以及类型完全一致,表名也要完全一致;

否则,请使用不同的Model对象来映射!


2)Model与数据库配置Config的映射是否有问题?

Model使用DbKit来根据configName获取Config,可以使用use方法传入configName来切换不同数据库;

如果Model没有传入configName,则根据当前Model的Class对象,从DbKit中获取Config来切换不同数据库;


3)Db+Record如何切换数据库?

可以使用Db.use()来切换不同的数据库来操作,use方法可以获取对应的DbPro对象;


之前写的一篇数据库框架对比文章,参考:http://blog.sina.com.cn/s/blog_667ac0360102xasd.html


8 表单验证——Validator

声明格式:public abstract class Validator implements Interceptor


Validator本身是一个拦截器,在Controller执行前被拦截,大部分字段校验方法已经在Validator中做了实现;

子类需要继承Validator,实现校验方法validate,校验失败的处理方法handleError,验证失败会调用handleError方法,可以立即返回结果给客户端;

V4.2以后提供了setRet方法可以把验证结果设入Ret内,默认是调用设在controller的属性内的,也就是request的属性内;


当默认校验字段的方法不够用时,支持使用正则表达式验证;如果还不行,最后就使用Java代码自己去验证吧!使用addError将验证失败的结果进行存储;


默认情况下,Validator在碰到验证失败时,还会向后继续验证剩下的字段;如果想使用短路验证,可以调用setShortCircuit(true)进行设置;短路验证失败时,会立即执行handleError方法;


至此,表单的后端校验功能已经完整了,可以适应所有场景!简单而完美!

当验证成功,那么继续执行下面的拦截器。


9 国际化

JFinal 为国际化提供了极速化的支持,国际化模块仅三个类文件,使用方式要比spring这类框架容易得多。

参考:http://www.jfinal.com/doc/11-2


10 缓存——EhCache

EhCachePlugin是JFinal集成的缓存插件,通过使用EhCachePlugin可以提高系统的并发访问速度。EhCache可以对action请求做缓存,以及数据库查询做缓存,还有用户自定义的一些缓存;


10.1 配置

示例代码:

public class DemoConfig extends JFinalConfig {

  public void configPlugin(Plugins me) {

    me.add(new EhCachePlugin());

  }

}


10.2 使用注解

CacheInterceptor可以将action所需数据全部缓存起来,下次请求到来时如果cache存在则直接使用数据并render,而不会去调用action。

@Before(CacheInterceptor.class)

public void list() {

    List<Blog> blogList = Blog.dao.find("select * from blog");

    User user = User.dao.findById(getParaToInt());

    setAttr("blogList", blogList);

    setAttr("user", user);

    render("blog.html");

}


上例中的用法将使用actionKey作为cacheName,在使用之前需要在ehcache.xml中配置以actionKey命名的cache如:<cache name="/blog/list" …>,注意actionKey作为cacheName配置时斜杠”/”不能省略。此外CacheInterceptor还可以与CacheName 注解配合使用,以此来取代默认的actionKey作为cacheName,以下是示例代码:

@Before(CacheInterceptor.class)

@CacheName("blogList")

public void list() {

    List<Blog> blogList = Blog.dao.find("select * from blog");

    setAttr("blogList", blogList);

    render("blog.html");

}

以上用法需要在ehcache.xml中配置名为blogList的cache如:<cache name="blogList" …>。


EvictInterceptor可以根据CacheName注解自动清除缓存。以下是示例代码:

@Before(EvictInterceptor.class)

@CacheName("blogList")

public void update() {

    getModel(Blog.class).update();

    redirect("blog.html");

}

上例中的用法将清除cacheName为blogList的缓存数据,与其配合的CacheInterceptor会自动更新cacheName为blogList的缓存数据。

jfinal 3.6 版本支持 @CacheName 参数使用逗号分隔多个 cacheName,方便针对多个 cacheName 进行清除,例如:

@Before(EvictInterceptor.class)

@CacheName("blogList, hotBlogList")   // 逗号分隔多个 cacheName

public void update() {

   ...

}


10.3 使用工具类

CacheKit 中最重要的两个方法是get(String cacheName, Object key)与put(String cacheName, Object key, Object value)。get方法是从cache中取数据,put方法是将数据放入cache。参数cacheName与ehcache.xml中的<cache name="blog" …>name属性值对应;参数key是指取值用到的key;参数value是被缓存的数据。


11 Json

对FastJson、Jackson做了实现,还提供了自己的实现JFinalJson,实现了序列化toJson、反序列化Parse接口;

还有一种混合模式的实现版本MixedJson,在序列化的时候使用JFinalJson,反序列化使用FastJson;

web环境下,默认使用JFinalJson实现,可以在configConstant中指定JsonFactory;

非web环境下,使用JsonManager来指定JsonFactory;

FastJson、Jackson的序列化都依赖于 Model、java bean的getter方法,JFinalJson不依赖于getter方法;


工具类

JsonKit

个人建议:所有Model都生成get/set支持普通JavaBean,Json统一使用FastJson,json转换不要搞太复杂!


12 Log

对log4j、jdklog做了实现,默认使用log4j;

使用Log.getLog(Class clazz)或Log.getLog(String name)的方式获得Log对象;


iPan

2019-07-17


评论区

北流家园网

2019-07-18 12:21

不是有文档了吗?没看出有什么新意

rctmlb

2019-07-18 14:29

总结概要很好,方便学习回顾

热门分享

扫码入社