BeetlSQL 3 功能预览

一曲冷凌霜 提交于 2020-08-17 19:20:20

BeetlSQL 3 目前正在研发过程,预计9月能发版。相比于BeetlSQL2,有非常多的改进,本博客会用一部分介绍BeetlSQL3的功能,另外一部分介绍如何定制Beetl3。

BeetSql是一个全功能DAO工具, 同时具有Hibernate 优点 & Mybatis优点功能,适用于承认以SQL为中心,同时又需求工具能自动能生成大量常用的SQL的应用

文档较长,可以点击右侧的目录导航到你想关注的内容

目前BeetlSQL3 还有大量工作需要完善,欢迎留言给我,可以加入开发或者测试

BeetlSQL3 特点

  • 派别:SQL为中心
    • 内置常见增删改查功能,节省项目50%工作量
    • 强化SQL管理,通过md文件管理sql,使用Beetl模板编写复杂sql
    • 简单SQL可以通过Query类链式API完成
  • 全面支持跨数据库平台
  • 支持NOSQL,如ClickhHouse,Elastic,Hive等
  • 支持SQL查询引擎,如Apache Drill,Presto等
  • 支持一对一,一对多等常见的映射。
  • 可以使用约定习俗映射,复杂查询结果支持通过json配置映射到POJO
  • 提供idea插件
  • 其他
    • 具备代码生成功能,提供代码生成框架
    • 最大程度减少数据库重构对项目造成的影响
    • 最大程度减少数据库切换对项目造成的影响
    • 支持多数据源,数据源包含传统数据库,NOSQL,SQL查询引擎,且可以根据规则使用数据源
    • 内置主从支持
    • 提供丰富的扩展功能,80%的功能都可以自行扩展,打造自己个性化的数据库发访问框架,扩展适应新的数据库&NOSQL&查询引擎

数据库工具的痛点

  • 开发效率低,如mybatis,还需要搭配plus工具才能提高开发效率,而JOOQ这样的又不适合复杂访问
  • 无SQL管理,遇到复杂的sql特别难维护,比如在Java里拼写sql,遇到调整就麻烦
  • 跨数据库平台,即使Hibenerate,也完全做不到跨数据库
  • 缺少数据库和NOSQL无缝切换很难,比如一部分业务要无缝切换到NOSQL上
  • 数据库重构对代码影响非常大,数据库列修改,增加要改很多代码
  • 难以调试数据库访问代码

BeetlSQL不仅仅知道所有这些痛点,而且能很好的解决这些痛点

BeetlSQL3 例子

所有例子都可以从 https://gitee.com/xiandafu/beetlsql/tree/3.0/sql-samples/sql-sample-quickstart 看到和运行

  • S1QuickStart.SQLMananger API,Query类,Mapper使用,基本的CRUD映射
  • S2MappingSample: 如何把结果集映射到Java对象,通过注解,通过json配置,或者约定习俗进行复杂映射,通过自定义注解来扩展映射方式
  • S3PageSample: 翻页和范围查询
  • S4Other: 其他常用操作示例,一些常见的like,in,batch操作
  • S5Fetch:自动fetch功能 ,在查询对象后,还可以自动fetch其他对象,类似JPA的ORM,但ORM对CRUD影响过大,fetch功能则简单很多
  • S6MoreSource: 非常方便的实现多数据源,每个实体可以标记自己的数据源;或者简单一个主从数据库的例子;或者分表例子;或者分库+分表。
  • S7CodeGen: 使用BeetlSQL生成代码,SQL语句和数据库文档

用户能在2小时内浏览完所有例子并基本掌握BeetlSQL的用法

基础例子


/**
 * 入门 演示内置SQLManager用法和BaseMapper用法,项目中更推荐使用BaseMapper,而不是较为底层的SQLManager
 * @author xiandafu
 *
 */

public class S1QuickStart {

    SQLManager sqlManager;
    UserMapper mapper = null;

    public S1QuickStart(SQLManager sqlManager) {
        this.sqlManager = sqlManager;
        mapper = sqlManager.getMapper(UserMapper.class);
    }

    public static void main(String[] args) throws Exception {
        SQLManager sqlManager = SampleHelper.getSqlManager();
        S1QuickStart quickStart = new S1QuickStart(sqlManager);
        quickStart.baseSqlManager();
        quickStart.executeSql();
        quickStart.executeTemplate();
        quickStart.query();
        quickStart.mapper();
        quickStart.sqlResource();
    }

    /**
     * 使用内置sqlManager方法
     */

    public  void baseSqlManager(){
        UserEntity user  = sqlManager.unique(UserEntity.class,1);

        user.setName("ok123");
        sqlManager.updateById(user);

        UserEntity newUser = new UserEntity();
        newUser.setName("newUser");
        newUser.setDepartmentId(1);
        sqlManager.insert(newUser);


        UserEntity template = new UserEntity();
        template.setDepartmentId(1);
        List<UserEntity> list = sqlManager.template(template);



    }


    //执行sql语句方法
    public  void executeSql(){
        String sql = "select * from user where id=?";
        Integer id  = 1;
        SQLReady sqlReady = new SQLReady(sql,new Object[id]);
        List<UserEntity> userEntities = sqlManager.execute(sqlReady,UserEntity.class);

        String updateSql = "update department set name=? where id =?";
        String name="lijz";
        SQLReady updateSqlReady = new SQLReady(updateSql,new Object[]{name,id});
        sqlManager.executeUpdate(updateSqlReady);


    }

    //执行sql模板语句
    public  void executeTemplate(){

        {
            String sql = "select * from user where department_id=#{id} and name=#{name}";
            UserEntity paras = new UserEntity();
            paras.setDepartmentId(1);
            paras.setName("lijz");
            List<UserEntity> list = sqlManager.execute(sql,UserEntity.class,paras);
        }

        {
            //或者使用Map作为参数
            String sql = "select * from user where department_id=#{myDeptId} and name=#{myName}";
            Map paras = new HashMap();
            paras.put("myDeptId",1);
            paras.put("myName","lijz");
            List<UserEntity> list = sqlManager.execute(sql,UserEntity.class,paras);
        }

        {
            //使用Beetl模板语句
            String sql = "select * from user where 1=1 \n" +
                        "-- @if(isNotEmpty(myDeptId)){\n" +
                        "   and department_id=#{myDeptId}\t\n" +
                        "-- @}\n" +
                        "and name=#{myName}";


            Map paras = new HashMap();
            paras.put("myDeptId",1);
            paras.put("myName","lijz");
            List<UserEntity> list = sqlManager.execute(sql,UserEntity.class,paras);

        }



    }

