View Properties
IdView
For data structures, frontends and backends have different preferences. Backends prefer structured data, while frontends prefer flattened data.
For IdOnly objects, Jimmer provides the @IdView annotation to easily flatten IdOnly objects into a field.
Modify the Example Entity Interface
In the example entity interface, two @IdView
properties are added, which are view properties. The value of these properties will be automatically synchronized with the value of the corresponding associated object.
For the IdView
of a collection property, the corresponding association property needs to be specified manually.
- Java
- Kotlin
@Entity
public interface Book {
...Other properties omitted...
@Nullable
@ManyToOne
BookStore store();
@ManyToMany
List<Author> authors();
@IdView
Long storeId();
@IdView("authors")
List<Long> authorIds();
}
@Entity
interface Book {
...Other properties omitted...
@ManyToOne
val store: BookStore?
@ManyToMany
val authors: List<Author>
@IdView
val storeId: Long?
@IdView("authors")
val authorsIds: List<Long>
}
Fetch @IdView
Properties
View properties need to be fetched manually. allScalarFields
and allTableFields
will not automatically fetch view properties.
- Java
- Kotlin
BookTable book = Tables.BOOK_TABLE;
List<Book> bookWithIdViews = sqlClient
.createQuery(book)
.where(book.edition().eq(3))
.select(
book.fetch(
Fetchers.BOOK_FETCHER
.allScalarFields()
.storeId()
.authorIds()
)
)
.execute();
System.out.println(bookWithIdViews);
var bookWithIdViews = sqlClient
.createQuery(Book::class) {
where(table.edition eq 3)
select(
table.fetchBy {
allScalarFields
storeId
authorIds
}
)
}
.execute()
println(bookWithIdViews)
The output is:
[
{
"id": 3,
"name": "Learning GraphQL",
"edition": 3,
"price": 51.00,
"storeId": 1,
"authorIds": [1,2]
}
...Other objects omitted
]
ManyToManyView
In some scenarios, the join table of a many-to-many association contains not only the IDs of both sides, but also some other fields.
A join table with additional fields besides the two IDs cannot be directly mapped to a ManyToMany
association. We have no choice but to create three entity types: the original two entities and an intermediate entity. Each side entity has a one-to-many association pointing to the intermediate entity, and the intermediate entity has two many-to-one associations pointing to the two side entities.
Fortunately, Jimmer provides @ManyToManyView to abstract a many-to-many association between the two side entities on top of this three-entity structure.
To better demonstrate, instead of using books and authors, a more realistic example with students, courses, and learning_link tables is used.
Prepare Data
For convenience, H2 database is used here. First create the table structure and initialize data with:
drop table course if exists;
drop table learning_link if exists;
drop table student if exists;
create table course(
id identity(100, 1) not null,
name varchar(50) not null,
academic_credit integer not null
);
create table student(
id identity(100, 1) not null,
name varchar(50) not null
);
create table learning_link(
id identity(100, 1) not null,
student_id int not null,
course_id int not null,
score int null
);
alter table learning_link
add constraint fk_learning_link__student
foreign key(student_id)
references student(id)
on delete cascade;
alter table learning_link
add constraint fk_learning_link__course
foreign key(course_id)
references course(id)
on delete cascade;
insert into student(id, name)
values (1, 'John'),
(2, 'Jane'),
(3, 'Jack'),
(4, 'Jill'),
(5, 'Joe');
insert into course(id, name, academic_credit)
values (1, 'Math', 100),
(2, 'English', 100),
(3, 'History', 100),
(4, 'Science', 100),
(5, 'Art', 100);
insert into learning_link(student_id, course_id, score)
values (1, 1, 100), (1, 2, 90), (1, 3, 80), (1, 4, 70), (1, 5, 60),
(2, 1, 90), (2, 2, 80), (2, 3, 70), (2, 4, 60), (2, 5, 100),
(3, 1, 80), (3, 2, 70), (3, 3, 60), (3, 4, 100), (3, 5, 90),
(4, 1, 70), (4, 2, 60), (4, 3, 100), (4, 4, 90), (4, 5, 80),
(5, 1, 60), (5, 2, 100), (5, 3, 90), (5, 4, 80), (5, 5, 70);
Define Entities
-
Student
- Java
- Kotlin
@Entity
public interface Student {
@Id
long id();
String name();
@OneToMany(mappedBy = "student")
List<LearningLink> learningLinks();
// ManyToMany view property
// prop: One-to-many to intermediate entity `learningLinks`
// deeperProp: Many-to-one from intermediate to target entity `course` (optional)
@ManyToManyView(prop = "learningLinks", deeperProp = "course")
List<Course> courses();
}@Entity
interface Student {
@Id
val id: Long
val name: String
@OneToMany(mappedBy = "student")
val learningLinks: List<LearningLink>
// ManyToMany view property
// prop: One-to-many to intermediate entity `learningLinks`
// deeperProp: Many-to-one from intermediate to target entity `course` (optional)
@ManyToManyView(prop = "learningLinks", deeperProp = "course")
val courses: List<Course>
} -
Course
- Java
- Kotlin
@Entity
public interface Course {
@Id
long id();
String name();
int academicCredit();
@OneToMany(mappedBy = "course")
List<LearningLink> learningLinks();
// ManyToMany view property
// prop: One-to-many to intermediate entity `learningLinks`
// deeperProp: Many-to-one from intermediate to target entity `student` (optional)
@ManyToManyView(prop = "learningLinks", deeperProp = "student")
List<Student> students();
}@Entity
interface Course {
@Id
val id: Long
val name: String
val academicCredit: Int
@OneToMany(mappedBy = "course")
val learningLinks: List<LearningLink>
// ManyToMany view property
// prop: One-to-many to intermediate entity `learningLinks`
// deeperProp: Many-to-one from intermediate to target entity `student` (optional)
@ManyToManyView(prop = "learningLinks", deeperProp = "student")
val students: List<Student>
} -
Intermediate Entity LearningLink
- Java
- Kotlin
@Entity
public interface LearningLink {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id();
// Many-to-one, points to Student entity
@ManyToOne
@OnDissociate(DissociateAction.DELETE)
Student student();
// Many-to-one, points to Course entity
@ManyToOne
@OnDissociate(DissociateAction.DELETE)
Course course();
Integer score();
}@Entity
interface LearningLink {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long
// Many-to-one, points to Student entity
@ManyToOne
@OnDissociate(DissociateAction.DELETE)
val student: Student
// Many-to-one, points to Course entity
@ManyToOne
@OnDissociate(DissociateAction.DELETE)
val course: Course
val score: Int
}
Fetch @ManyToManyView
Properties
With the introduction of the intermediate entity, Student and Course entities are no longer directly associated. Although they can be indirectly associated through a Student->LearningLink->Course
path, the path is too long.
Besides, the intermediate LearningLink
entity should be hidden as much as possible, only used when reading/writing scores.
@ManyToManyView properties (e.g. Student.courses
in this example) can be used in object fetchers to reduce the path length.
For example, the following query gets all course names for the student with id 1:
- Java
- Kotlin
Student student = sqlClient
.findNullable(
1L,
Fetchers.STUDENT_FETCHER
.name()
.courses(
Fetchers.COURSE_FETCHER
.name()
)
)
System.out.println(student);
val student = sqlClient
.findNullable(
1L,
newFetcher(Student::class).by {
name()
courses {
name()
}
}
)
println(student)
The generated SQL is:
-
Query
Student
select
tb_1_.ID, tb_1_.NAME
from STUDENT tb_1_
where tb_1_.ID = ? /* 1 */ -
Query
LearningLink
select
tb_1_.STUDENT_ID,
tb_1_.ID, tb_1_.COURSE_ID
from LEARNING_LINK tb_1_
where tb_1_.STUDENT_ID in (? /* 1 */) -
Query
Course
select
tb_1_.ID, tb_1_.NAME
from COURSE tb_1_
where tb_1_.ID in (
? /* 1 */, ? /* 2 */, ? /* 3 */,
? /* 4 */, ? /* 5 */
)
The output is:
{
"id": 1,
"name": "John",
"courses": [
{
"id": 1,
"name": "Math"
},
{
"id": 2,
"name": "English"
},
{
"id": 3,
"name": "History"
},
{
"id": 4,
"name": "Science"
},
{
"id": 5,
"name": "Art"
}
]
}
With many-to-many view properties, associated objects can be operated as conveniently as if the intermediate entity did not exist at all.