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 
BookStoreobject viastoreassociation property ofBook, expecting to get all non-associative properties of associated object - Fetches associated 
Authorobjects viaauthorsassociation property ofBook, expectingid(implicit + mandatory),firstNameandlastNameof 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.storeis a many-to-one association, rendered as a dropdown selectorBook.authorsis 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.storeobject.- 
Since
storeIditself ends withId, the parameter of@IdViewannotation 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.storeproperty can be null, i.e. annotated with@Nullablein Java, or returnsBookStore?in Kotlin.Therefore,
Book.storeIdmust also be nullable, i.e. returnsLonginstead oflongin Java, or returnsLong?instead ofLongin Kotlin.Otherwise it would cause compile error.
 - 
Book.authorIds: View of ids of allAuthorobjects in associatedBook.authorscollection.authorIdsitself does not end withId, so the parameter of@IdViewannotation 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
@IdViewproperty, 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.,
storeIdhere), you can provide an@IdViewproperty to change its name. - 
Using associated ids in the DTO language
The DTO language does not require
@IdViewproperties at all. Even if an entity's associated property already has a corresponding@IdViewproperty, it is not recommended to use it in the DTO language, as this is a fragile assumption. Once that@IdViewproperty 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
}