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:
<!-- In standalone -->
<dependency>
<groupId>com.github.braisdom</groupId>
<artifactId>objective-sql</artifactId>
<version>{objsql.version}</version>
</dependency>
Spring Boot 集成項目,請將下列代碼添加至dependencies:
<!-- In Spring Boot, you need add spring-jdbc dependency before -->
<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。
private static class MySQLConnectionFactory implements ConnectionFactory {
@Override
public Connection getConnection(String dataSourceName) throws SQLException {
try {
String url = "jdbc:mysql://localhost:4406/objective_sql";
String user = "root";
String password = "******";
return DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(e.getMessage(), e);
}
}
}
Databases.installConnectionFactory(new MySQLConnectionFactory());
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
# Configurations for multiple databases
extensions:
# The name of data source, which will match with @DomainModel definition
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 定義如下:
class Member {
private String memberNo;
private String name;
}
2)數據庫表定義如下:
create table members (
member_no varchar not null,
name varchar
);
類名:Member 在數據庫中對應的名稱為 members,而字段名memberNo 對應的列名為 member_no,而字段名name 沒有任何變化
4.1.1 關聯對象
1)Java 定義如下:
class Member {
private String memberNo;
private String name;
@Relation(relationType = RelationType.HAS_MANY)
private List<Order> orders;
}
class Order {
private String no;
private Long memberId;
@Relation(relationType = RelationType.BELONGS_TO)
private Member member;
}
2)數據庫表定義如下:
create table members (
member_no varchar not null,
name varchar
);
create table members (
member_no varchar not null,
member_id int(10) not null,
name varchar
);
通過上面的結構定義,可以看出幾個關鍵特征:
- 用于承載HAS_MANY 關聯對象的實例變量members 是由類型轉換成復數,而BELONGS_TO 與HAS_ONE則為單數
- Order 類中存在一個外鍵對應的實例變量memberId,同時在表中也存在一個member_id與其對應
- 其它規則與類與表轉換的規則一致
注意:所有類名在轉換為復雜時,遵循英文的規律,例如:person 對應 pepole
4.2 領域模型定義
@DomainModel
public class Member {
@Size(min = 5, max = 20)
private String no;
@Queryable
private String name;
private Integer gender;
private String mobile;
@Transient
private String otherInfo;
@Relation(relationType = RelationType.HAS_MANY)
private List<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); // Create a member without validating
Member.create(Member.newInstanceFrom(memberHash));
Member.create(new Member[]{newMember1, newMember2, newMember3}, false);
Member.update(1L, newMember, true); // Update a member by primary key and skip validationg
Member.update("name = ?", "name = ?", newName, oldName);
Member.destroy(1L); // Delete a member by primary key
Member.destroy("name = ?", "Mary");
4.5 事務
4.5.1 基于Annotation 的事務
// The method will be executed in a database thransaction
@Transactional
public static void makeOrder(Order order, OrderLine... orderLines) throws SQLException {
Order.create(order, false);
OrderLine.create(orderLines, false);
}
4.5.2 手動事務管理
// Transaction executing manually
Databases.executeTransactionally(((connection, sqlExecutor) -> {
Member.update(1L, newMember, true);
Member.update("name = ?", "name = ?", newName, oldName);
return null;
}));
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 分頁查詢
// Create a Page instance with current page and page size
Page page = 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 接口編程
Query query = Member.createQuery();
query.project("name").groupBy("name").having("COUNT(*) > 0").orderBy("name DESC");
List<Member> members = query.execute(Member.HAS_MANY_ORDERS);
// Paged querying with querying dynamically
Paginator paginator = Databases.getPaginator();
Page page = 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` 方法
Member newMember = new Member()
.setNo("100")
.setName("Pamela")
.setGender(1)
.setMobile("15011112222");
// Violations occurred in field 'no'
Validator.Violation[] violations = newMember.validate();
4.9.2 創建對象時 `validate`
Member newMember = new Member()
.setNo("100000")
.setName("Pamela")
.setGender(1)
.setMobile("15011112222");
Member.create(newMember);
Member.create(newMember, true); // Skip validation
4.10 自定義SQL
Member.execute("DELETE FROM members WHERE name = ?", "Pamela");
5 復雜SQL 編程指南
ObjectiveSQL 提供的復雜SQL 編程,其實是對SQL 語法的一種抽象和建模,以Java API 形式進行互相作用,使得復雜SQL 不再以字符串的形式出現在Java 中,從而實現動態化SQL 變得清晰易理解,不同的業務系統也可以基于ObjectiveSQL 對自身業務的再抽象和建模,實現SQL 邏輯的復用。
5.1 JOIN 查詢
5.1.1 隱式 Join
Member.Table member = Member.asTable();
Order.Table order = Order.asTable();
Select select = new Select();
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` )
GROUP BY `T0`.`NO` , `T0`.`name`
5.1.2 顯式Join
Member.Table member = Member.asTable();
Order.Table order = Order.asTable();
Select select = new Select();
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`
LEFT OUTER JOIN `orders` AS `T1` ON (`T1`.`member_id` = `T0`.`id` )
GROUP BY `T0`.`NO` , `T0`.`name`
5.2 分頁查詢
Member.Table member = Member.asTable();
Order.Table order = Order.asTable();
Paginator<Member> paginator = Databases.getPaginator();
Page page = Page.create(0, 20);
Select select = new Select();
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);
-- Counting SQL
SELECT COUNT(*) AS count_
FROM (
SELECT
`T0`.`NO`,
`T0`.`name`,
COUNT(*) AS `order_count`
FROM `members` AS `T0`, `orders` AS `T1`
WHERE (`T0`.`id` = `T1`.`member_id`)
GROUP BY `T0`.`NO`, `T0`.`name`
) T
-- Querying SQL
SELECT `T0`.`NO`, `T0`.`name`, COUNT(*) AS `order_count`
FROM `members` AS `T0`, `orders` AS `T1`
WHERE (`T0`.`id` = `T1`.`member_id`)
GROUP BY `T0`.`NO`, `T0`.`name`
LIMIT 0, 20
5.3 復雜表達式查詢
ObjectiveSQL 通過運算符重域技術使得Expression 也可以參與各類運算符計算,從而使得Java 代碼變得簡單易懂,而不是通過各類運算符方法進行計算。ObjectiveSQL 表達式計算時并不能夠與SQL 表達完匹配,默認情況下所有表達式均可以進行算術運算,在IntelliJ IDEA 中并不能給出完整的提醒,例如:JoinExpression 也可以進行算術運算,此時在IntelliJ IDEA 中并不會出現語法錯誤的提醒,但在執行運算過程中會拋出
UnsupportedArithmeticalException,該異常為RuntimeException 的子類。
Order.Table orderTable = Order.asTable();
Select select = new Select();
select.project((sum(orderTable.amount) / sum(orderTable.quantity) * 100).as("unit_amount"))
.from(orderTable)
.where(orderTable.quantity > 30 &&
orderTable.salesAt.between("2020-05-01 00:00:00", "2020-05-02 23:59:59"))
.groupBy(orderTable.memberId);
List<Order> orders = select.execute(Order.class);
SELECT ((((SUM(`T0`.`amount` ) / SUM(`T0`.`quantity` ) )) * 100)) AS unit_amount
FROM `orders` AS `T0`
WHERE ((`T0`.`quantity` > 30)
AND `T0`.`sales_at` BETWEEN '2020-05-01 00:00:00' AND '2020-05-02 23:59:59' )
GROUP BY `T0`.`member_id`
5.4 動態查詢
所謂動態查詢,實際上就是表達式的構建過程跟隨著參數的有無而變化,基于這種使用場景,ObjectiveSQL 設計了一個永真的邏輯表達式EternalExpression ,永真表達式是程序上的一種巧妙設計,使得代碼邏輯變得更清晰,即使所有參數均未賦值,整個表達式也會存在一個永的表達,確保最終SQL 語句的正常。
String[] filteredNo = {"202000001", "202000002", "202000003"};
int filteredQuantity = 0;
Order.Table orderTable = Order.asTable();
Select select = new Select();
LogicalExpression eternalExpression = new EternalExpression();
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)) AS unit_amount
FROM `orders` AS `T0`
WHERE ((1 = 1) AND `T0`.`NO` IN ('202000001', '202000002', '202000003') )
GROUP BY `T0`.`member_id`
6 高級使用
6.1 日志集成
由于 ObjectiveSQL 無法決定應用系統使用哪一個日志框架,所以ObjectiveSQL 并未集成任何第三方日志框架,確認使用JDK 自身的日志框架,如果應用系統需要使用自身的日志框架,并在系統啟動完成后注入ObjectiveSQL,請按下列方式集成(以Slf4j 為例)。
6.1.1 LoggerFactory 擴展實現
public class ObjLoggerFactoryImpl implements LoggerFactory {
private class ObjLoggerImpl implements Logger {
private final org.slf4j.Logger logger;
public ObjLoggerImpl(org.slf4j.Logger logger) {
this.logger = logger;
}
@Override
public void debug(long elapsedTime, String sql, Object[] params) {
logger.debug(createLogContent(elapsedTime, sql, params));
}
@Override
public void info(long elapsedTime, String sql, Object[] params) {
logger.info(createLogContent(elapsedTime, sql, params));
}
@Override
public void error(String message, Throwable throwable) {
logger.error(message, throwable);
}
private String createLogContent(long elapsedTime, String sql, Object[] params) {
String[] paramStrings = Arrays.stream(params)
.map(param -> String.valueOf(param)).toArray(String[]::new);
String paramString = String.join(",", paramStrings);
return String.format("[%dms] %s, with: [%s]",
elapsedTime, sql, String.join(",",
paramString.length() > 100 ? StringUtil
.truncate(paramString, 99) : paramString));
}
}
@Override
public Logger create(Class<?> clazz) {
org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(clazz);
return new ObjLoggerImpl(logger);
}
}
6.1.1 普通應用程序注入方式
public class Application {
public static void main(String[] args) {
Databases.installLoggerFactory(new ObjLoggerFactoryImpl());
// others
}
}
6.1.2 Spring Boot 應用程序注入方式
@SpringBootApplication
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication springApplication = new SpringApplication(Application.class);
springApplication.addListeners(new ApplicationListener<ApplicationReadyEvent>() {
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
Databases.installLoggerFactory(new ObjLoggerFactoryImpl());
}
});
springApplication.run(args);
}
}
6.2 基于SQL 語句的對象緩存
應用系統中對時間性不強的數據會進行數據緩存,通常會將數據緩存至redis 中,針對些特性,可以擴展ObjectiveSQL 的 SQLExecutor 接口輕易實現。
6.1.1 SQLExecutor 擴展實現
public class CacheableSQLExecutor<T> extends DefaultSQLExecutor<T> {
private static final List<Class<? extends Serializable>> CACHEABLE_CLASSES =
Arrays.asList(new Class[]{Member.class});
private static final Integer CACHED_OBJECT_EXPIRED = 60;
private static final String KEY_SHA = "SHA";
private Jedis jedis = new Jedis("localhost", 6379);
private MessageDigest messageDigest;
public CacheableSQLExecutor() {
try {
messageDigest = MessageDigest.getInstance(KEY_SHA);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e.getMessage(), e);
}
}
@Override
public List<T> query(Connection connection, String sql,
TableRowAdapter tableRowAdapter, Object... params)
throws SQLException {
Class<?> domainClass = tableRowAdapter.getDomainModelClass();
if (CACHEABLE_CLASSES.contains(domainClass)) {
if(!Serializable.class.isAssignableFrom(domainClass)) {
throw new IllegalArgumentException(String
.format("The %s cannot be serialized"));
}
messageDigest.update(sql.getBytes());
String hashedSqlId = new BigInteger(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);
SetParams expiredParams = SetParams.setParams().ex(CACHED_OBJECT_EXPIRED);
jedis.set(hashedSqlId.getBytes(), encodedObjects, expiredParams);
return objects;
}
}
return super.query(connection, sql, tableRowAdapter, params);
}
}
6.1.1 注入方式
public class Application {
public static void main(String[] args) {
Databases.installSqlExecutor(new CacheableSQLExecutor());
// others
}
}
Spring Boot 的注入方式去 LogFactory 的注入方式相同
6.3 ColumnTransition 擴展
ColumnTransition 是ObjectiveSQL 對外提供的一種數據類型轉的擴展接口,該接口的詳細定義請參考:ColumnTransition.java ,以日期形式為例,介紹ColumnTransition 的擴展方式。
public class SqlDateTimeTransition<T> implements ColumnTransition<T> {
@Override
public Object sinking(DatabaseMetaData databaseMetaData, T object,
TableRowAdapter tableRowDescriptor,
String fieldName, FieldValue fieldValue)
throws SQLException {
String databaseName = databaseMetaData.getDatabaseProductName();
if (fieldValue != null && fieldValue.getValue() != null) {
if (SQLite.equals(databaseName) || Oracle.equals(databaseName)) {
return fieldValue;
} else if (PostgreSQL.equals(databaseName)) {
if (fieldValue.getValue() instanceof Timestamp) {
return fieldValue.getValue();
} else if (fieldValue.getValue() instanceof Long) {
Instant value = Instant.ofEpochMilli((Long) fieldValue.getValue());
return Timestamp.from(value);
} else {
return Timestamp.valueOf(String.valueOf(fieldValue.getValue()));
}
} else {
return fieldValue;
}
}
return null;
}
@Override
public Object rising(DatabaseMetaData databaseMetaData,
ResultSetMetaData resultSetMetaData,
T object, TableRowAdapter tableRowDescriptor,
String columnName, Object columnValue) throws SQLException {
String databaseName = databaseMetaData.getDatabaseProductName();
try {
if (columnValue != null) {
if (SQLite.equals(databaseName)) {
Instant value = Instant
.ofEpochMilli(Long.valueOf(String.valueOf(columnValue)))
return Timestamp.from(value);
} else {
return columnValue;
}
}
} catch (DateTimeParseException ex) {
String message = String.format("Invalid raw DataTime of '%s' from database: %s",
columnName, columnValue);
throw new IllegalArgumentException(message, ex);
}
return null;
}
}
sinking 方法是將Java 中的值,轉換為數據庫所能接受的值,rising則為將數據庫中的值,轉換為Java 所能接受的值。
文章轉自:架構師必備