    public  void query(){
        {
            Query<UserEntity> query = sqlManager.query(UserEntity.class);
            List<UserEntity> entities = query.andEq("department_id",1)
                    .andIsNotNull("name").select();
        }
        {
            //使用LambdaQuery,能很好的支持数据库重构
            LambdaQuery<UserEntity> query = sqlManager.lambdaQuery(UserEntity.class);
            List<UserEntity> entities = query.andEq(UserEntity::getDepartmentId,1)
                    .andIsNotNull(UserEntity::getName).select();
        }



    }


    /**
     * 最常用的方式,编写一个Mapper类,mapper方法提供数据库访问接口,beetlsql提供丰富的beetlsql实现
     */
    public  void mapper(){


//        内置BaseMapper方法调用
        List<UserEntity> list = mapper.all();
        boolean isExist = mapper.exist(2);
        UserEntity me = mapper.unique(1);
        me.setName("newName");
        mapper.updateById(me);

        //调用其他方法
        UserEntity user = mapper.getUserById(1);
        UserEntity user2 = mapper.queryUserById(2);
        mapper.updateName("newName2",2);

        List<UserEntity> users = mapper.queryByNameOrderById("newName2");

        List<DepartmentEntity> depts = mapper.findAllDepartment();

    }

    /**
     * 对于复杂sql语句,比如几十行,甚至几百行的sql模板语句,放到markdown文件里是个不错的想法
     * 参考sql/user.md#select
     */
    public  void sqlResource(){
        SqlId id = SqlId.of("user","select");
        //or SqlId id = SqlId.of("user.select");
        Map map = new HashMap();
        map.put("name","n");
        List<UserEntity> list = sqlManager.select(id,UserEntity.class,map);

        UserMapper mapper = sqlManager.getMapper(UserMapper.class);
        mapper.select("n");

    }

}

结果集映射

/**
 * 演示如何将数据库查询结果映射到java对象上
 *
 * @author xiandafu
 */

public class S2MappingSample {

    SQLManager sqlManager;
    UserMapper mapper =null;

    public S2MappingSample(SQLManager sqlManager) {
        this.sqlManager = sqlManager;
        mapper = sqlManager.getMapper(UserMapper.class);
    }

    public static void main(String[] args) throws Exception {
        SQLManager sqlManager = SampleHelper.getSqlManager();
        S2MappingSample mappingSample = new S2MappingSample(sqlManager);
        mappingSample.column();
        mappingSample.toMap();
        mappingSample.view();
        mappingSample.mappingProvider();
        mappingSample.jsonConfig();
        mappingSample.autoMapping();
        mappingSample.myAttributeAnnotation();
    }

    /**
     * 使用@Column注解,或者按照NameConversion来自动映射
     */
    public void column() {
        MyUser user = sqlManager.unique(MyUser.class, 1);

    }

    /**
     * 可以把查询结果转化成Map,在java中,注意,滥用map作为也业务对象是非常糟糕设计
     */
    public void toMap() {
        SQLReady sqlReady = new SQLReady("select id,name from user");
        List<Map> list = sqlManager.execute(sqlReady, Map.class);

    }


    /**
     *
     */
    public void view() {
        //映射所有列
        TestUser user = sqlManager.unique(TestUser.class, 1);

        //映射只有一个KeyInfo标注的属性,本例子中department属性不在查询结果范围里
        TestUser keyInfo = sqlManager.viewType(TestUser.KeyInfo.class).unique(TestUser.class, 1);
    }

    /**
     * 使用额外的映射类来映射
     */
    public void mappingProvider() {
        //运行时刻指定一个映射类
        TestUser testUser = sqlManager.rowMapper(MyRowMapper.class).unique(TestUser2.class, 1);
        //使用@RowProvider注解为类指定一个Mapper,这个更常用
        TestUser testUse2 = sqlManager.unique(TestUser2.class, 1);
    }



    /**
     * 使用json 配置来映射,类似mybatis的xml配置
     */
    public void jsonConfig() {
        String sql = "select d.id id,d.name name ,u.id u_id,u.name u_name " +
                " from department d join user u on d.id=u.department_id  where d.id=?";
        Integer deptId = 1;
        SQLReady ready = new SQLReady(sql,new Object[]{deptId});
        List<DepartmentInfo> list = sqlManager.execute(ready,DepartmentInfo.class);
        System.out.println(list.toString());

    }


    /**
     * 使用json 配置来映射,类似mybatis的xml配置
     */
    public void autoMapping() {
        List<MyUserView> list = mapper.allUserView();
        System.out.println(list);

    }

    /**
     * 自定义一个属性注解Base64,用于编码和解码属性字段
     */
    public void myAttributeAnnotation(){

        UserData userData = new UserData();
        userData.setName("123456");
        sqlManager.insert(userData);
        UserData data = sqlManager.unique(UserData.class,userData.getId());
        System.out.println("user name "+data.getName());

        UserEntity entity = sqlManager.unique(UserEntity.class,userData.getId());
        System.out.println("db value "+entity.getName());

    }

    /**
     * 演示使用Column 注解映射java属性与表列名,
     */
    @Data
    @Table(name="user")
    public static class MyUser {
        @Column("id")
        @AutoID
        Integer myId;
        @Column("name")
        String myName;
    }


    @Data
    @Table(name="user")
    public static class TestUser {
        public static interface KeyInfo {
        }

        @Column("id")
        @AutoID
        @View(KeyInfo.class)
        Integer myId;

