RedKale

##

RedKale 入门 Convert 组件 Service 组件 Source 组件 Net 组件 Watch 组件 Javadoc API Github 源码

### Source 组件介绍

Source 主要为数据源提供简易的操作接口，使底层的具体数据源(传统数据库、文件系统、内存数据库、Memcached/Redis缓存)对上层是透明的。其提供两种类型的数据源：DataSource 和 CacheSource。DataSource 为数据库或内存数据库，提供类似JPA、Hibernate的接口与功能。CacheSource 为缓存数据提供类似Memcached、Redis的接口和功能。两者也提供了异步接口(基于远程模式Service)。

### DataSource 入门

JPA虽然已经提供了简洁成熟的数据库操作接口，但当数据、业务量很庞大的时候就显得捉襟见肘，与JPA相比，DataSource有以下几个特点：
1、简易的过滤查询接口，但仅支持简单的表关联查询。
2、分布式的主键的分布式自增策略。
3、动态更新变更后的数据库连接参数。
4、读写分离的简易配置。
5、进程间的缓存自动同步。
数据库操作方面常见的是过滤查询操作，JPA规范中的JPQL虽然简化了SQL，但是对于动态产生的过滤条件,使用者还是无法免去组装过滤条件的过程(无论JPQL还是CriteriaQuery), DataSource定义了FilterBean接口可以省略组装条件的过程，FilterNode提供了类似CriteriaQuery的功能，且这两种对象都可以序列化，给远程模式Service提供了基础，微服务架构提倡服务之间尽量降低耦合，因此DataSource仅支持简单的关联查询，复杂的表关联查询或统计应放在数据分析系统中。一个服务通常部署多个进程，若用JPA的缓存则进程之间的缓存无法同步，而DataSource采用SNCP协议即可方便地达到自动同步缓存功能。JPA无法在主数据库异常时动态切换到备份数据库，DataSource会监听persistence.xml文件，当文件配置发生变化时自动切换新的数据库配置。
为了降低学习成本，DataSource重用了JPA里的部分注解与配置文件，使用方法基本相同，与JPA用法唯一区别是注解只能标记于字段，不能标记在方法上。

注解类名 | 功能描述
--- | ---
javax.persistence.Cacheable | 标记Entity类是否需要缓存，与JPA用法一致
javax.persistence.Column | 标记字段，只使用其name()、insertable()、updatable()属性
javax.persistence.Entity | JPA的Entity类必须标记为@Entity, 而RedKale不强制要求，所以该注解一般无用
javax.persistence.GeneratedValue | 仅用于标记主键是否为数据库自增长，其内值的两属性废弃
javax.persistence.GenerationType | 被@GeneratedValue的属性引用，在RedKale内不被使用到
javax.persistence.Id | 标记主键字段，与JPA用法一致
javax.persistence.Table | 标记表的别名，与JPA用法一致
javax.persistence.Transient | 标记是否为表对应的字段，与JPA用法一致
以下是RedKale自定义的注解
@VirtualEntity | 用于非数据库表对应的Entity类，且仅用于开启缓存模式的DataSource
@DistributeGenerator | 标记主键是否采用分布式自增长策略，不能与@GeneratedValue同用
@DistributeTables | 当多个表的主键唯一性公用时需要该注解
@FilterColumn | 用于FilterBean过滤类的字段设置
@FilterJoinColumn | 用于FilterBean过滤类的关联表字段设置
@FilterGroup | 用于FilterBean过滤类的过滤条件分组设置

操作数据源主要使用的对象有 DataSource、FilterBean、FilterNode。DataSource 提供的接口分几种系列：

系列方法 | 功能描述
--- | ---
insert | 插入数据
delete | 删除数据
update | 更新数据
updateColumnXXX | 更新数据的部分字段
getNumberResult | 统计查询，用于查询字段的总和、最大值、平均值等数据
queryColumnXXX | 单个字段数据查询和字段的统计查询
find | 查找单个对象
queryList | 查询对象的List集合
querySheet | 查询对象的Sheet页式集合
directXXX | 直接运行SQL语句，用于复杂的关联查询与更新

