IdView
Basic Concepts: Short Associations
Before introducing IdView, we need to first introduce a concept: short associations.
Before introducing short associations, let's first look at a normal association
- 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);
In this code:
- Fetches associated
BookStore
object viastore
association property ofBook
, expecting to get all non-associative properties of associated object - Fetches associated
Author
objects viaauthors
association property ofBook
, expectingid
(implicit + mandatory),firstName
andlastName
of associated objects
The output is:
{
"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"
}
]
}
Here, associated objects BookStore
and Author
on aggregate root Book
have properties other than id
, with relatively complete information.
More importantly, non-id
properties of course also include associated properties, so this data structure can be nested multiple levels or even recursive. This kind of association can also be called a "long association".
However, not all cases require such a deep data structure. In actual projects, sometimes only a very simple UI is needed, like below:
Book Form
In this UI:
Book.store
is a many-to-one association, rendered as a dropdown selectorBook.authors
is a many-to-many association, rendered as a multiple dropdown selector
Of course, if there are too many options, a dropdown list is no longer a reasonable design. In this case, improve it to a popup dialog with pagination. But these UI details are unimportant and irrelevant to the current topic.
It is obvious that at this point, the user only cares about the id
of the associated object, and has no interest in other properties of the associated object.
That is, we want the associated object to only have the id property
To allow aggregate roots to be associated with some objects that only have id, we can improve the code.
- Java
- Kotlin
Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.allScalarFields()
.store() // no args means id only
.authors() // no args means id only
);
System.out.println(book);
val book = bookRepository.findNullable(
1L,
newFetcher(Book::class).by {
allScalarFields()
store() // no args means id only
authors() // no args means id only
}
);
System.out.println(book);
This time, we get a data structure like this:
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"store":{
// Only has id property
"id":1
},
"authors":[
{
// Only has id property
"id":1
},
{
// Only has id property
"id":2
}
]
}
In Hibernate, this kind of object with only id property is called a proxy object.
However, associated objects with only id are not as simple as just the id of the association. Let's look at the same data expressed with associated ids instead of associated objects:
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"storeId": 1,
"authorIds":[1, 2]
}
It is obvious that for short association use cases, associated ids or their collections are simpler than associated objects or their collections with only id.
Microsoft's Solution
ADO.NET EF Core
is Microsoft's ORM. Let's look at its design: 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; }
}
It's easy to see that:
- Associated object:
public Blog Blog { get; set; }
- Associated id:
public int BlogId { get; set; }
They coexist.
Jimmer learns from this design of ADO.NET EF Core
and provides the @IdView
property.
IdView Property
Declaring View Properties
IdView properties are declared with @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 {
...other properties omitted...
@ManyToOne
@Nullable
BookStore store();
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
List<Author> authors();
@IdView // View of store id
Long storeId();
// View of all author ids in authors collection
@IdView("authors")
List<Long> authorIds();
}
package com.example.model
import org.babyfish.jimmer.sql.*
@Entity
interface Book {
...other properties omitted...
@ManyToOne
val store: BookStore?
@ManyToMany
@JoinTable(
name = "BOOK_AUTHOR_MAPPING",
joinColumnName = "BOOK_ID",
inverseJoinColumnName = "AUTHOR_id"
)
val authors: List<Author>
@IdView // View of store id
val storeId: Long?
// View of all author ids in authors collection
@IdView("authors")
val authorIds: List<Long>
}
Where:
-
Book.storeId
: View of the id of associatedBook.store
object.-
Since
storeId
itself ends withId
, the parameter of@IdView
annotation can be omitted. Jimmer infers the original association property to beBook.store
. -
Nullability of original association property and IdView property must be consistent.
In this example,
Book.store
property can be null, i.e. annotated with@Nullable
in Java, or returnsBookStore?
in Kotlin.Therefore,
Book.storeId
must also be nullable, i.e. returnsLong
instead oflong
in Java, or returnsLong?
instead ofLong
in Kotlin.Otherwise it would cause compile error.
-
Book.authorIds
: View of ids of allAuthor
objects in associatedBook.authors
collection.authorIds
itself does not end withId
, so the parameter of@IdView
annotation must be specified to explicitly indicate its original association isBook.authors
.This is required in this case due to irregular noun pluralization in English.
-
Essence of View Properties
The emphasis on the word "view" above is intentional. IdView properties do not have their own data, they are just views of original association properties.
IdView properties and original association properties are linked. Setting one necessarily affects the other.
-
Setting view property affects original property:
- Java
- Kotlin
// Set view property
Book book = Immutables.createBook(draft -> {
draft.setStoreId(10L);
draft.setAuthorIds(Arrays.asList(100L, 101L));
});
// Print original property
System.out.println("Store: " + book.store());
System.out.println("Authors:" + book.authors());// Set view property
val book = Book {
storeId = 10L
authorIds = listOf(100L, 101L)
}
// Print original property
println("Store: $book.store}")
println("Authors: ${book.authors}")Prints:
Store: {"id":10}
Authors: [{"id":100},{"id":101}] -
Setting original property affects view property:
- Java
- Kotlin
// Set original property
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");
});
});
// Print view property
System.out.println("StoreId: " + book.storeId());
System.out.println("AuthorIds:" + book.authorIds());// Set original property
val book = Book {
store {
id = 10L
name = "TURING"
}
authors().addBy {
id = 100L;
firstName = "Fabrice"
lastName = "Marguerie"
}
authors().addBy {
id = 101L
firstName = "Steve"
lastName = "Eichert"
}
}
// Print view property
println("Store: $book.storeId}")
println("Authors: ${book.authorIds}")Prints:
StoreId: 10
AuthorIds: [100, 101]
This shows that view properties and original properties are highly unified. Jimmer is still a ORM framework that is core-associated-object-oriented. View properties are just syntactic sugar.
Except for the impact on ObjectFetcher to be explained below, view properties do not affect ORM and core logic at all.
Fetching IdView Properties
- 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);
Prints:
{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"storeId": 1,
"authorIds":[1, 2]
}
For Jimmer dynamic entities, original association properties and view properties are absolutely consistent. Either both can be accessed, or both are missing.
Whether choosing to fetch the original association property or choosing to fetch the IdView view property does not affect the underlying execution logic of Jimmer, including the ultimately generated SQL.
The only difference brought by different choices is that the Jackson visibility flag of original association properties and view properties are different.
That is, properties fetched directly will be serialized by Jackson, while properties not fetched directly will be ignored.
Here is the English translation of the file, with the code indentation preserved:
Do Not Abuse
Without the assistance of DTOs, hope that the entity itself can express associated ids, is the only scenario where it is appropriate to use @IdView
.
Other features make no assumptions about whether an association property has a corresponding @IdView
property.
-
Using associated IDs in the SQL DSL
Even if an entity's one-to-one or many-to-one association property does not have a corresponding
@IdView
property, you can still use associated id expressions in the SQL DSL, for example:- Java
- Kotlin
where(table.storeId().eq(2L));
where(table.storeId eq 2L)
Of course, if you are not satisfied with the auto-generated name for the associated id (e.g.,
storeId
here), you can provide an@IdView
property to change its name. -
Using associated ids in the DTO language
The DTO language does not require
@IdView
properties at all. Even if an entity's associated property already has a corresponding@IdView
property, it is not recommended to use it in the DTO language, as this is a fragile assumption. Once that@IdView
property is removed, the DTO code cannot be correctly compiled until it is synchronized with the change.The DTO language should directly use the association properties, for example:
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
}