        @Column("name")
        @View(KeyInfo.class)
        String myName;

        Integer departmentId;
    }


    @RowProvider(MyRowMapper.class)
    public static class TestUser2 extends TestUser {

    }

    /**
     * 使用json配置来映射,如果映射配置过长,建议放到文件中,使用resource说明配置路径
     *
     */
    @Data
    @ResultProvider(JsonConfigMapper.class)
//    @JsonMapper(
//            "{'id':'id','name':'name','users':{'id':'u_id','name':'u_name'}}")
    @org.beetl.sql.annotation.entity.JsonMapper(resource ="user.departmentJsonMapping")
    public static class DepartmentInfo {
        Integer id;
        String name;
        List<UserInfo> users;
    }

    @Data
    public static class UserInfo {
        Integer id;
        String name;
    }


    /**
     * 如果数据库查询的结果与类定义一致,也可以使用AutoJsonMapper
     */
    @Data
    @ResultProvider(AutoJsonMapper.class)
    public static class MyUserView {
        UserInfo user;
        DepartmentEntity dept;
    }



    public static class MyRowMapper implements RowMapper<TestUser> {

        @Override
        public TestUser mapRow(ExecuteContext ctx, Object obj, ResultSet rs, int rowNum, Annotation config) throws SQLException {
            TestUser testUser = (TestUser) obj;
            testUser.setMyName(testUser.getMyName() + "-" + System.currentTimeMillis());
            return testUser;

        }
    }




    @Table(name="user")
    @Data
    public static class UserData{
        @AutoID
        Integer id;
        @Base64
        String name;
    }


    @Retention(RetentionPolicy.RUNTIME)
    @Target(value = {ElementType.METHOD, ElementType.FIELD})
    @Builder(Base64Convert.class)
    public static @interface Base64 {

    }


    /**
     * 自定义一个注解,实现把属性字段加密存入数据库,取出的时候解密
     */
    public static class Base64Convert  implements AttributeConvert {
        Charset utf8  = Charset.forName("UTF-8");

        public  Object toDb(ExecuteContext ctx, Class cls, String name, Object dbValue) {

            String value= (String) BeanKit.getBeanProperty(dbValue,name);
            byte[] bs = java.util.Base64.getEncoder().encode(value.getBytes(utf8));
            return new String(bs,utf8);

        }
        public  Object toAttr(ExecuteContext ctx, Class cls, String name, ResultSet rs, int index) throws SQLException {
            String value  = rs.getString(index);
            return new String(java.util.Base64.getDecoder().decode(value),utf8);
        }
    }


}

翻页查询

public class S3PageSample {

    SQLManager sqlManager;
    UserMapper mapper =null;

    public S3PageSample(SQLManager sqlManager) {
        this.sqlManager = sqlManager;
        mapper = sqlManager.getMapper(UserMapper.class);
    }

    public static void main(String[] args) throws Exception {
        SQLManager sqlManager = SampleHelper.getSqlManager();
        S3PageSample page = new S3PageSample(sqlManager);
        page.baseRange();
        page.page();
        page.jdbcPage();
        page.resourceSqlPage();
        page.resourceGroupSqlPage();
    }

    /**
     * 范围查询
     */
    public void baseRange(){
        List<UserEntity> all = mapper.all();
        long count = mapper.allCount();

        UserEntity template = new UserEntity();
        template.setDepartmentId(1);
        UserEntity user1 = mapper.templateOne(template);




    }

    /**
     * 翻页查询,使用模板sql
     */

    public void page(){

        /**
         * sql模板语句的page函数能自动把sql模板语句转为为求总数语句
         */
        String sql = "select #{page('*')} from user where department_id=#{id}";
        PageRequest request = DefaultPageRequest.of(1,10);
        Map map = new HashMap<>();
        map.put("id",1);
        PageResult pr = sqlManager.executePageQuery(sql,UserEntity.class,map,request);
        //强制转化为DefaultPageResult,
        DefaultPageResult pageResult = (DefaultPageResult)pr;
        printPageResult(pageResult);

    }

    /**
     * 直接使用jdbc sql
     */
    public void jdbcPage(){

        /**
         * 解析jdbc sql语句,生成求总数语句
         */
        String sql = "select * from user where department_id=?";
        PageRequest request = DefaultPageRequest.of(1,10);
        SQLReady sqlReady = new SQLReady(sql,new Object[]{1});
        PageResult pr = sqlManager.execute(sqlReady,UserEntity.class,request);
        DefaultPageResult pageResult = (DefaultPageResult)pr;
        printPageResult(pageResult);

    }


    /**
     * 翻页查询通常很复杂,SQL很长,把sql语句放到sql文件里是个好办法,也是最常用的办法
     */
    public void resourceSqlPage(){
        PageRequest request = DefaultPageRequest.of(1,10);
        PageResult pr = mapper.pageQuery(1,request);
        DefaultPageResult pageResult = (DefaultPageResult)pr;
       printPageResult(pageResult);
    }

    /**
     * 对分组语句进行翻页查询,需要嵌套在子查询里,比如
     * <pre>
     *     select count(1),name from user group by name
     * </pre>
     * 如上分组提供给beetlsql的时候,应该编写成
     * <pre>
     *    select #{page()} from ( select count(1),name from user group by name ) a
     * </pre>
     *
     */
    public void resourceGroupSqlPage(){
        PageRequest request = DefaultPageRequest.of(1,10);

        PageResult pr = mapper.pageQuery2(1,request);
        DefaultPageResult pageResult = (DefaultPageResult)pr;
        printPageResult(pageResult);
    }


    public void printPageResult(DefaultPageResult pageResult){
        System.out.println(pageResult.getPage());
        System.out.println(pageResult.getPageSize());
        System.out.println(pageResult.getTotal());
        System.out.println(pageResult.getTotalPage());
        System.out.println(pageResult.getResult());
    }

}

演示like,batchUpdate,in 操作

public class S4Other {

    SQLManager sqlManager;
    UserMapper mapper = null;

    public S4Other(SQLManager sqlManager) {
        this.sqlManager = sqlManager;
        mapper = sqlManager.getMapper(UserMapper.class);
    }

