IdView
基本概念:短关联
在介绍Id视图之前,我们要先介绍一个概念:短关联。
在介绍短关联之前,我们先看一看普通关联
- Java
- Kotlin
Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.allScalarFields()
.store(
Fetchers.BOOK_STORE_FETCHER
.allScalarFields()
)
.authors(
Fetchers.AUTHOR_FETCHER
.firstName()
.lastName()
)
);
System.out.println(book);
val book = bookRepository.findNullable(
1L,
newFetcher(Book::class).by {
allScalarFields()
store {
allScalarFields()
}
authors {
firstName()
lastName()
}
}
);
System.out.println(book);
代码中
- 通过关联属性
Book.store
关联抓取关联对象BookStore
,并期望获得关联对象的所有非关联属性 - 通过关联属性
Book.authors
关联抓取关联对象Author
,并期望获得关联对象的的id
(隐含+强制)、firstName
和lastName
输出结果为
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"store":{
"id":1,
"name":"O'REILLY",
"website":null
},
"authors":[
{
"id":2,
"firstName":"Alex",
"lastName":"Banks"
},
{
"id":1,
"firstName":"Eve",
"lastName":"Procello"
}
]
}
这里,聚合根对象Book
上的关联对象,BookStore
和Author
,具备除了id以外的其他属性,具有相对完善的信息。
更重要的是,非id
属性当然也包括关联属性,所以此数据结构可以多层嵌套甚至递归,这种关联也可以叫做“长关联”。
然而,并非所有时候都需要层次很深的数据结构。实际项目中,有时需要的只是一种非常简单的界面,如下
Book Form
在这个界面中
Book.store
是多对一关联,在界面上体现为单选下拉框Book.authors
是多对多关联,在界面上体现为多选下拉框
当然,如果候选数据很多,下拉列表不再是合理的设计,这时,改进为弹出对话框并在对话框中使用分页。但,这些UI细节不重要,和现有话题无关。
很明显,这时,用户只关注关联对象对象的id
,对关联对象的其他属性没有兴趣。
即, 希望关联对象只有id属性
为了让聚合根挂上一些只有id的的关联对象,我们可以改进代码。
- Java
- Kotlin
Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.allScalarFields()
.store() //无参数表示id only
.authors() //无参数表示id only
);
System.out.println(book);
val book = bookRepository.findNullable(
1L,
newFetcher(Book::class).by {
allScalarFields()
store() //无参数表示id only
authors() //无参数表示id only
}
);
System.out.println(book);
这次,我们得到了这样的数据结构
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"store":{
// 只有id属性
"id":1
},
"authors":[
{
// 只有id属性
"id":1
},
{
// 只有id属性
"id":2
}
]
}
在Hibernate中,这种只有id属性的对象被称为代理对象。
但是,只有id的关联对象,并没有关联id那么简单。让同样的的数据用关联id而非关联对象来表达的样子。
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"storeId": 1,
"authorIds":[1, 2]
}
很明显,对于短关联业务而言,关联id或其集合比只有id的关联对象或其集合简单。
Microsoft的解决方案
ADO.NET EF Core
是Microsoft的ORM ,让我们来看看其设计: https://learn.microsoft.com/en-us/ef/core/modeling/relationships?tabs=fluent-api%2Cfluent-api-simple-key%2Csimple-key
public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public int BlogId { get; set; }
public Blog Blog { get; set; }
}
不难发现
- 关联对象:
public Blog Blog { get; set; }
- 关联id:
public int BlogId { get; set; }
二者并存。
Jimmer借鉴ADO.NET EF Core
这种设计,提供了@IdView
属性。
IdView属性
声明视图属性
IdView属性由@org.babyfish.jimmer.sql.IdView
声明
- Java
- Kotlin
package com.example.model;
import org.babyfish.jimmer.sql.*;
import org.jetbrains.annotations.Nullable;
@Entity
public interface Book {
...省略其他属性...
@ManyToOne
@Nullable
BookStore store();
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
List<Author> authors();
@IdView // 关联对象store的id的视图
Long storeId();
// 关联对象集合authors中所有对象的id的视图
@IdView("authors")
List<Long> authorIds();
}
package com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Book {
...省略其他属性...
@ManyToOne
val store: BookStore?
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
val authors: List<Author>
@IdView // 关联对象store的id的视图
val storeId: Long?
// 关联对象集合authors中所有对象的id的视图
@IdView("authors")
val authorIds: List<Long>
}
其中:
-
Book.storeId
: 关联Book.store
对象的id的视图。-
storeId
本身以Id
结尾,这种情况下,可以不指定@IdView
注解的参数,Jimmer认为该视图属性的原始关联属性为Book.store
。 -
原始关联属性和IdView属性的可空性必须一致。
在这个例子中,
Book.store
属性可以为null,即,Java版本被@Nullable修饰
,Kotlin版本返回BookStore?
。因此,
Book.storeId
也必须可以为null,即,Java版本返回必须返回Long
而非long
,Kotlin版本必须返回Long?
而非Long
。否则,会导致编译错误。
-
-
Book.authorIds
: 关联Book.authors
对象集合中,所有Author对象的id形成的视图。authorIds
本身不以Id
结尾,必须指定@IdView
注解的参数,明确表示其原始关联为Book.authors
。这种情况下,需要这样做的原因是英文存在不规则名词复数变形的问题。
视图的本质
上文反复强调视图
二字是有原因的。IdView属性并没有自己的数据,它只是原始关联属性的视图。
IdView属性和原始关联属性是联动的,设置一个,必然影响另外一个。
-
设置视图属性,影响原始属性:
- Java
- Kotlin
// 设置视图属性
Book book = Immutables.createBook(draft -> {
draft.setStoreId(10L);
draft.setAuthorIds(Arrays.asList(100L, 101L));
});
// 打印原始属性
System.out.println("Store: " + book.store());
System.out.println("Authors:" + book.authors());// 设置视图属性
val book = Book {
storeId = 10L
authorIds = listOf(100L, 101L)
}
// 打印原始属性
println("Store: $book.store}")
println("Authors: ${book.authors}")打印结果:
Store: {"id":10}
Authors: [{"id":100},{"id":101}] -
设置原始属性,影响视图属性:
- Java
- Kotlin
// 设置原始属性
Book book = Immutables.createBook(draft -> {
draft.applyStore(store -> {
store.setId(10L).storeName("TURING")
});
draft.addIntoAuthors(author -> {
author.setId(101L);
author.setFirstName("Fabrice");
author.setLastName("Marguerie");
});
draft.addIntoAuthors(author -> {
author.setId(101L);
author.setFirstName("Steve");
author.setLastName("Eichert");
});
});
// 打印视图属性
System.out.println("StoreId: " + book.storeId());
System.out.println("AuthorIds:" + book.authorIds());// 设置原始属性
val book = Book {
store {
id = 10L
name = "TURING"
}
authors().addBy {
id = 100L;
firstName = "Fabrice"
lastName = "Marguerie"
}
authors().addBy {
id = 101L
firstName = "Steve"
lastName = "Eichert"
}
}
// 打印视图属性
println("Store: $book.storeId}")
println("Authors: ${book.authorIds}")打印结果:
StoreId: 10
AuthorIds: [100, 101]
这说明视图属性和原始属性是高度统一的,Jimmer仍然是以关联对象为核心的ORM框架,视图属性仅仅是一种语法糖。
除了接下来要讲解的视图属性对对象抓取器的影响外,视图属性对ORM和核心逻辑不会造成任何影响。
抓取IdView属性
- Java
- Kotlin
Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.allScalarFields()
.storeId()
.authorIds()
);
System.out.println(book);
val book = bookRepository.findNullable(
1L,
newFetcher(Book::class).by {
allScalarFields()
storeId()
authorIds()
}
);
System.out.println(book);
打印结果为
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"storeId": 1,
"authorIds":[1, 2]
}
对Jimmer动态实体而言,原始关联属性和视图属性绝对一致,要么 都可以访问,要么都缺失。
无论选择抓取原始关联属性,还是选择抓取IdView视图属性,都不会影响Jimmer底层执行逻辑,当然包括最终生成的SQL。
不同选择带来差异只有一个,原始关联属性和视图属性的Jackson可见性标志不同。
即,使用Jackson序列化时,被直接抓取的属性会被序列化,未被直接抓取的属性会被忽略。
请勿滥用
不借助DTO,希望实体本身能表达关联id,是唯一适合采用@IdView
的场景。
其他功能并不对关联属性是否有对应的@IdView
属性做任何假设。
-
在SQL DSL中使用关联id
即使实体的某个一对一或多对一关联属性没有对应的
@IdView
属性,也可以在SQL DSL中使用关联id表达式,例如- Java
- Kotlin
where(table.storeId().eq(2L));
where(table.storeId eq 2L)
当然,如果你对SQL DSL自动生成的关联id名称 (比如,这里的
storeId
) 并不满意,就可以提供@IdView
属性改变其名称。 -
在DTO语言中使用关联id
DTO语言根本不需要
@IdView
属性。即使实体的某个关联属性已经具备了对应的@IdView
属性,也不建议在DTO语言中使用它,因为这是一个脆弱的假设,一旦那个@IdView
属性被删除,DTO代码在同步修改前无法被正确编译。DTO语言应该直接使用关联属性,例如
export yourpackage.Book
-> package yourpackage.dto
input BookInput {
allScalarFields()
id(store) // as storeId
id(authors) as authorIds
}
specification BookSpecification {
like/i(name)
associatedIdIn(store) as storeIds
associatedIdNotIn(store) as excludedStoreIds
}