以上接口除了directXXX，其他都有等同的异步接口。insert、delete、update接口与JPA同名接口用法一样。DataSource提供了丰富的查询接口，且有独特的翻页查询功能。每以系列的方法主要重载三类： 单个字段过滤、FilterBean过滤和FilterNode过滤。

过滤条件

FilterBean、FilterNode对象用于构造过滤条件。FilterBean可以转化为FilterNode。FilterBean主要用于接收外界构建的过滤条件，而FilterNode为了构建内部的过滤条件且降低过滤条件变化的耦合性。

    public class UserBean implements FilterBean {

        private int userid;

        @FilterColumn(express = FilterExpress.LIKE)
        private String username;

        private Range age;

        public UserBean(int userid, String username, Range age) {
            this.userid = userid;
            this.username = username;
            this.age = age;
        }

        /** 以下省略getter setter方法 */

    }

     new UserBean(200001, "redkale", new IntRange(14, 36)) 等价于
     FilterNode.create("userid", 200001).and("username", FilterExpress.LIKE, "redkale").and("age", new Range.IntRange(14, 36))

     new UserBean(200001, "redkale", new IntRange(14, 36)) 等价于 "WHERE userid = 200001 AND username LIKE '%redkale%' AND age BETWEEN 14 AND 36"
     new UserBean(200001, "redkale", null) 等价于 "WHERE userid = 200001 AND username LIKE '%redkale%'"
     new UserBean(0, "redkale", null) 等价于 "WHERE username LIKE '%redkale%'"

如上定义UserBean过滤条件，当非数值类字段值为null、字符串值为空、数值类字段值小于@FilterColumn.least()值(least的默认值为1)都不会构建成过滤条件。@FilterColumn.express根据字段的类型有不同的默认值，若字段类型为Collection子类或数组则express默认为FilterExpress.IN；若字段类型为Range的子类则express默认为FilterExpress.BETWEEN；其他类型则express默认为FilterExpress.EQUAL。默认字段之间是AND关系，若想使用OR关系则需要使用@FilterGroup进行标记：

    public class UserBean implements FilterBean {

        private int userid;

        @FilterGroup("[OR]a")
        @FilterColumn(express = FilterExpress.LIKE)
        private String username;

        @FilterGroup("[OR]a")
        private Range age;

        public UserBean(int userid, String username, Range age) {
            this.userid = userid;
            this.username = username;
            this.age = age;
        }

        /** 以下省略getter setter方法 */

    }

     new UserBean(200001, "redkale", new IntRange(14, 36)) 等价于
     FilterNode orNode = FilterNode.create("username" , FilterExpress.LIKE, "redkale").or("age", new Range.IntRange(14, 36));
     FilterNode node = FilterNode.create("userid", 200001).and(orNode));

     new UserBean(200001, "redkale", new IntRange(14, 36)) 等价于 "WHERE userid = 200001 AND (username LIKE '%redkale%' OR age BETWEEN 14 AND 36)"
     new UserBean(200001, "redkale", null) 等价于 "WHERE userid = 200001 AND username LIKE '%redkale%'"
     new UserBean(0, "redkale", null) 等价于 "WHERE username LIKE '%redkale%'"

     source.getNumberResult(User.class, FilterFunc.COUNT, null, new UserBean(0, "redkale", new IntRange(14, 36))).intValue() 等价于
     "SELECT COUNT(*) FROM user WHERE username LIKE '%redkale%' AND  age BETWEEN 14 AND 36"

如上@FilterGroup 的value 必须是[OR]或者[AND]开头，没有标记@FilterGroup的字段等价于标记了@FilterGroup(value = "[AND]")。[AND]、[OR]后面的字符串为GROUP_NAME，默认的GROUP_NAME为空字符串。如上"[OR]a"可以直接使用"[OR]"，有多个[OR]或者[AND]则需要加上不同的NAME。

### CacheSource 入门

