视觉角度: jfinal的Model与Beetlsql比较

JFinal 是目前在 git.oschina.net java中关注最多的项目.

亲自用JFinal开发过有上百张表的项目.项目完结后总要做个总结

这篇文章是介绍项目中开发的一些经历.

  1. 会首先列出JFinal的Model开发 (只是Model的使用)
  2. 使用Beetlsql替换JFinal的Model (给出一种较爽编码方式)
  3. 对比两个在写多条件查询sql时, (就是需要条件判断, 为null的不参与查询)

1.JFinal的Model开发

JFinal的ORM下面称JFinal的Model

这是Model的新增 修改 删除

jfinal 实体类1

public class Elephant extends Model<Elephant> {
    public static final Elephant Dao = new Elephant();
    public List<Integer> findIds() {
        return Db.query("select id from tb_bird");
    }
}

jfinal 测试用例1

@Log4j
public class ElephantTest {
    @Test
    public void testChainSave() {
        // 无需要创建字段
        new Elephant().set("age", 17).set("name", "jfinal").set("desc", "mvc + orm").save();
    }

    @Test
    public void testUpdate() {
        Elephant elephant = Elephant.Dao.findById(1);
        elephant.set("age", 18).update();
    }

    @Test
    public void testDelete() {
        Elephant elephant = Elephant.Dao.findById(1);
        elephant.delete();
    }
}

    说实话,刚开始看示例的时候感觉很不错. 毕竟不需要自己定义任何字段就能直接将数据保存到表中.

    刚开始也使用这种方式进行开发, 但是如果有多个地方使用了这样的代码, 并且产品开发过程中需要更改某几个字段,那么小噩梦就开始了. 毕竟没有工具可以进行这种检测. 表字段名直接写成字符串也都是合法的.那么唯一可以做的就只能搜索整个项目使用到这个类的地方, 然后逐个检查而且需要看得很细. 因为这是人工的检查,所以工具并不能帮上你很大忙.

还有一点是采用这种方式开发就是你必须记住每个表的每个字段, 并且不能出错(需要手动在代码中敲击数据库表字段的名字).

如果是几张表并且字段少的情况下这样玩代码也没有什么. 如果是百张表的项目你会工作得很愉快.

所以就约定了实体类代码必须提供getter,setter. 这样做工具可以提供错误检测

于是项目中的实体类就变成了下面的方式

jfinal 实体类2 getter, setter

public class Elephant extends Model<Elephant> {
    public static final Elephant Dao = new Elephant();

    public int getId() {
        return this.getInt("id");
    }

    public Elephant setId(int id) {
        this.set("id", id);
        return this;
    }

    public int getAge() {
        return this.getInt("age");
    }

    public Elephant setAge(int age) {
        this.set("age", age);
        return this;
    }

    public String getName() {
        return this.getStr("name");
    }

    public Elephant setName(String name) {
        this.set("name", name);
        return this;
    }

    public String getDesc() {
        return this.getStr("desc");
    }

    public Elephant setDesc(String desc) {
        this.set("desc", desc);
        return this;
    }

    public List<Integer> findIds() {
        return Db.query("select id from tb_bird");
    }
}

实体类代码提供了getter,setter (为了继续保持链式风格在setter都返回了自身). 这样做工具可以提供错误检测, 看起来这样很不错了.

jfinal 测试用例2

这是添加getter,setter后的测试用例

@Log4j
public class ElephantChainTest {
    @Test
    public void testChainSave() {
        new Elephant().setAge(17).setName("jfinal").setDesc("mvc + orm").save();
    }

    @Test
    public void testUpdate() {
        Elephant elephant = Elephant.Dao.findById(1);
        elephant.setAge(18).update();
    }

    @Test
    public void testDelete() {
        Elephant elephant = Elephant.Dao.findById(1);
        elephant.delete();
    }
}

    好了, Model实体类都拥有了getter, setter方法了. 即便表业务需要改一个字段的名字也没什么大碍, 我就改这个实体类对应的方法名就行. 工具会给我们报错在代码的哪一行使用到了这个属性(字段). 并且也不需要使用到这个实体类的地方都手动敲击这个字段名了. 很不错. 方便了很多!

    但是我要告诉你们的是这些getter, setter代码都是人工手动敲上去的. 当初是想用JFinal的Model节省时间的,毫无疑问的是这样做并没有节省我的时间,而且还浪费了很多时间.