    public static void main(String[] args) throws Exception {
        SQLManager sqlManager = SampleHelper.getSqlManager();
        S4Other others = new S4Other(sqlManager);
        others.like();
        others.in();
        others.batch();
        others.sqlResult();

    }

    /**
     * like
     */
    public void like() {
        String sql = "select * from user where name like #{name}";
        Map paras = new HashMap();
        String name = "%li%";
        paras.put("name", name);
        List<UserEntity> users = sqlManager.execute(sql, UserEntity.class, paras);
        //同样效果
        sql = "select * from user where name like #{'%'+name+'%'}";
        paras = new HashMap();
        name = "li";
        paras.put("name", name);
        users = sqlManager.execute(sql, UserEntity.class, paras);

        //同样小姑
        SQLReady sqlReady = new SQLReady("select * from user where name like ?"
                ,new Object[]{"%"+name+"%"});
        users = sqlManager.execute(sqlReady,UserEntity.class);


    }

    /**
     * in
     */
    public void in() {
        //使用beetlsql提供的join函数,接受一个list变量
        String sql = "select * from user where id in ( #{join(ids)} )";
        List list = Arrays.asList(1,2,3,4,5);
        Map paras = new HashMap();
        paras.put("ids", list);
        List<UserEntity> users = sqlManager.execute(sql, UserEntity.class, paras);

    }

    /**
     * batch
     */
    public void batch() {
        //批量插入
        UserEntity user1 = new UserEntity();
        user1.setName("b1");
        user1.setDepartmentId(1);

        UserEntity user2 = new UserEntity();
        user2.setName("b2");
        user2.setDepartmentId(1);


        //根据组件批量更新
        List<UserEntity> data = Arrays.asList(user1,user2);
        sqlManager.insertBatch(UserEntity.class,data);

        data.get(1).setName("bb11");
        sqlManager.updateByIdBatch(data);

        //循环删除,执行多次
        data.stream().forEach(userEntity -> mapper.deleteById(userEntity.getId()));

    }

    /**
     * 不执行,只得到sql语句和参数
     */
    public void sqlResult(){

        Map map = new HashMap();
        map.put("name","li");
        SQLResult sqlResult = sqlManager.getSQLResult(SqlId.of("user","select"),map);
        String targetJdbc = sqlResult.jdbcSql;
        Object[] paras = sqlResult.toObjectArray();
        System.out.println(targetJdbc);
        System.out.println(Arrays.asList(paras));

    }


}

自动fetch


/**
 * 演示自动fetch,类似orm,但不同于orm,CRUD在ORM概念下过于复杂,
 * BeetlSQL的fetch没有那么多复杂概念,仅仅是加载对象后看看还有没有需要再加载的对象
 *
 *
 * @author xiandafu
 */

public class S5Fetch {

    SQLManager sqlManager;


    public S5Fetch(SQLManager  sqlManager) {
       this.sqlManager = sqlManager;
    }

    public static void main(String[] args) throws Exception {
        //为了简单起见,俩个sqlManager都来自同一个数据源,实际是不同数据库,甚至是NOSQL
        SQLManager sqlManager = SampleHelper.init();
        S5Fetch fetch = new S5Fetch(sqlManager);
        fetch.fetchOne();
        fetch.fetchMany();
    }

    /**
     *
     */
    public void fetchOne(){
        UserData user = sqlManager.unique(UserData.class,1);
        System.out.println(user.getDept());

        //fetchOne 会合并查询提高性能
        List<UserData> users = sqlManager.all(UserData.class);
        System.out.println(users.get(0).getDept());
    }

    public void fetchMany(){
        DepartmentData dept = sqlManager.unique(DepartmentData.class,1);
        System.out.println(dept.getUsers());
    }


    /**
     * 用户数据使用"a" sqlmanager
     */
    @Data
    @Table(name="user")
    @Fetch
    public static class UserData {
        @Auto
        private Integer id;
        private String name;
        private Integer departmentId;
        @FetchOne("departmentId")
        private DepartmentData dept;
    }

    /**
     * 部门数据使用"b" sqlmanager
     */
    @Data
    @Table(name="department")
    @Fetch
    public static class DepartmentData {
        @Auto
        private Integer id;
        private String name;
        @FetchMany("departmentId")
        private List<UserData> users;
    }

}

多数据库

可能是BeetlSQL最不好理解的部分,然而,应该还是比其他DAO工具更容易实现和理解


/**
 * <ui>
 *     <li>
 *      演示多数据源操作中的ConditionalSQLManager,按照条件决定使用哪个SQLManager
 *      ConditionalSQLManager.decide方法决定使用哪个SQLManager,
 *      decide默认会读取目标对象的TargetSQLManager注解来决定,SQLManager的有些api参数没有目标对象,则使用默认SQLManager
 *     </li>
 *     <li>
 *         演示user分表操作,动态表名实现分表
 *     </li>
 *     <li>
 *         演示user分库操作,根据条件决定数据访问哪个数据库,使用了{@link ConditionalConnectionSource}
 *     </li>
 * </ui>

 *
 *
 * 注意:分库分表最好使用中间件
 * @author xiandafu
 */

public class S6MoreDatabase {

    public S6MoreDatabase() {
    }

    public static void main(String[] args) throws Exception {

        S6MoreDatabase moreSource = new S6MoreDatabase();
        moreSource.conditional();
        moreSource.masterSlave();
        moreSource.multipleTables();
        moreSource.multipleDataBaseAndTables();
    }

    /**
     * 多数据源协作
     */
    public void conditional() {

        SQLManager a = SampleHelper.init();
        SQLManager b = SampleHelper.init();
        Map<String, SQLManager> map = new HashMap<>();
        map.put("a", a);
        map.put("b", b);
        SQLManager sqlManager = new ConditionalSQLManager(a, map);

        //不同实体,用不同sqlManager操作,存入不同的数据库
        UserData user = new UserData();
        user.setName("hello");
        user.setDepartmentId(2);
        sqlManager.insert(user);

        DepartmentData dept = new DepartmentData();
        dept.setName("dept");
        sqlManager.insert(dept);
    }


