Skip to main content

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

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

In this code:

  • Fetches associated BookStore object via store association property of Book, expecting to get all non-associative properties of associated object
  • Fetches associated Author objects via authors association property of Book, expecting id (implicit + mandatory), firstName and lastName 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".

info

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 selector
  • Book.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.

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

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
}
]
}
note

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:

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

Where:

  • Book.storeId: View of the id of associated Book.store object.

    • Since storeId itself ends with Id, the parameter of @IdView annotation can be omitted. Jimmer infers the original association property to be Book.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 returns BookStore? in Kotlin.

      Therefore, Book.storeId must also be nullable, i.e. returns Long instead of long in Java, or returns Long? instead of Long in Kotlin.

      Otherwise it would cause compile error.

    • Book.authorIds: View of ids of all Author objects in associated Book.authors collection.

      authorIds itself does not end with Id, so the parameter of @IdView annotation must be specified to explicitly indicate its original association is Book.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.

info

IdView properties and original association properties are linked. Setting one necessarily affects the other.

  • Setting view property affects original property:

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

    Prints:

    Store: {"id":10}
    Authors: [{"id":100},{"id":101}]
  • Setting original property affects view property:

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

    Prints:

    StoreId: 10
    AuthorIds: [100, 101]
tip

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

Book book = bookRepository.findNullable(
1L,
Fetchers.BOOK_FETCHER
.allScalarFields()
.storeId()
.authorIds()
);
System.out.println(book);

Prints:

{
"id":1,
"name":"Learning GraphQL",
"edition":1,
"price":45,
"storeId": 1,
"authorIds":[1, 2]
}
tip

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

caution

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:

    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
    }