这里只是其中一个表对应的实体类(字段也很少, 因为是举例这里只用了4个字段). 如果百个实体类而有的实体类有8个,15个甚至更多属性(是的,你没看错, 你会越来越愉快).

    上面说到为什么这些代码是人工手动敲上去的. 工具不是可以生成getter,setter吗. 我想说的的确是可以生成, 但是工具生成这些代码是有前提条件的. 这些条件就是你必须显示的拥有类的成员属性.

所以很遗憾, 你享受不到工具带来的便捷

    好了, 代码约定Model实体类都必须提供getter, setter方法. 那么代码总要加上点注释吧. 于是代码就变成如下:

public class Elephant extends Model<Elephant> {
    public static final Elephant Dao = new Elephant();

    /**
     * @return 年龄
     */
    public int getAge() {
        return this.getInt("age");
    }

    /**
     * @param age 年龄
     * @return this
     */
    public Elephant setAge(int age) {
        this.set("age", age);
        return this;
    }

    /**
     * @return 名字
     */
    public String getName() {
        return this.getStr("name");
    }

    /**
     * @param name 名字
     * @return this
     */
    public Elephant setName(String name) {
        this.set("name", name);
        return this;
    }

    /**
     * @return 描述
     */
    public String getDesc() {
        return this.getStr("desc");
    }

    /**
     * @param desc 描述
     * @return this
     */
    public Elephant setDesc(String desc) {
        this.set("desc", desc);
        return this;
    }

    public List<Integer> findIds() {
        return Db.query("select id from tb_bird");
    }
}

    这样从一个最初只有1行代码的实体类, 最后变成了这样. 如果你在看这篇文字, 感觉变成了这样并有没什么. 但是你想知道变成拥有getter, setter加上注释花了多少时间吗? (请读者自行建立一个拥有8个字段的实体玩下).

我在想我为什么要花如此多的时间创建在model上 (我这样安慰我自己的, 这也属于有氧运动吧).

JAVA 极速WEB+ORM框架 JFinal

JFinal 是基于 Java 语言的极速 WEB + ORM 框架,其核心设计目标是开发迅速、代码量少、学习简单、功能强大、轻量级、易扩展、Restful。在拥有Java语言所有优势的同时再拥有ruby、python等动态语言的开发效率!为您节约更多时间,去陪恋人、家人和朋友 ;)

    块引用中的开发迅速与代码量少.没有明确的说是JFianl的代码量少还是使用JFinal开发项目的代码量少. 至少我认为项目中的Model如果显示提供getter,setter代码量就已经巨大且编码效率极慢(仅是JFinal的ORM给我的感觉. 也并不完全是感觉, 毕竟上面的代码示例包含了我想说的话了).

显示提供也不是,不显示提供也不是. 两难.

2.使用Beetlsql替换JFinal的Model

beetlsql + java8 + lombok 结合使用,达到代码最简洁效果, 并且可阅读性更强 (下面称为第二种编码方式)

(极简版)实体类1

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)  // 属性默认都是private
@lombok.experimental.Accessors(chain = true) // 链式编程
@Table(name = "tb_bird")                     // 实体类与表映射
public class Bee implements $Sql { // 实现$Sql接口, 可以在对象上直接使用 save, update, delete 方法 (不是必须的)
    int id;
    /** 年龄 */
    Integer age;
    /** 名字 */
    String name;
    /** 描述 */
    String desc;
}

lombok.@Data自动提供getter,setter,toString等方法. 所以并不需要在代码中显示的生成getter,setter方法(方法上都会有注释), 这让实体类更加的干净.

上面的示例中实体类只需要实现接口,而不需要任何继承就能达到想要的效果.(Effective Java 第18条:接口优于抽象类)

测试类

@Log4j
public class BeeChainTest {
    @Test
    public void testChainSave() {
        new Bee().setAge(17).setName("beetl").setDesc("beetlsql + orm").save();
    }

    @Test
    public void testUpdate() {
        Bee bee = Bee.Dao.$.unique(1);
        bee.setAge(18).update();
    }