    /**
     * 普通一主多从
     */
    public void masterSlave(){
        //为了简单起见,主从库都走同一个数据库
        DataSource master = SampleHelper.mysqlDatasource();
        DataSource slave1 = SampleHelper.mysqlDatasource();
        DataSource slave2 = SampleHelper.mysqlDatasource();
        ConnectionSource source = ConnectionSourceHelper.getMasterSlave(master,new DataSource[]{slave1,slave2});

        SQLManagerBuilder builder = new SQLManagerBuilder(source);
        builder.setNc(new UnderlinedNameConversion());
        builder.setInters(new Interceptor[]{new DebugInterceptor()});
        builder.setDbStyle(new MySqlStyle());
        SQLManager sqlManager = builder.build();

        //更新操作走主库
        UserData user = new UserData();
        user.setName("a");
        user.setDepartmentId(1);
        sqlManager.insert(user);
        //查询走从库
        sqlManager.unique(UserData.class,1);


    }

    /**
     * 单库分表操作,user对象的{@code @Table}注解是逻辑表达式
     * <pre>{@code
     *     @Table(name="${toTable('user',id)}"
     *     public class User{
     *
     *     }
     * }</pre>
     * toTable方法是一个自定义注册的beetl方法,在运行的时候会根据id换算出真实表
     *
     * 对于beetlsql所有内置方法,都可以自动分表,但你自己的sql,也要类似使用
     * {@code ${toTable('user',id)}}
     * @see TableChoice
     */
    public void multipleTables(){

        SQLManager sqlManager = getSQLManager4MultipleTables();
        //使用user表
        sqlManager.deleteById(MyUser.class,199);
        MyUser user = new MyUser();
        user.setName("abc");
        user.setId(199);
        sqlManager.insert(user);

        //使用user_1表. 为了简单起见,分表逻辑返回的目标表还是user表
        MyUser user2 = new MyUser();
        user2.setName("abc");
        user2.setId(1500);
        sqlManager.insert(user2);

    }

    /**
     * 分库分布表操作,同{@link #multipleTables()} 方法,但增加如果id超过一定限额,走另外一个数据库
     * 核心还是需要定义一个分库分表逻辑
     * @see TableAndDataBaseChoice
     */
    public void multipleDataBaseAndTables(){

        SQLManager sqlManager = getSQLManager4MultipleDatBase();
        sqlManager.deleteById(MyUser.class,199);
        MyUser user = new MyUser();
        user.setName("abc");
        user.setId(199);
        sqlManager.insert(user);

        //这条记录使用第二个库的user表
        sqlManager.deleteById(MyUser.class,2900);
        MyUser user2 = new MyUser();
        user2.setName("abc");
        user2.setId(2900);
        sqlManager.insert(user2);

    }

    protected SQLManager getSQLManager4MultipleTables(){
        SQLManager sqlManager = SampleHelper.getSqlManager();
        //告诉sqlManager遇到USER_TABLE这个不存在的表不慌,他是个虚表,真实表是user
        sqlManager.addVirtualTable("user",USER_TABLE);
        BeetlTemplateEngine templateEngine = (BeetlTemplateEngine)sqlManager.getSqlTemplateEngine();
        // 注册一个方法来实现映射到多表的逻辑
        templateEngine.getBeetl().getGroupTemplate().registerFunction("toTable", new Function(){

            @Override
            public Object call(Object[] paras, Context ctx) {
                String tableName = (String)paras[0];
                Integer id = (Integer)paras[1];
                //使用分表逻辑
                TableChoice tableChoice = new TableChoice();
                return tableChoice.getTableName(tableName,id);

            }
        });
        return sqlManager;
    }

    /**
     * 分表选择逻辑
     */
    public static  class TableChoice{
        public String getTableName(String tableName,Integer id){
            if(id<1000){
                return tableName;
            }else{
                //根据需要返回另外一个表,比如tableName+"_1"
                return tableName;
//                return tableName+"_1";
            }
        }
    }

    /**
     * 分库选择逻辑,用户自由实现分表分库逻辑,
     */
    public static  class TableAndDataBaseChoice{
        public String getTableName(ExecuteContext executeContext,String tableName,Integer id){

            if(id<1000){
                return tableName;
            }else if(id<2000){
                return tableName+"_1";
            }else{
                //如果继续大,设置一个标记,进入另外一个数据库cs2库的user表

                executeContext.setContextPara(FLAG,"cs2");
                if(id<3000){
                    return tableName;
                }else{
                    return  tableName+"_1";
                }

            }

        }
    }



    private static final String FLAG ="connectionSource";
    protected  SQLManager getSQLManager4MultipleDatBase(){
        //为了测试方便,假设指向同一个数据库
        DataSource db1 = SampleHelper.mysqlDatasource();
        ConnectionSource cs1 = ConnectionSourceHelper.getSingle(db1);
        DataSource  db2 = SampleHelper.mysqlDatasource();
        ConnectionSource cs2 = ConnectionSourceHelper.getSingle(db2);
        Map<String,ConnectionSource>  datas = new HashMap<>();
        datas.put("cs1",cs1);
        datas.put("cs2",cs2);
        // 配置策略
        ConditionalConnectionSource.Policy policy = new ConditionalConnectionSource.Policy() {
            @Override
            public String getConnectionSourceName(ExecuteContext ctx, boolean isUpdate) {
                String name = (String)ctx.getContextPara(FLAG);
                if(name!=null){
                    return name;
                }else{
                    // 如果没有设置,则返回一个默认库
                    return "cs1";
                }
            }

            @Override
            public String getMasterName() {
                return "cs1";
            }
        };


       ConditionalConnectionSource ds  = new ConditionalConnectionSource(policy,datas);
        // 初始化sqlManager,使用ConditionalConnectionSource
        SQLManagerBuilder builder = new SQLManagerBuilder(ds);
        builder.setNc(new UnderlinedNameConversion());
        builder.setInters(new Interceptor[]{new DebugInterceptor()});
        builder.setDbStyle(new MySqlStyle());
        SQLManager sqlManager = builder.build();
        // 申明一个虚表 "${toTable('user',id)}",实际上是user表
        sqlManager.addVirtualTable("user",USER_TABLE);
        BeetlTemplateEngine templateEngine = (BeetlTemplateEngine)sqlManager.getSqlTemplateEngine();
        // 注册一个方法来实现映射到多表的逻辑
        templateEngine.getBeetl().getGroupTemplate().registerFunction("toTable", new Function(){

            @Override
            public Object call(Object[] paras, Context ctx) {
                String tableName = (String)paras[0];
                Integer id = (Integer)paras[1];
                ExecuteContext executeContext = (ExecuteContext)ctx.getGlobal(ExecuteContext.NAME);
                //使用分库逻辑
                TableAndDataBaseChoice choice = new TableAndDataBaseChoice();
                return choice.getTableName(executeContext,tableName,id);

            }
        });

        return sqlManager;

    }








