Language: English | 简体中文
Get Best practice or demo to quick start.
This is a lightweight persistence layer framework, provides a complete database operation solution, through encapsulation and abstraction, simplifies the complexity of database access, but also provides flexibility and scalability for developers. simple, stable and efficient as the goal, some features following:
- execute procedure/function
- simple transaction
- prepare sql
- sql in file
- dynamic sql
- interface mapping
- entity mapping
java 8+
<dependency>
<groupId>com.github.chengyuxing</groupId>
<artifactId>rabbit-sql</artifactId>
<version>10.1.2</version>
</dependency>- support rabbit-sql autoconfigure;
- compatible with spring jdbc transaction;
- compatible mybatis、spring-data-jpaand so on to use transaction together;
Get some usage from document.
Dynamic sql test:
Xql interface mapper generate:
Plugin marketplace: Rabbit sql and documentation.
There are two ways, Baki interface oriented and xql mapping interface oriented, that are compatible with each other.
Datasource datasource = new HikariDataSource();
...
BakiDao baki = new BakiDao(dataSource);
XQLFileManager xqlFileManager = new XQLFileManager("xql-file-manager.yml");
...
baki.setXqlFileManager(xqlFileManager);Supports registered xql file mapping(BakiDao#proxyXQLMapper) to interface which annotated with @XQLMapper, do some sql operation by invoke dynamic proxy method.
ExampleMapper mapper = baki.proxyXQLMapper(ExampleMapper.class)If Springboot is used, the annotation @XQLMapperScan can be directly added to the boot class to automatically register it in the context through the interface scanning mechanism, and then the interface can be injected Specific can consult document .
example.xql
/*[queryGuests]*/
select * from test.guest where id = :id;
/*[addGuest]*/
insert into test.guest(name, address, age)values (:name, :address, :age);ExampleMapper.java
@XQLMapper("example")
public interface ExampleMapper {
List<DataRow> queryGuests(Map<String, Object> args);
@XQL(value = "queryGuests")
Optional<Guest> findById(@Arg("id") int id);
@XQL(type = SqlStatementType.insert)
int addGuest(DataRow dataRow);
}By default, all methods behaviors are depends on method prefix and sql name mapping to method name if matched, otherwise use @XQL(value = "sql name",type = SqlStatementType.insert) annotate method to specify the sql name and modify the default query behave, methods must follow the rules:
| Sql type | Method prefix |
|---|---|
| select | select | query | find | get | fetch | search |create |
| insert | insert | save | add | append |create |
| update | update | modify | change |
| delete | delete | remove |
| procedure / function | call | proc | func |
Argument type:
- Argument dictionary:
DataRow|Map<String,Object>|<JavaBean> - Argument List: Each argument annotated with
@Arg
| Return Type | sql Type(Type) | Remark |
|---|---|---|
List<DataRow/Map<String,Object>/<JavaBean>> |
query | |
Set<DataRow/Map<String,Object>/<JavaBean>> |
query | |
Stream<DataRow/Map<String,Object>/<JavaBean>> |
query | |
Optional<DataRow/Map<String,Object>/<JavaBean>> |
query | |
Map<String,Object> |
query | |
PagedResource<DataRow/Map<String,Object>/<JavaBean>> |
query | @CountQuery(optional) |
IPageable |
query | @CountQuery(optional) |
Long, Integer, Double |
query | |
<JavaBean> |
query | |
DataRow |
query, procedure, function, ddl, plsql, unset | |
int/Integer |
insert, update, delete |
If the method annotated with special annotations, method will not mapping to xql file sql name, it just execute by the itself:
@Procedure@Function
Basic interface to access database.
baki.query("select … where id = :id").arg("id", "1")baki.query("&my.users")flowchart LR;
A[#quot;select ...#quot;] --> Baki["query()"];
B[#quot;&my.users#quot;] --> X[XQLFileManager];
X --> Baki;
click X href "#XQLFileManager" "go to defenition"
Except accept sql statement, also support accept sql by name, name start with
&to get sql from sql file manager.
Example
- Stream-query
try(Stream<DataRow> fruits = baki.query("select * from fruit").stream()){
fruits.forEach(System.out::println);
}Query will not truly execute until invoke Stream terminal operation(e.g.
foreach()), use jdk7 try-with-resource to release connection when query complete.
- Paging
Default pageable query will auto generate paging statement and count statement by database.
Built-in support oracle, mysql, postgresql, sqlite, mariadb, db2, or extends class com.github.chengyuxing.sql.plugins.PageHelperProvider and set to BakiDao get support.
PagedResource<DataRow> resource = baki.query("select ... where id < :id")
.arg("id", 8)
.pageable(1, 7)
.collect();- Custom paging
/pgsql/data.sql
/*[custom_paged]*/
with t as (
select * from test.region
where id > :id limit :limit offset :offset
)
select * from t;
;PagedResource<DataRow> res = baki.query("&data.custom_paged")
.pageable(1, 7)
.count("select count(*) ... where id > :id")
.disableDefaultPageSql()
.collect();
disableDefaultPageSql()will not wrap sql to generate paging statement of name custom_paged.count statement is required now.
baki.call("{:res = call test.sum(:a, :b)}",
Args.of("res", Param.OUT(StandardOutParamType.INTEGER))
.add("a", Param.IN(34))
.add("b", Param.IN(56))
).getOptional("res")
.ifPresent(System.out::println);If postgresql, you must use transaction when returns cursor.
To achieve the purest execution of SQL in this framework, the entity mapping logic will no longer be hard-coded internally to reach the maximum compatibility with various frameworks. The core of entity mapping is the DataRow class, which provides the methods toEntity and ofEntity. If there are special entity mapping requirements, Custom parsing can be achieved by configuring the attribute BakiDao#entityFieldMapper. For example, if the standard entity of JPA has the annotation @Column, then a simple mapping implementation is as follows:
class MyEntityFieldMapper implements EntityFieldMapper {
@Override
public String apply(Field field) {
if (field.isAnnotationPresent(Column.class)) {
Column column = field.getAnnotation(Column.class);
return column.name();
}
return field.getName();
}
}In other frameworks, even custom field annotation parsing can meet the requirements and is no longer limited by the entity mapping rules of other frameworks.
Use of transactions follows thread isolation:
Tx.using(() -> {
......
});Prepare sql support named parameter style, e.g.
:name (jdbc standard named parameter syntax, sql will be prepare saftly, parameter name is name )
Named parameter will be compile to
?, Recommend to use prepare sql for avoid sql injection.
${[!]name} (string template holder, not prepare, use for sql fragment reuse)
2 styles:
${name}: if value type is boxed type array(String[], Integer[]...) or collection (Set, List...), just expand value and replace.${!name}: name start with!, if value type is boxed type array(String[], Integer[]...) or collection(Set, List...), expand value and safe quote, then replace.
sql:
select ${fields} from ... where word in (${!words}) or id = :id;args:
Args.<Object>of("id","uuid")
.add("fields", Arrays.asList("name", "age"))
.add("words", Arrays.asList("I'm OK!", "book", "warning"));generate sql:
select name, age from ... where id in ('I''m Ok!', 'book', 'warning') or id = ?;Dynamic SQL depends on XQLFileManager, based on resolve special comment mark, dynamic compile without breaking sql file standards.
Comment mark must be pair and follows open-close tag.
It should be particularly noted that if a value type string literal is not a pure number or keyword (null, blank, true, false), it does not need to be enclosed in quotation marks and defaults to a string. For example, in :name = bob, the quotation mark for 'bob' is not necessary.
The precondition check statement throws an exception message (CheckViolationException) if the condition is met.
Before the database actually executes the sql, conduct a validity verification of the parameters to avoid parameter type error anomalies at the database level and save resources.
-- #check :id > 10 throw 'ID cannot gt 10.'
...Variable definition statements: Variable values can be constants or passed parameters processed through pipelines. By extending pipelines, various complex variable definitions can be achieved.
-- #var list = 'cyx,jack,mike' | split(',')
-- #var newId = :id
select * from table where id = :newId and name in (
-- #for item of :list
:item
-- #done
)The IF conditional judgment statement has the same logical effect as the if in programming languages.
-- #if :user <> null
...
-- #else (optional)
...
-- #fiGuard statement: If the condition is met, the branch processing logic is executed; otherwise, #throw is executed to throw the exception information and terminate all subsequent operations.
-- #guard :user <> blank
...
-- #throw 'message'The switch flow control statement has the same effect as the switch in programming languages. It matches each case branch in sequence and directly exits the entire switch when the first condition is met.
-- #switch :name
-- #case 'a', 'b', c
...
-- #break
-- #case 'd'
...
-- #break
...
-- #default
...
-- #break
-- #endThe choose flow control statement is similar in effect to the switch statement. It matches each when branch in sequence and directly exits the entire choose when the first condition is met.
-- #choose
-- #when :id >= 0
...
-- #break
...
-- #default
...
-- #break
-- #endThe for loop statement, similar to programming languages, traverses a collection and accumulates the contents within the loop body.
-- #for item,idx of :list delimiter ',' open '' close ''
...
-- #doneFor expression syntax:
Keywords: of delimiter open close
item[,index] of :list [|pipe1|pipeN|... ] [delimiter ','] [open ''] [close ''][...]means optional;itemis current value,indexis current index;:listis iterator, it can be following some pipes to do something special;delimiteris a separator for concat each item,,is default;openis a prefix which will pe prepend to result if result is not empty;closeis a suffix which will be append to result if result is not empty.
Data's key is starts with :.
A simple expression syntax following:
!(:id >= 0 || :name | length <= 3) && :age > 21| Operator | Means |
|---|---|
| < | less than |
| > | great than |
| >= | great than or equal |
| <= | less than or equal |
| ==, = | equal |
| !=, <> | not equal |
| ~ | regex find |
| !~ | regex not find |
| @ | regex match |
| !@ | regex not match |
-
Support logic symbol:
||,&&,!; -
Support nest bracket:
(,); -
Support data type: string(
""、''), number(12、3.14), boolean(true,false); -
Built-in constants:
null,blank(null, empty string、empty array、empty collection);
use custom pipe to implement more features.
Syntax look like :id | upper | is_id_card | ... e.g.
flowchart LR;
A[abc] --upper--> B[ABC];
B --is_id_card--> C[false];
C --pipeN--> D[...]
-- get value by name through length pipe and compare with number 3
:name|length <= 3Implement com.github.chengyuxing.common.script.pipe.IPipe interface and add to XQLFileManager to use pipe.
Built-In pipes:
- length: get length of string value;
- upper: convert to upper case;
- lower: convert to lower case;
- kv: object or map convert to keyValues
List<KeyValue>; - nvl: if value is null, the default will be returned, e.g.
nvl('default'); - type: returns the Java object type name;
- split: split string to string array by delimiter, e.g.
split(','); - in: returns the params is containing the value or not, e.g.
in('a','b','c')
Here is about dynamic generate named parameter sql, named parameter will be prepare compile to ? to keep safe.
for is useful at sql in statement, it can be build prepared sql:
/*[query]*/
select * from test.user where id = 1
-- #for id of :ids delimiter ', ' open ' or id in (' close ')'
-- #if :id >= 8
:id
-- #fi
-- #doneTo maintain sql syntax integrity, highlighting syntax errors does not occur in ides with syntax checking, and the following equivalent writing is recommended:
select * from test.user where id = 1
-- #if :ids != blank
or id in (
-- #for id of :ids delimiter ', '
-- #if :id >= 8
:id
-- #fi
-- #done
)
-- #fi
;{"ids": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]}example above will be generate sql and variables:
select * from test.user where id = 1
or id in (
:_for.id_0_7,
:_for.id_0_8,
:_for.id_0_9,
:_for.id_0_10,
:_for.id_0_11
){
"_for": {
"id_0_0": 1,
"id_0_2": 3,
"id_0_1": 2,
"id_0_10": 11,
"id_0_11": 12,
"id_0_4": 5,
"id_0_3": 4,
"id_0_6": 7,
"id_0_5": 6,
"id_0_8": 9,
"id_0_7": 8,
"id_0_9": 10
}
}For a few special places to explain:
- If for loop result is not empty,
openmeansor id in(will prepend to result,closemeans)will append to result; - Variable starts with
:in sql means it's a named parameter which will prepare compile;
for work with update statement sets part:
/*[update]*/
update test.user
set
-- #for set of :sets | kv delimiter ', '
${set.key} = :set.value
-- #done
where id = :id;{
"id": 10,
"sets": {
"name": "abc",
"age": 30,
"address": "kunming"
}
}example above will generate sql and variables:
update test.user
set
address = :_for.set_0_0.value,
name = :_for.set_0_1.value,
age = :_for.set_0_2.value
where id = :id{
"id": 10,
"_for": {
"set_0_2": {
"key": "age",
"value": 30
},
"set_0_1": {
"key": "name",
"value": "abc"
},
"set_0_0": {
"key": "address",
"value": "kunming"
}
}
}Explain:
:setsis a map, it convert toList<KeyValue>by pipekv, so it can be work with for expression;
Concat different sql statement by database name:
/*[query]*/
select * from test.user
where id = 3
-- #if :_databaseId == 'postgresql'
...
-- #fi
-- #if :_databaseId == 'oracle'
...
-- #fi
;- Built-In variable
_databaseIdis current database name.
A little important details you need to know.
Default implement of interface Baki, support some basic operation.
-
If XQLFileManager configured , you can manage sql in file and support dynamic sql;
-
Default named parameter start with
:, it can be customized by specific propertynamedParamPrefix, e.g.where id = ?id
-
if pageable query not support your database, implement custom page helper provider to property
globalPageHelperProviderget support.
Custom sql interceptor, default:
(rawSql, parsedSql, args, metaData) -> StringCustom prepared sql statement parameter value handler, default:
(ps, index, value, metaData) -> JdbcUtil.setStatementValue(ps, index, value)Default: null
Global paging help provider that implements this class to override or extend if the built-in does not satisfy the current database.
Default: null
SQL Execution observer to view the execution of each sql, such as the execution time.
Default: null
XQL file manager, support unified management of SQL, according to the SQL name to obtain SQL execution, parsing dynamic SQL, support interface mapping, etc.
Default: 1000
The JDBC low-level batch operation executes the number of data submitted each time.
默认值::
Prepared SQL named parameter prefix, used to mark the prepared parameter placeholder, and finally compiled to ? .
Default: page
Internal paging Query Page number Default parameter name.
Default: size
Internal paging Indicates the default parameter name for querying the number of entries per page.
Default: 0
Query timeout handler, when the query times out, will throw an exception, the specific implementation effect depends on the JDBC driver: Statement#setQueryTimeout(int) 。
Default: null
The query cache manager caches query results to improve performance, increase concurrency, and reduce database stress.
Make a reasonable automatic cache expiration policy to prevent data from being updated in time.
Default:the entity field name
All operations involving the return of entities within the framework's internal interfaces will use this function to map the fields and convert the values.
e.g. baki.entity(class)
SQL file manager extends standard sql comment implement more features, for support dynamic sql and expression scripts logic judgment without breaking standard sql structure, also it's more powerful SQL file resolver.
you can get sql syntax highlight, intelligent suggestions and error check when using sql develop tools cause support sql file with extension .sql, dba developer work with java developer together so easy.
Supported file extension with .sql or .xql, you can write any standard sql comment in file, format reference template.xql.
💡 Recommend use .xql file to get plugin supports.
Every managed sql file must follows "k-v" structure, and each sql object must be marked with a semicolon ; Ending, e.g.
my.sql
/*#some description...#*/
/*[query]*/
/*#some more
description...#*/
select * from test."user" t ${part1};
/*part 1*/
/*{part1}*/
where id = :id
${order};
/*{order}*/
order by id;
...-
Sql object description formatter is
/*#some description...#*/; -
Sql object name formatter is
/*[name]*/, sql object supports nest sql fragment by using${fragment name}holder; -
Sql fragment name formatter is
/*{name}*/, sql fragment supports nest sql fragment by using${fragment name}holder to reuse, as above examplemy.sql:select * from test."user" t where id = :id order by id;
-
new XQLFileManager()
Default options:
xql-file-manager.yml!pathtag use for merge list to path string.constants: # base: &basePath pgsql files: # use !path tag merge list to "pgsql/other.xql" # dt: !path [ *basePath, other.xql ] # other: another.xql pipes: # upper: org.example.Upper charset: UTF-8 named-param-prefix: ':' database-id:
Sql file mapping dictionary, key is alias, value is sql file name, you can get sql statement by alias.your_sql_name when sql file added, as above example: my.sql;
Custom pipe dictionary, key is pipe name, value is pipe class, for dynamic sql expression's value, get more dynamic sql expression's features by implement custom pipe;
String template constant pool. If there is a template placeholder for ${name} in the SQL, look it up from the constant pool and replace it if found.
Encoding used to parse XQL files, default: UTF-8。
It mainly works with plug-in parsing to perform named parameter dynamic SQL.
The main effect is that the plug-in parses parameters when executing dynamic SQL.

