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;
}
}