    /**
     * 用户数据使用"a" sqlmanager
     */
    @Data
    @Table(name = "user")
    @TargetSQLManager("a")
    public static class UserData {
        @Auto
        private Integer id;
        private String name;
        private Integer departmentId;
    }

    /**
     * 部门数据使用"b" sqlmanager
     */
    @Data
    @Table(name = "department")
    @TargetSQLManager("b")
    public static class DepartmentData {
        @Auto
        private Integer id;
        private String name;
    }


    static final String USER_TABLE="${toTable('user',id)}";
    @Data
    @Table(name = USER_TABLE)
    public static class MyUser {
        @AssignID
        private Integer id;
        private String name;
    }




}

代码生成框架

演示代码生成框架,以及生成代码和数据库文档


/**
 * 演示beetlsql 代码生成框架
 *
 * @author xiandafu
 */

public class S7CodeGen {

    SQLManager sqlManager;


    public S7CodeGen(SQLManager  sqlManager) {
       this.sqlManager = sqlManager;
        initGroupTemplate();
    }

    protected  void initGroupTemplate(){
        //指定模板文件路径,正常情况下,不需要要指定,默认在classpath:templates,但idea的环境读取不到
        GroupTemplate groupTemplate = BaseTemplateSourceBuilder.getGroupTemplate();
        String root = System.getProperty("user.dir");
        //代码模板在sql-gen,你可以指定自己的模板路径
        String templatePath = root+"/sql-gen/src/main/resources/templates/";
        FileResourceLoader  resourceLoader = new FileResourceLoader(templatePath);
        groupTemplate.setResourceLoader(resourceLoader);
    }

    public static void main(String[] args) throws Exception {
        //为了简单起见,俩个sqlManager都来自同一个数据源,实际是不同数据库,甚至是NOSQL
        SQLManager sqlManager = SampleHelper.init();
        S7CodeGen gen = new S7CodeGen(sqlManager);
        gen.genCode();
        gen.genDoc();
        gen.genAllDoc();
    }

    /**
     * 代码生成,生成实体,mapper代码
     */
    public void genCode(){
        List<SourceBuilder> sourceBuilder = new ArrayList<>();
        SourceBuilder entityBuilder = new EntitySourceBuilder();
        SourceBuilder mapperBuilder = new MapperSourceBuilder();
        SourceBuilder mdBuilder = new MDSourceBuilder();

		sourceBuilder.add(entityBuilder);
		sourceBuilder.add(mapperBuilder);
		sourceBuilder.add(mdBuilder);

        SourceConfig config = new SourceConfig(sqlManager,sourceBuilder);
        //如果有错误,抛出异常而不是继续运行1
        EntitySourceBuilder.getGroupTemplate().setErrorHandler(new ReThrowConsoleErrorHandler() );

        ConsoleOnlyProject project = new ConsoleOnlyProject();
        String tableName = "USER";
        config.gen(tableName,project);
    }


    /**
     * 生成数据库文档
     */
    public void genDoc(){
        List<SourceBuilder> sourceBuilder = new ArrayList<>();
        SourceBuilder docBuilder = new MDDocBuilder();

        sourceBuilder.add(docBuilder);

        SourceConfig config = new SourceConfig(sqlManager,sourceBuilder);
        //如果有错误,抛出异常而不是继续运行1
        EntitySourceBuilder.getGroupTemplate().setErrorHandler(new ReThrowConsoleErrorHandler() );

        ConsoleOnlyProject project = new ConsoleOnlyProject();
        String tableName = "USER";
        config.gen(tableName,project);
    }


    /**
     * 生成数据库文档
     */
    public void genAllDoc(){
        List<SourceBuilder> sourceBuilder = new ArrayList<>();
        SourceBuilder docBuilder = new MDDocBuilder();
        sourceBuilder.add(docBuilder);

        SourceConfig config = new SourceConfig(sqlManager,sourceBuilder);
        //如果有错误,抛出异常而不是继续运行1
        EntitySourceBuilder.getGroupTemplate().setErrorHandler(new ReThrowConsoleErrorHandler() );

        StringOnlyProject project = new StringOnlyProject();
        config.genAll(project);
        String output = project.getContent();
        System.out.println(output);
    }

}

BeetlSQL3 架构(节选)

Mapper定制

使用或者定制BeetlSQL,可以从Mapper开始,BeetlSQL提供了BaseMapper接口,作为推荐的使用BeetlSQL首选方式

public interface UserMapper extends BaseMapper<User>{
  
}

这里,我们假定一个User对象,我们希望对其增删改查,对应数据库user表。

如果你还不熟悉BeetSQL用法,阅读《BeetlSQL使用指南》

BaseMapper提供了内置的CRUD等若干方法,满足50%的常用DAO操作,选取如下4个方法


public interface BaseMapper<T> {
  
  @AutoMapper(InsertAmi.class)
  void insert(T entity);
  
  @AutoMapper(UpdateByIdAmi.class)
  int updateById(T entity);
  
  @AutoMapper(UniqueAmi.class)
  T unique(Object autoKey);
  
