跳到主要内容

IdView

基本概念:短关联

在介绍Id视图之前,我们要先介绍一个概念:短关联。

在介绍短关联之前,我们先看一看普通关联

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);

代码中

  • 通过关联属性Book.store关联抓取关联对象BookStore,并期望获得关联对象的所有非关联属性
  • 通过关联属性Book.authors关联抓取关联对象Author,并期望获得关联对象的的id(隐含+强制)、firstNamelastName

输出结果为

{
"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上的关联对象,BookStoreAuthor,具备除了id以外的其他属性,具有相对完善的信息。

更重要的是,非id属性当然也包括关联属性,所以此数据结构可以多层嵌套甚至递归,这种关联也可以叫做“长关联”。

信息

然而,并非所有时候都需要层次很深的数据结构。实际项目中,有时需要的只是一种非常简单的界面,如下

Book Form

在这个界面中

  • Book.store是多对一关联,在界面上体现为单选下拉框
  • Book.authors是多对多关联,在界面上体现为多选下拉框

当然,如果候选数据很多,下拉列表不再是合理的设计,这时,改进为弹出对话框并在对话框中使用分页。但,这些UI细节不重要,和现有话题无关。

很明显,这时,用户只关注关联对象对象的id,对关联对象的其他属性没有兴趣。

即, 希望关联对象只有id属性

为了让聚合根挂上一些只有id的的关联对象,我们可以改进代码。

Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.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

这段C#代码从上面的链接的页面复制
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声明

Book.java
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();
}

其中:

  • 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属性和原始关联属性是联动的,设置一个,必然影响另外一个。

  • 设置视图属性,影响原始属性:

    // 设置视图属性
    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());

    打印结果:

    Store: {"id":10}
    Authors: [{"id":100},{"id":101}]
  • 设置原始属性,影响视图属性:

    // 设置原始属性
    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());

    打印结果:

    StoreId: 10
    AuthorIds: [100, 101]
提示

这说明视图属性和原始属性是高度统一的,Jimmer仍然是以关联对象为核心的ORM框架,视图属性仅仅是一种语法糖。

除了接下来要讲解的视图属性对对象抓取器的影响外,视图属性对ORM和核心逻辑不会造成任何影响。

抓取IdView属性

Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.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表达式,例如

    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
    }