    @Test
    public void testDelete() {
        Bee bee = Bee.Dao.$.unique(1);
        bee.delete();
    }
}

    可以看出, 就ORM方式而言. 我更倾向于后面这种结合.

  1. 自动拥有getter setter, 在也不需要手工敲击(有氧运动)
  2. 代码更加清晰,简洁, 甚至可以说真正的做到了代码量少. 让你开发更迅速.

    在上面JFinal的Model中和下面一种方式, 如果我们都在两个实体类中在追加1个字段(并加上注释), 第二种方式无疑是最方便的.

    因为JFinal的Model添加一个字段的getter setter并加上注释 需要14行代码, 而第二种方式只需要两行(可以在上面给出的示例中自己数).也就是说实体类每增加一个字段, 你的Model类相比使用第二种方式多出12行代码(7倍代码量, 这还是在你手工敲击getter setter名字完全正确率100%的情况下, getName getNema不出现这种拼写意外).

    所以为了防止这种意外你也许会说我可以在Model里面先定义属性并确定类型, 然后使用工具生成getter setter经过部分改动接着在把这个定义的属性删除掉就行了(这样就不会产生拼写意外, 而且能享受工具生成代码的便捷). 我想说的是你很幽默. 并且我想对你说的是曾经我也这样幽默过.

    说实话假设类有10个字段. 那么一个是140代码量和一个是20行代码量的,你更愿意看见哪一个? 不知道你们是怎么想的,我看140行代码的时候, 我还得在工具里上下翻滚.而第二种方式我一眼就能明白是怎么回事.

如果字段改个名字那么Model需要改动的地方有7处(换句话说就是维护7处). 第二种方式只需要改1处. (这里仅计算实体类里面的改动, 暂不考虑被项目中其他地方引用该实体类的地方)

beetlsql的链式编程

有一个坏消息是beetlsql在发布这篇文章的时候还不能支持链式编程

作者( @闲大赋 )在论坛上是这样回复的:

因为setter方法这种写法不是Javabean的标准写法,所以beetl无法认定这是一个属性,因此才会有刚才那种生成的sql语句

beetlsql会在下个版本支持读取field而不是getter,setter,不过需要点时间

当然有坏消息就会有好消息. 细心的朋友会发现将来的版本中作者会提供

3.对比两个在写多条件查询sql时

    当然,如果仅仅是从上面的Model做对比还是无法吸引你的话,那么这一节中就要拿出Beetlsql锋利的一面了(这里不会列举得很详细,只是简单的几个方法介绍).更具体的得到beetlsql官网上查阅资料.

有多个条件参数(就是需要条件判断, 为null的不参与查询)

下面各举三个方法执行同样的sql查询做比较

  • 查询所有id - findIds
  • 三个条件参数来查询单个对象 - findOne
  • 两个条件参数来查询一个对象列表 - finds

jfinal Model类现在的代码加上这三个查询后的代码

public class Elephant extends Model<Elephant> {
    public static final Elephant Dao = new Elephant();

    /**
     * @return 年龄
     */
    public int getAge() {
        return this.getInt("age");
    }

    /**
     * @param age 年龄
     * @return this
     */
    public Elephant setAge(int age) {
        this.set("age", age);
        return this;
    }

    /**
     * @return 名字
     */
    public String getName() {
        return this.getStr("name");
    }

    /**
     * @param name 名字
     * @return this
     */
    public Elephant setName(String name) {
        this.set("name", name);
        return this;
    }

    /**
     * @return 描述
     */
    public String getDescription() {
        return this.getStr("description");
    }

    /**
     * @param description 描述
     * @return this
     */
    public Elephant setDescription(String description) {
        this.set("description", description);
        return this;
    }

    public List<Integer> findIds() {
        return Db.query("select id from tb_bird");
    }

    public Elephant findOne(int age, String name, String description) {
        StringBuilder sql = new StringBuilder("select * from tb_bird where 1 = 1");
        List<Object> pars = new LinkedList<>();

        if (age != 0) {
            sql.append(" and age = ? ");
            pars.add(age);
        }

        if (name != null) {
            sql.append(" and name = ? ");
            pars.add(name);
        }

        if (description != null) {
            sql.append(" and description = ? ");
            pars.add(description);
        }

        return this.findFirst(sql.toString(), pars.toArray());
    }

    public List<Elephant> finds(int age, String name) {
        StringBuilder sql = new StringBuilder("select * from tb_bird where 1 = 1");
        List<Object> pars = new LinkedList<>();

        if (age != 0) {
            sql.append(" and age = ? ");
            pars.add(age);
        }

        if (name != null) {
            sql.append(" and name = ? ");
            pars.add(name);
        }

        return Db.query(sql.toString(), pars.toArray());
    }
}