CacheSource同Memcached类似，像一个带有过期功能地Map容器，存放key-value数据。常见的使用场景就是存放HTTP的Session信息。RedKale把用户会话信息数据当做业务数据处理，而不是接入层的数据。WebSocket的连接态数据也是用CacheSource存储。key为WebSocket的groupid，value为WebSocket服务端节点的IP地址列表。

    public class UserService implements Service {

        //用户简单信息缓存
        private final Map<Integer, UserInfo> users = new ConcurrentHashMap<>();

        //使用CacheSource必须要指明泛型
        @Resource(name = "usersessions")
        protected CacheSource<String, Integer> sessions;

        //登录
        public RetResult<UserInfo> login(LoginBean bean) { //bean.sessionid 在接入层进行赋值
            UserInfo user = null;
            // 登陆逻辑 user = ...
            users.put(user.getUserid(), user);
            sessions.set(600, bean.getSessionid(), user.getUserid()); //session过期时间设置为10分钟
            return new RetResult<>(user);
        }

        //获取当前用户信息
        public UserInfo current(String sessionid) { //给HTTP的BaseServlet用
            Integer userid = sessions.getAndRefresh(sessionid);
            return userid == null ? null : users.get(userid);
        }

        //注销
        public void logout(String sessionid) {
            sessions.remove(sessionid);
        }
    }

以上是个简单的范例，用于用户模块存放sessionid。

### persistence.xml 配置说明

    <!-- 其配置算是标准的JPA配置文件的缩略版 -->
    <persistence>
        <!-- 系统基本库 -->
        <persistence-unit name="demouser">
            <!-- 为NONE表示不启动缓存，@Cacheable 失效； 非NONE值(通常用ALL)表示开启缓存。 -->
            <shared-cache-mode>NONE</shared-cache-mode>
            <properties>
                <property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/dbuser?characterEncoding=utf8"/>
                <!--
                    javax.persistence.jdbc.driver在JPA的值是JDBC驱动，RedKale有所不同，值应该是javax.sql.DataSource的子类。
                    为了兼容用户习惯，RedKale内置常见JDBC驱动到javax.sql.DataSource的映射关系：
                                           com.mysql.jdbc.Driver  ——————  com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource
                                         org.mariadb.jdbc.Driver  ——————  org.mariadb.jdbc.MySQLDataSource
                                 oracle.jdbc.driver.OracleDriver  ——————  oracle.jdbc.pool.OracleConnectionPoolDataSource
                    com.microsoft.sqlserver.jdbc.SQLServerDriver  ——————  com.microsoft.sqlserver.jdbc.SQLServerConnectionPoolDataSource
                    因此 com.mysql.jdbc.Driver 会被自动转换成 com.mysql.jdbc.jdbc2.optional.MysqlConnectionPoolDataSource
                -->
                <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
                <property name="javax.persistence.jdbc.user" value="root"/>
                <property name="javax.persistence.jdbc.password" value="123456"/>
    	    <!--
    		最大连接数，默认值：CPU数*16
                <property name="javax.persistence.connections.limit" value="32"/>
    	    -->
    	    <!--
    		包含的SQL模板，相当于反向LIKE，不同的JDBC驱动的SQL语句不一样，RedKale内置了MySQL、Oracle、Sqlserver的语句
                <property name="javax.persistence.contain.sqltemplate" value="LOCATE(${keystr}, ${column}) > 0"/>
                <property name="javax.persistence.notcontain.sqltemplate" value="LOCATE(${keystr}, ${column}) = 0"/>
    	    -->
            </properties>
        </persistence-unit>
        <!-- IM消息库 -->
        <persistence-unit name="demoim">
            <shared-cache-mode>NONE</shared-cache-mode>
            <properties>
                <!-- jdbc:mysql://127.0.0.1:3306/dbim?autoReconnect=true&amp;autoReconnectForPools=true&amp;characterEncoding=utf8 -->
                <property name="javax.persistence.jdbc.url" value="jdbc:mysql://127.0.0.1:3306/dbim?characterEncoding=utf8"/>
                <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver"/>
                <property name="javax.persistence.jdbc.user" value="root"/>
                <property name="javax.persistence.jdbc.password" value="123456"/>
            </properties>
        </persistence-unit>
    </persistence>