國產非常好的Java ORM 框架
技術標籤:# Springboot# Spring資料庫資料庫javaspring bootspringmysql
目錄
1 前言
ObjectiveSQL 是一個Java ORM 框架,它不僅是Active Record
模式在Java 中的應用,同時還針對複雜SQL 程式設計提供近乎完美的解決方案,使得Java 程式碼與SQL 語句有機的結合,改變了傳統SQL 的程式設計模型(以字串拼接為主的程式設計模型)。
ObjectiveSQL 專案分為兩部分:一部分是執行期Maven 依賴objective-sql
或objsql-springboot
,主要實現了基礎的ORM 特性和SQL 程式設計模型,另一部分是IntelliJ IDEA 外掛,相容Java 運算子過載和動態程式碼提示。
ObjectiveSQL 主要解決:
-
動態程式碼生成:基於領域模型(Domain Model),自動生成簡單SQL 程式設計程式碼,使應用系統開發只關注自身的業務特性,提升開發效率
-
可程式設計SQL:將SQL 中的控制原語、謂詞、函式以及過程化邏輯等抽象為Java 中的高階型別,與Java 融為一體,使得SQL 成為真正過程化、邏輯型程式語言,可封裝、可複用以及單元測試
-
表示式語法一致性:Java 語法與SQL 語法等價替換,包括:數學計算、函式呼叫、比較與邏輯計算表示式,Java 表示式可以直接轉換為SQL 表示式。
2 依賴安裝
2.1 IntelliJ IDEA 外掛安裝
Preferences/Settings
->Plugins
->Search with "ObjectiveSql" in market
->Install
2.2 Maven 整合
獨立應用程式,請將下列程式碼新增至dependencies
:
<!--Instandalone-->
<dependency>
<groupId>com.github.braisdom</groupId>
<artifactId>objective-sql</artifactId>
<version>{objsql.version}</version>
</dependency>
Spring Boot 整合專案,請將下列程式碼新增至dependencies
:
<!--InSpringBoot,youneedaddspring-jdbcdependencybefore-->
<dependency>
<groupId>com.github.braisdom</groupId>
<artifactId>objsql-springboot</artifactId>
<version>{objsql.version}</version>
</dependency>
最新版本請訪問 ObjectiveSQL,ObjSqlSpringBoot
2.3 Maven Compiler 引數配置
請將下列程式碼新增至pom.xml 中的<build>
/<plugins>
結點下:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>8</source>
<target>8</target>
<encoding>UTF-8</encoding>
<compilerArgs>
<arg>-Xplugin:JavaOO</arg>
</compilerArgs>
<annotationProcessorPaths>
<path>
<groupId>com.github.braisdom</groupId>
<artifactId>objective-sql</artifactId>
<version>${objsql.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
3 資料庫連線注入
3.1 獨立應用系統
以MySQL 為例,基於ConnectionFactory
構造資料連接獲取邏輯,並將其注入Databases
。
privatestaticclassMySQLConnectionFactoryimplementsConnectionFactory{
@Override
publicConnectiongetConnection(StringdataSourceName)throwsSQLException{
try{
Stringurl="jdbc:mysql://localhost:4406/objective_sql";
Stringuser="root";
Stringpassword="******";
returnDriverManager.getConnection(url,user,password);
}catch(SQLExceptione){
throwe;
}catch(Exceptione){
thrownewIllegalStateException(e.getMessage(),e);
}
}
}
Databases.installConnectionFactory(newMySQLConnectionFactory());
getConnection
方法中的的dataSourceName
引數僅在多資料來源的場景下使用,getConnection
方法可以根據不同的dataSourceName
返回不同的資料庫連線,其它場景下可以忽略該引數。
3.2 整合Spring Boot
應用系統基於Spring Boot 框架開發時,無需手動注入資料來源,請按下列方法進行配置即可:
spring:
profiles:
name:objective-sql-example
active:development
datasource:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:4406/objective_sql
username:root
password:******
hikari:
idle-timeout:10000
maximum-pool-size:10
minimum-idle:5
pool-name:Master
#Configurationsformultipledatabases
extensions:
#Thenameofdatasource,whichwillmatchwith@DomainModeldefinition
slave:
driver-class-name:com.mysql.cj.jdbc.Driver
url:jdbc:mysql://localhost:4406/objective_sql
username:root
password:******
hikari:
idle-timeout:10000
maximum-pool-size:10
minimum-idle:5
pool-name:Slave
其中
extensions
標記僅當多資料來源時需要配置,而slave
作為資料來源名稱,應該與DomainModel
中定義的資料來源名稱匹配,或者通過DomainModelDescriptro
中動態資料來源名稱匹配。
4 簡單SQL 程式設計指南
ObjectiveSQL 提供的簡單SQL 程式設計主要針對單表的相關SQL 使用,通過動態生成的Java API 為應用系統的開發提供便捷的開發體驗。
4.1 命名約定
4.1.1 類名與表名
預設情況下,ObjectiveSQL 以駝峰與下劃線的形式對Java 元素與資料庫元素進行互相轉換,示例如下:
1)Java 定義如下:
classMember{
privateStringmemberNo;
privateStringname;
}
2)資料庫表定義如下:
createtablemembers(
member_novarcharnotnull,
namevarchar
);
類名:
Member
在資料庫中對應的名稱為members
,而欄位名memberNo
對應的列名為member_no
,而欄位名name 沒有任何變化
4.1.2 關聯物件
1)Java 定義如下:
classMember{
privateStringmemberNo;
privateStringname;
@Relation(relationType=RelationType.HAS_MANY)
privateList<Order>orders;
}
classOrder{
privateStringno;
privateLongmemberId;
@Relation(relationType=RelationType.BELONGS_TO)
privateMembermember;
}
2)資料庫表定義如下:
createtablemembers(
member_novarcharnotnull,
namevarchar
);
createtablemembers(
member_novarcharnotnull,
member_idint(10)notnull,
namevarchar
);
通過上面的結構定義,可以看出幾個關鍵特徵:
-
用於承載
HAS_MANY
關聯物件的例項變數members
是由型別轉換成複數,而BELONGS_TO
與HAS_ONE
則為單數 -
Order
類中存在一個外來鍵對應的例項變數memberId
,同時在表中也存在一個member_id
與其對應 -
其它規則與類與錶轉換的規則一致
注意:所有類名在轉換為複雜時,遵循英文的規律,例如:person 對應 pepole
4.2 領域模型定義
@DomainModel
publicclassMember{
@Size(min=5,max=20)
privateStringno;
@Queryable
privateStringname;
privateIntegergender;
privateStringmobile;
@Transient
privateStringotherInfo;
@Relation(relationType=RelationType.HAS_MANY)
privateList<Order>orders;
}
ObjectiveSQL 會根據上述模型定義,自動生成基礎的SQL 程式設計相關方法和SQL 抽象模型定義
4.3 資料查詢
Member.countAll();
Member.count("name=?","braisdom");
Member.queryByPrimaryKey(1);
Member.queryFirst("id>?",1);
Member.query("id>?",1);
Member.queryAll();
4.4 資料更新
Member.create(newMember);
Member.create(newMember,true);//Createamemberwithoutvalidating
Member.create(Member.newInstanceFrom(memberHash));
Member.create(newMember[]{newMember1,newMember2,newMember3},false);
Member.update(1L,newMember,true);//Updateamemberbyprimarykeyandskipvalidationg
Member.update("name=?","name=?",newName,oldName);
Member.destroy(1L);//Deleteamemberbyprimarykey
Member.destroy("name=?","Mary");
4.5 事務
4.5.1 基於Annotation 的事務
//Themethodwillbeexecutedinadatabasethransaction
@Transactional
publicstaticvoidmakeOrder(Orderorder,OrderLine...orderLines)throwsSQLException{
Order.create(order,false);
OrderLine.create(orderLines,false);
}
4.5.2 手動事務管理
//Transactionexecutingmanually
Databases.executeTransactionally(((connection,sqlExecutor)->{
Member.update(1L,newMember,true);
Member.update("name=?","name=?",newName,oldName);
returnnull;
}));
4.6 關聯物件查詢
Member.queryAll(Member.HAS_MANY_ORDERS);
Member.queryFirst("id>?",Member.HAS_MANY_ORDERS,1);
Member.query("id>?",Member.HAS_MANY_ORDERS,1);
Member.queryByPrimaryKey(1,Member.HAS_MANY_ORDERS);
Member.queryByName("braisdom",Member.HAS_MANY_ORDERS);
上述程式碼中的
Member.HAS_MANY_ORDERS
屬性為ObjectiveSQL 自動生成,在特殊情況下,可以基於com.github.braisdom.objsql.relation.Relationship
自定義關聯關係的構建邏輯。
4.7 分頁查詢
//CreateaPageinstancewithcurrentpageandpagesize
Pagepage=Page.create(0,10);
PagedList<Member>members=Member.pagedQueryAll(page,Member.HAS_MANY_ORDERS);
PagedList<Member>members=Member.pagedQuery(page,"name=?","braisdom");
4.8 Query 介面程式設計
Queryquery=Member.createQuery();
query.project("name").groupBy("name").having("COUNT(*)>0").orderBy("nameDESC");
List<Member>members=query.execute(Member.HAS_MANY_ORDERS);
//Pagedqueryingwithqueryingdynamically
Paginatorpaginator=Databases.getPaginator();
Pagepage=Page.create(0,10);
PagedList<Member>pagedMembers=paginator
.paginate(page,query,Member.class,Member.HAS_MANY_ORDERS);
針對SQL 中的分組和排序,需要通過
Query
介面完成,同時Query
介面也可以進行分頁和關聯物件查詢。
4.9 Validation
ObjectiveSQL Validation 內部集成了Jakarta Bean Validation
詳細使用方法請參考:https://beanvalidation.org/
4.9.1 手工呼叫 `validate` 方法
MembernewMember=newMember()
.setNo("100")
.setName("Pamela")
.setGender(1)
.setMobile("15011112222");
//Violationsoccurredinfield'no'
Validator.Violation[]violations=newMember.validate();
4.9.2 建立物件時 `validate`
MembernewMember=newMember()
.setNo("100000")
.setName("Pamela")
.setGender(1)
.setMobile("15011112222");
Member.create(newMember);
Member.create(newMember,true);//Skipvalidation
4.10 自定義SQL
Member.execute("DELETEFROMmembersWHEREname=?","Pamela");
5 複雜SQL 程式設計指南
ObjectiveSQL 提供的複雜SQL 程式設計,其實是對SQL 語法的一種抽象和建模,以Java API 形式進行互相作用,使得複雜SQL 不再以字串的形式出現在Java 中,從而實現動態化SQL 變得清晰易理解,不同的業務系統也可以基於ObjectiveSQL 對自身業務的再抽象和建模,實現SQL 邏輯的複用。
5.1 JOIN 查詢
5.1.1 隱式 Join
Member.Tablemember=Member.asTable();
Order.Tableorder=Order.asTable();
Selectselect=newSelect();
select.project(member.no,member.name,count().as("order_count"))
.from(member,order)
.where(member.id.eq(order.memberId))
.groupBy(member.no,member.name);
List<Member>members=select.execute(Member.class);
SELECT`T0`.`NO`,`T0`.`name`,COUNT(*)AS`order_count`
FROM`members`AS`T0`,`orders`AS`T1`
WHERE(`T0`.`id`=`T1`.`member_id`)
GROUPBY`T0`.`NO`,`T0`.`name`
5.1.2 顯式Join
Member.Tablemember=Member.asTable();
Order.Tableorder=Order.asTable();
Selectselect=newSelect();
select.project(member.no,member.name,count().as("order_count"))
.from(member)
.leftOuterJoin(order,order.memberId.eq(member.id))
.groupBy(member.no,member.name);
List<Member>members=select.execute(Member.class);
SELECT`T0`.`NO`,`T0`.`name`,COUNT(*)AS`order_count`
FROM`members`AS`T0`
LEFTOUTERJOIN`orders`AS`T1`ON(`T1`.`member_id`=`T0`.`id`)
GROUPBY`T0`.`NO`,`T0`.`name`
5.2 分頁查詢
Member.Tablemember=Member.asTable();
Order.Tableorder=Order.asTable();
Paginator<Member>paginator=Databases.getPaginator();
Pagepage=Page.create(0,20);
Selectselect=newSelect();
select.project(member.no,member.name,count().as("order_count"))
.from(member,order)
.where(member.id.eq(order.memberId))
.groupBy(member.no,member.name);
PagedList<Member>members=paginator.paginate(page,select,Member.class);
--CountingSQL
SELECTCOUNT(*)AScount_
FROM(
SELECT
`T0`.`NO`,
`T0`.`name`,
COUNT(*)AS`order_count`
FROM`members`AS`T0`,`orders`AS`T1`
WHERE(`T0`.`id`=`T1`.`member_id`)
GROUPBY`T0`.`NO`,`T0`.`name`
)T
--QueryingSQL
SELECT`T0`.`NO`,`T0`.`name`,COUNT(*)AS`order_count`
FROM`members`AS`T0`,`orders`AS`T1`
WHERE(`T0`.`id`=`T1`.`member_id`)
GROUPBY`T0`.`NO`,`T0`.`name`
LIMIT0,20
5.3 複雜表示式查詢
ObjectiveSQL 通過運算子重域技術使得Expression 也可以參與各類運算子計算,從而使得Java 程式碼變得簡單易懂,而不是通過各類運算子方法進行計算。ObjectiveSQL 表示式計算時並不能夠與SQL 表達完匹配,預設情況下所有表示式均可以進行算術運算,在IntelliJ IDEA 中並不能給出完整的提醒,例如:JoinExpression
也可以進行算術運算,此時在IntelliJ IDEA 中並不會出現語法錯誤的提醒,但在執行運算過程中會丟擲UnsupportedArithmeticalException
,該異常為RuntimeException
的子類。
Order.TableorderTable=Order.asTable();
Selectselect=newSelect();
select.project((sum(orderTable.amount)/sum(orderTable.quantity)*100).as("unit_amount"))
.from(orderTable)
.where(orderTable.quantity>30&&
orderTable.salesAt.between("2020-05-0100:00:00","2020-05-0223:59:59"))
.groupBy(orderTable.memberId);
List<Order>orders=select.execute(Order.class);
SELECT((((SUM(`T0`.`amount`)/SUM(`T0`.`quantity`)))*100))ASunit_amount
FROM`orders`AS`T0`
WHERE((`T0`.`quantity`>30)
AND`T0`.`sales_at`BETWEEN'2020-05-0100:00:00'AND'2020-05-0223:59:59')
GROUPBY`T0`.`member_id`
5.4 動態查詢
所謂動態查詢,實際上就是表示式的構建過程跟隨著引數的有無而變化,基於這種使用場景,ObjectiveSQL 設計了一個永真的邏輯表示式EternalExpression
,永真表示式是程式上的一種巧妙設計,使得程式碼邏輯變得更清晰,即使所有引數均未賦值,整個表示式也會存在一個永的表達,確保最終SQL 語句的正常。
String[]filteredNo={"202000001","202000002","202000003"};
intfilteredQuantity=0;
Order.TableorderTable=Order.asTable();
Selectselect=newSelect();
LogicalExpressioneternalExpression=newEternalExpression();
if(filteredNo.length>0){
eternalExpression=eternalExpression.and(orderTable.no.in(filteredNo));
}
if(filteredQuantity!=0){
eternalExpression=eternalExpression.and(orderTable>filteredQuantity);
}
select.project((sum(orderTable.amount)/sum(orderTable.quantity)*100).as("unit_amount"))
.from(orderTable)
.where(eternalExpression)
.groupBy(orderTable.memberId);
List<Order>orders=select.execute(Order.class);
SELECT((((SUM(`T0`.`amount`)/SUM(`T0`.`quantity`)))*100))ASunit_amount
FROM`orders`AS`T0`
WHERE((1=1)AND`T0`.`NO`IN('202000001','202000002','202000003'))
GROUPBY`T0`.`member_id`
6 高階使用
6.1 日誌整合
由於 ObjectiveSQL 無法決定應用系統使用哪一個日誌框架,所以ObjectiveSQL 並未整合任何第三方日誌框架,確認使用JDK 自身的日誌框架,如果應用系統需要使用自身的日誌框架,並在系統啟動完成後注入ObjectiveSQL,請按下列方式整合(以Slf4j 為例)。
6.1.1 LoggerFactory 擴充套件實現
publicclassObjLoggerFactoryImplimplementsLoggerFactory{
privateclassObjLoggerImplimplementsLogger{
privatefinalorg.slf4j.Loggerlogger;
publicObjLoggerImpl(org.slf4j.Loggerlogger){
this.logger=logger;
}
@Override
publicvoiddebug(longelapsedTime,Stringsql,Object[]params){
logger.debug(createLogContent(elapsedTime,sql,params));
}
@Override
publicvoidinfo(longelapsedTime,Stringsql,Object[]params){
logger.info(createLogContent(elapsedTime,sql,params));
}
@Override
publicvoiderror(Stringmessage,Throwablethrowable){
logger.error(message,throwable);
}
privateStringcreateLogContent(longelapsedTime,Stringsql,Object[]params){
String[]paramStrings=Arrays.stream(params)
.map(param->String.valueOf(param)).toArray(String[]::new);
StringparamString=String.join(",",paramStrings);
returnString.format("[%dms]%s,with:[%s]",
elapsedTime,sql,String.join(",",
paramString.length()>100?StringUtil
.truncate(paramString,99):paramString));
}
}
@Override
publicLoggercreate(Class<?>clazz){
org.slf4j.Loggerlogger=org.slf4j.LoggerFactory.getLogger(clazz);
returnnewObjLoggerImpl(logger);
}
}
6.1.2 普通應用程式注入方式
publicclassApplication{
publicstaticvoidmain(String[]args){
Databases.installLoggerFactory(newObjLoggerFactoryImpl());
//others
}
}
6.1.3 Spring Boot 應用程式注入方式
@SpringBootApplication
@EnableAutoConfiguration
publicclassApplication{
publicstaticvoidmain(String[]args){
SpringApplicationspringApplication=newSpringApplication(Application.class);
springApplication.addListeners(newApplicationListener<ApplicationReadyEvent>(){
@Override
publicvoidonApplicationEvent(ApplicationReadyEventevent){
Databases.installLoggerFactory(newObjLoggerFactoryImpl());
}
});
springApplication.run(args);
}
}
6.2 基於SQL 語句的物件快取
應用系統中對時間性不強的資料會進行資料快取,通常會將資料快取至Redis 中,針對些特性,可以擴充套件ObjectiveSQL 的SQLExecutor
介面輕易實現。
6.2.1 SQLExecutor 擴充套件實現
publicclassCacheableSQLExecutor<T>extendsDefaultSQLExecutor<T>{
privatestaticfinalList<Class<?extendsSerializable>>CACHEABLE_CLASSES=
Arrays.asList(newClass[]{Member.class});
privatestaticfinalIntegerCACHED_OBJECT_EXPIRED=60;
privatestaticfinalStringKEY_SHA="SHA";
privateJedisjedis=newJedis("localhost",6379);
privateMessageDigestmessageDigest;
publicCacheableSQLExecutor(){
try{
messageDigest=MessageDigest.getInstance(KEY_SHA);
}catch(NoSuchAlgorithmExceptione){
thrownewIllegalArgumentException(e.getMessage(),e);
}
}
@Override
publicList<T>query(Connectionconnection,Stringsql,
TableRowAdaptertableRowAdapter,Object...params)
throwsSQLException{
Class<?>domainClass=tableRowAdapter.getDomainModelClass();
if(CACHEABLE_CLASSES.contains(domainClass)){
if(!Serializable.class.isAssignableFrom(domainClass)){
thrownewIllegalArgumentException(String
.format("The%scannotbeserialized"));
}
messageDigest.update(sql.getBytes());
StringhashedSqlId=newBigInteger(messageDigest.digest()).toString(64);
byte[]rawObjects=jedis.get(hashedSqlId.getBytes());
if(rawObjects!=null){
return(List<T>)SerializationUtils.deserialize(rawObjects);
}else{
List<T>objects=super.query(connection,sql,tableRowAdapter,params);
byte[]encodedObjects=SerializationUtils.serialize(objects);
SetParamsexpiredParams=SetParams.setParams().ex(CACHED_OBJECT_EXPIRED);
jedis.set(hashedSqlId.getBytes(),encodedObjects,expiredParams);
returnobjects;
}
}
returnsuper.query(connection,sql,tableRowAdapter,params);
}
}
6.2.2 注入方式
publicclassApplication{
publicstaticvoidmain(String[]args){
Databases.installSqlExecutor(newCacheableSQLExecutor());
//others
}
}
Spring Boot 的注入方式去 LogFactory 的注入方式相同
6.3 ColumnTransition 擴充套件
ColumnTransition 是ObjectiveSQL 對外提供的一種資料型別轉的擴充套件介面,該介面的詳細定義請參考:ColumnTransition.java ,以日期形式為例,介紹ColumnTransition 的擴充套件方式。
publicclassSqlDateTimeTransition<T>implementsColumnTransition<T>{
@Override
publicObjectsinking(DatabaseMetaDatadatabaseMetaData,Tobject,
TableRowAdaptertableRowDescriptor,
StringfieldName,FieldValuefieldValue)
throwsSQLException{
StringdatabaseName=databaseMetaData.getDatabaseProductName();
if(fieldValue!=null&&fieldValue.getValue()!=null){
if(SQLite.equals(databaseName)||Oracle.equals(databaseName)){
returnfieldValue;
}elseif(PostgreSQL.equals(databaseName)){
if(fieldValue.getValue()instanceofTimestamp){
returnfieldValue.getValue();
}elseif(fieldValue.getValue()instanceofLong){
Instantvalue=Instant.ofEpochMilli((Long)fieldValue.getValue());
returnTimestamp.from(value);
}else{
returnTimestamp.valueOf(String.valueOf(fieldValue.getValue()));
}
}else{
returnfieldValue;
}
}
returnnull;
}
@Override
publicObjectrising(DatabaseMetaDatadatabaseMetaData,
ResultSetMetaDataresultSetMetaData,
Tobject,TableRowAdaptertableRowDescriptor,
StringcolumnName,ObjectcolumnValue)throwsSQLException{
StringdatabaseName=databaseMetaData.getDatabaseProductName();
try{
if(columnValue!=null){
if(SQLite.equals(databaseName)){
Instantvalue=Instant
.ofEpochMilli(Long.valueOf(String.valueOf(columnValue)))
returnTimestamp.from(value);
}else{
returncolumnValue;
}
}
}catch(DateTimeParseExceptionex){
Stringmessage=String.format("InvalidrawDataTimeof'%s'fromdatabase:%s",
columnName,columnValue);
thrownewIllegalArgumentException(message,ex);
}
returnnull;
}
}
sinking 方法是將Java 中的值,轉換為資料庫所能接受的值,rising則為將資料庫中的值,轉換為Java 所能接受的值。
專案地址
開源地址:https://github.com/braisdom/ObjectiveSql