1. 程式人生 > 其它 >國產非常好的Java ORM 框架

國產非常好的Java ORM 框架

技術標籤:# Springboot# Spring資料庫資料庫javaspring bootspringmysql

目錄

1 前言

2 依賴安裝

2.1 IntelliJ IDEA 外掛安裝

2.2 Maven 整合

2.3 Maven Compiler 引數配置

3 資料庫連線注入

3.1 獨立應用系統

3.2 整合Spring Boot

4 簡單SQL 程式設計指南

4.1 命名約定

4.1.1 類名與表名

4.1.2 關聯物件

4.2 領域模型定義

4.3 資料查詢

4.4 資料更新

4.5 事務

4.5.1 基於Annotation 的事務

4.5.2 手動事務管理

4.6 關聯物件查詢

4.7 分頁查詢

4.8 Query 介面程式設計

4.9 Validation

4.9.1 手工呼叫 `validate` 方法

4.9.2 建立物件時 `validate`

4.10 自定義SQL

5 複雜SQL 程式設計指南

5.1 JOIN 查詢

5.1.1 隱式 Join

5.1.2 顯式Join

5.2 分頁查詢

5.3 複雜表示式查詢

5.4 動態查詢

6 高階使用

6.1 日誌整合

6.1.1 LoggerFactory 擴充套件實現

6.1.2 普通應用程式注入方式

6.1.3 Spring Boot 應用程式注入方式

6.2 基於SQL 語句的物件快取

6.2.1 SQLExecutor 擴充套件實現

6.2.2 注入方式

6.3 ColumnTransition 擴充套件

專案地址


1 前言

ObjectiveSQL 是一個Java ORM 框架,它不僅是Active Record模式在Java 中的應用,同時還針對複雜SQL 程式設計提供近乎完美的解決方案,使得Java 程式碼與SQL 語句有機的結合,改變了傳統SQL 的程式設計模型(以字串拼接為主的程式設計模型)。

ObjectiveSQL 專案分為兩部分:一部分是執行期Maven 依賴objective-sqlobjsql-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_TOHAS_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