jfinal Model查询测试用例

@Log4j
public class ElephantFindTest {
    @Before
    public void bf() {
        Config.jfinalInit();
    }

    @Test
    public void findIds() {
        // 查询所有主键
        List<Integer> ids = Elephant.Dao.findIds();
        log.info(ids);
    }

    @Test
    public void findOne() {
        Elephant one = Elephant.Dao.findOne(18, "a", "n");
        log.info(one);
    }

    @Test
    public void finds() {
        List<Elephant> list = Elephant.Dao.finds(18, "b");
        log.info(list);
    }
}

(极简版) 第二种方式类现在的代码加上这三个查询后的代码

@Data
@FieldDefaults(level = AccessLevel.PRIVATE)  // 属性默认都是private
@lombok.experimental.Accessors(chain = true) // 链式编程
@Table(name = "tb_bird")                     // 实体类与表映射
public class Bee implements $Sql { // 实现$Sql接口, 可以在对象上直接使用 save, update, delete 方法 (不是必须的)
    int id;
    /** 年龄 */
    Integer age;
    /** 名字 */
    String name;
    /** 描述 */
    String description;

    // 自定义sqlDao处理类. (如果没有自定义sql就没必要定义这个接口)
    public interface Dao extends $Mapper<Bee> {
        /** 约定使用$符号表示自己 (这句代码也不是必须的) */
        Dao $ = mapper(Dao.class);

        /**
         * 使用注解忽略默认的内置处理
         * 也可以写成下面的方式, 这里只是示例使用
         * @return 查询表所有主键
         */
        @Ignore
        default List<Integer> findIds() {
            return $().execute(new SQLReady("select id from tb_bird"), Integer.class);
        }

        // 这里无需再写接口实现类, 只需要在bee.md文件写sql
        @SqlStatement(params = "age,name,description")
        Bee findOne(int age, String name, String description);

        // 这里无需再写接口实现类, 只需要在bee.md文件写sql
        @SqlStatement(params = "age,name")
        List<Bee> finds(int age, String name);
    }
}

第二种方式查询测试用例

@Log4j
public class BeeFindTest {
    @Before
    public void bf() {
        Config.dbInit();
    }

    @Test
    public void findIds() {
        // 查询所有主键
        List<Integer> ids = Bee.Dao.$.findIds();
        log.info(ids);
    }

    @Test
    public void findOne() {
        Bee one = Bee.Dao.$.findOne(18, "a", "n");
        log.info(one);
    }

    @Test
    public void finds() {
        List<Bee> list = Bee.Dao.$.finds(18, "b");
        log.info(list);
    }
}

    从上面两个示例代码中可以看出优缺点:

试着想一下,假设这类有15个字段并且与数据库交互的方法有10个左右.这里只是这样列举了一个类的三个方法(并且最多的查询条件只有三个,Model需要代码量就太多了点.那么我相信你的项目中有条件参数的并且大于三个的不会没有), 实际项目中表的数量比这个恐怖得多,这个时候就不是假设了,而是玩真的了.

关于Model

    Model把所有与数据库打交道的地方都写在一个类里面. , 你可以想象这个Model的代码有多庞大与臃肿,维护费时

关于beetlsql

    不需要编写数据库打交道的实体类. 只需要定义接口并编写对应 md文件

所有sql语句都存放类对应的md文件中管理 (如果简单的语句可以直接写在接口中, 声明default就可以)

在两个实现同样功能的情况下, 而第二种方式编写代码是极其的简洁, 甚至预览代码都快很多倍.

这里在放上第二种方式与md(sql)文件管理的截图

最后

上面都是亲自开发在有很多表的项目中的一些总结与讨论.

当然本文并不是说JFinal的Model不好用也不是说jfinal的缺点, 而是找到了一种(这个是仅是自认为)可以替代Model更好的编码方式.

这里是一个 beetlsql demo 大家可以参考.

最后我想说的是,一旦你使用beetlsql并结合上面推荐的方式编码. 你就在也回不去了,因为真的很爽.

曾经使用过 (Hibernate Mybatis) 在使用beetlsql, 你也不会想回到过去

猜你喜欢

转载自my.oschina.net/iohao/blog/882268