  @AutoMapper(SingleAmi.class)
  T single(Object autoKey);
  
}

  • insert 插入User到相应的表里
  • updateById,更新User,按照主键id作为条件更新
  • unique,按照主键查找User,如果未找到,抛出异常
  • single,同unique,如果未找到,返回null

这四个内置方法,都使用了注解@AutoMapper,定义如下

@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoMapper {
	Class<? extends MapperInvoke> value() ;
}

AutoMapper需要提供一个类,必须是MapperInvoke的子类,此子类真正负责执行Dao操作。我们以InsertAmi为例子,代码如下

public class InsertAmi implements MapperInvoke {
    @Override
    public Object call(SQLManager sm, Class entityClass, Method m, Object[] args) {
        int ret = sm.insert(args[0]);
        return ret;
    }
}

可以看到InsertAmi非常简单,仅仅调用SQLManager的insert方法,SQLManager是BeetlSQL提供的核心类.流程如下

如果你想定义自己的通用Mapper,可以定义任意一个接口,或者继承已经做好的BaseMapper,使用@AutoMapper标明其实现类即可。

除了AutoMapper注解,BeetlSQL也提供了其他注解,甚至是可以自定义注解,解释如下

BeetlSQL提供了@Sql注解,允许同时提供sql语句


@Sql("select * from user where id= ?)
public User queryById(Integer id);

解释@Sql注解的也是一个MapperInvoke子类,位于org.beetl.sql.mapper.ready包下,

public class SelectSQLReadyMI extends BaseSQLReadyMI {
    boolean isSingle = false;
    public SelectSQLReadyMI(String sql,Class targetType,boolean isSingle){
        this.sql = sql;
        this.targetType = targetType;
        this.isSingle = isSingle;
    }

    @Override
    public Object call(SQLManager sm, Class entityClass, Method m, Object[] args) {
        SQLReady sqlReady = new SQLReady(this.getSql(),args);
        List list =  sm.execute(sqlReady,this.getTargetType());
        if(isSingle){
            return list.isEmpty()?null:list.get(0);
        }else{
            return list;
        }
    }
}

可以看到,SelectSQLReadyMI的call方法仍然非常简单,调用SQLManager的jdbc查询方法。SelectSQLReadyMI的构造是通过MapperMethodParser.parse方法,稍后会介绍这个类

如果你想利用BeetlSQL实现自定义注解,比如你觉得提供sql语句繁琐,你更喜欢SpringData那种根据方法名来推测sql语句,类似如下

@SpringData
public User queryByNameAndAge(String name,Integer a);

Spring Data 是一个Spring封装的DAO框架,它会根据dao的方法命名来判断,如上方法,表示查询用户(query),条件是通过name属性,因此sql是 select * from user where name=? and age=?

为了使BeetlSQL能识别SpringData注解,且能使用MapperInvoke执行,你可以定义SpringData注解,如下

@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Builder(SpringDataBuilder.class)
public @interface SpringData {

}

这里,@SpringData注解又使用了BeetlSQL的@Builder注解,这表明@SpringData注解的解释是通过此类来定义的,BeetlSQL期望SpringDataBuilder实现MapperExtBuilder接口,能解析当前方法,返回一个MapperInvoke接口,类似如下

public class SpringDataBuilder  implements MapperExtBuilder {
    @Override
    public MapperInvoke parse(Class entity, Method m) {
        ......
    }
}

MapperExtBuilder 用于解析Mapper定义的方法,转化为对应的sql查询(通常是调用SQLMananager API),系统已经提供了SpringDataBuilder和ProviderMapperExtBuilder .

@Builder注解是BeetlSQL一个很强大的注解,它是注解的注解,表明了注解的含义,我们在后面还能看到很多注解都采用了@Builder这个方法,比如Mapper中的SqlProvider, Bean的注解扩展@UpdateTime,以及关联查询的@FetchOne,@FetchMany

Mapper类图如下

数据库表到Java对象

BeetlSQL提供多种方式实现数据库映射,包括

  • 约定习俗,指定NameConversion
  • 通过@Table和@Column注解
  • 通过ViewType 只映射一部分结果集
  • 通过RowMapper自定义行映射,想在如上映射结果基础上,在定制映射结果
  • 通过ResultSetMapper 自定义结果集映射,这有包含了@JsonMapper 的实现JsonConfigMapper和AutoJsonMapper俩种复杂结果集映射,类似MyBatis通过XML配置映射

映射完毕后,可以通过AttributeConvert或者BeanConvert再次对映射结果处理。比如加密字段的解密,或者字符串变成json操作

在返回结果集前,BeetlSQL还会查看是否有@Fetch标签,进行额外数据的抓取

NameConversion

NameConversion 定义了如何把Java名字转化为数据库名字,或者相反

public abstract String getTableName(Class<?> c);
public abstract String getColName(Class<?> c,String attrName);
public abstract String getPropertyName(Class<?> c,String colName);

NameConversion 的子类内置了DefaultNameConversion,即不做任何改变。UnderlinedNameConversion,把下划线去掉,其后字母大写。最为常用,也符合数据库设计规范,使用UnderlinedNameConversion

重写NameConversion需要考虑读取@Table和@Cloumn注解,可以复用NameConversion.getAnnotationColName,getAnnotationAttrName和getAnnotationTableName,如下是UnderlinedNameConversion的实现

@Override
public String getTableName(Class<?> c) {
  String name = getAnnotationTableName(c);
  if(name!=null){
    return name;
  }
  return StringKit.enCodeUnderlined(c.getSimpleName());
}

@Override
public String getColName(Class<?> c,String attrName) {
  String col = super.getAnnotationColName(c,attrName);
  if(col!=null){
    return col;
  }
  return StringKit.enCodeUnderlined(attrName);
}

@Override
public String getPropertyName(Class<?> c,String colName) {
  String attrName = super.getAnnotationAttrName(c,colName);
  if(attrName!=null){
    return attrName;
  }
  return StringKit.deCodeUnderlined(colName.toLowerCase());
}

ViewType

ViewType 类似Jackson的@View注解,在BeetlSQL查询过程中,查询被VIewType申明的字段,如下TestUser,属性myId和myName被@View注解标注,因此sqlManager指定viewType为KeyInfo.class的时候,仅仅查询此俩列

 TestUser keyInfo = sqlManager.viewType(TestUser.KeyInfo.class).unique(TestUser.class, 1);

@Data
public static class TestUser {
  public static interface KeyInfo {
  }

  @Column("id")
  @AutoID
  @View(KeyInfo.class)
  Integer myId;

  @Column("name")
  @View(KeyInfo.class)
  String myName;

  Integer departmentId;
}

VIewType会影响代码生成,因此对于TestUser对象来说,根据主键查询会有俩条内置sql语句生成,参考代码AbstractDBStyle

public SQLSource genSelectByIds(Class<?> cls,Class viewType) {
  ConcatContext concatContext = this.createConcatContext();
  Select select = concatContext.select();
  appendIdsCondition(cls,select);
  select.from(cls);
  if(viewType!=null){
    select.all(cls,viewType);
  }else{
    select.all();
  }
  return new SQLTableSource(select.toSql());
}

对于普通的sql'语句,也可以只映射部分查询结果,而不需要映射所有结果集,比如某些大字段(TODO,未完成)

RowMapper

RowMapper 可以在BeetlSQL默认的映射规则基础上,添加用户自定义映射,RowMapper可以通过SQLManager传入,或者通过POJO上的注解来申明,比如

@RowProvider(MyRowMapper.class)
public static class TestUser2 extends TestUser {

}

所有查询结果映射到TestUser2后,还需要执行MyRowMapper接口

@RowProvider注解定义如下

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface RowProvider {
	Class<? extends RowMapper>  value();
}

ResultSetMapper

ResultSetMapper对象相当于告诉BeetlSQL,不需要BeetlSQL来映射,交给ResultSetMapper来实现,比如一个select join结果需要映射到复杂的对象上(比如一个用户有多个角色,属于多个组织),BeetlSQL自带了JsonConfigMapper实现,用json来申明如何映射,类似MyBatis用xml来申明如何映射

String sql = "select d.id id,d.name name ,u.id u_id,u.name u_name " +
                " from department d join user u on d.id=u.department_id  where d.id=?";
Integer deptId = 1;
SQLReady ready = new SQLReady(sql,new Object[]{deptId});
List<DepartmentInfo> list = sqlManager.execute(ready,DepartmentInfo.class);

@Data
@ResultProvider(JsonConfigMapper.class)
@JsonMapper(
  "{'id':'id','name':'name','users':{'id':'u_id','name':'u_name'}}") 
public static class DepartmentInfo {
  Integer id;
  String name;
  List<UserInfo> users;
}

注解ResultProvider提供了一个ResultSetMapper实现类,@JsonMapper是一个配置注解,与ResultProvider搭档,提供额外配置,JsonMapper支持配置在java代码里,或者通过文件配置

Pojo类上所有注解都在ClassAnnotation类上存放,ResultProvider和JsonMapper 被缓存在ClassAnnotation类里,因为JsonMapper注解被ProviderConfig注解所申明,所以他俩是一对一

@ProviderConfig()
public @interface JsonMapper {
	String value() default "";
	String resource() default  "";
}

ClassAnnotation 不仅仅寻找ResultProvider注解,也寻找使用了@ProviderConfig()的注解,并作为配置注解放在一起。BeetlSQL大量使用这种注解的注解,来提供扩展机制

JsonConfigMapper定义如下

public class JsonConfigMapper extends ConfigJoinMapper {
   protected AttrNode parse(ExecuteContext ctx, Class target, ResultSetMetaData rsmd, Annotation config){
     
   }
}

ConfigJoinMapper 是基类,他会根据AttrNode描述来做映射,因此JsonConfigMapper只需要读取config注解申明的配置,然后转化成AttrNode即可,如果你想让配置是yml或者xml,可以实现parse方法即可

AttributeConvert

AttributeConvert用于属性转化,定义如下

public default Object toAttr(ExecuteContext ctx, Class cls,String name, ResultSet rs, int index) throws SQLException {
  return rs.getObject(index);
}
public default Object toDb(ExecuteContext ctx,  Class cls,String name, Object dbValue) {
  return dbValue;
}

toAttr用于把数据库转化成属性值,比如数据库字符串转成Java的json对象,toDb则是把属性值在存入数据库之前转成合适的值,比如json对象转成字符串

在定义了AttributeConvert类后,需要在定义一个注解,这样,beetlsql遇到此注解,将按照上述机制执行,注解的注解仍然使用@Builder 来完成,Builder接受一个AttributeConvert子类

@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(Base64Convert.class)
public static @interface Base64 {

}

因此,可以自pojo上使用此注解

@Table(name="user")
@Data
public static class UserData{
  @AutoID
  Integer id;
  @Base64
  String name;
}

所有关于pojo的注解都在ClassAnnotation里维护

一个简单的实现Base64注解实现如下,这样保证name字段存入数据库是经过base64加密,取出是base64解密

public static class Base64Convert  implements AttributeConvert {
  Charset utf8  = Charset.forName("UTF-8");

  public  Object toDb(ExecuteContext ctx, Class cls, String name, Object dbValue) {

    String value= (String) BeanKit.getBeanProperty(dbValue,name);
    byte[] bs = java.util.Base64.getEncoder().encode(value.getBytes(utf8));
    return new String(bs,utf8);

  }
  public  Object toAttr(ExecuteContext ctx, Class cls, String name, ResultSet rs, int index) throws SQLException {
    String value  = rs.getString(index);
    return new String(java.util.Base64.getDecoder().decode(value),utf8);
  }
}

BeanConvert

BeanConvert同AttributeConvert类似,但用于整个Bean,BeanConvert定义如下

public interface BeanConvert {
    public default Object before(ExecuteContext ctx, Object obj, Annotation an){
        return obj;
    }
    public default Object after(ExecuteContext ctx, Object obj, Annotation an){
        return obj;
    }
}

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!