ManyToManyView
Limitations of classic ORM associations
In Basic Mapping/Associative Mapping, we learned about the classic associative mappings in ORM, including one-to-one, many-to-one, one-to-many and many-to-many.
However, there is one scenario that makes the choice of mapping mode very tangled. To show this scenario, let's start with a familiar scenario.
Undisputed many-to-many association
Let's look at a piece of DDL
create table book(
    ...omit...
)engine=innodb;;
create table author(
    ...omit...  
) engine=innodb;
create table book_author_mapping(
    book_id bigint unsigned not null,
    author_id bigint unsigned not null
) engine=innodb;
alter table book_author_mapping
    add constraint pk_book_author_mapping
        primary key(book_id, author_id)
;
alter table book_author_mapping
    add constraint fk_book_author_mapping__book
        foreign key(book_id)
            references book(id)
                on delete cascade
;
alter table book_author_mapping
    add constraint fk_book_author_mapping__author
        foreign key(author_id)
            references author(id)
                on delete cascade
;
In this piece of DDL, book_author_mapping table is very special, it only has two foreign keys, one pointing to book table, and one pointing to author table. This kind of subtable with only two foreign keys is used to express the many-to-many association between two parent tables.
ORM's many-to-many mapping will hide the intermediate table, that is, the intermediate table does not correspond to a Java/Kotlin entity type. Therefore, the intermediate table does not need an independent primary key, but directly uses the two foreign keys as a combined primary key.
In addition to the two associative foreign keys, the intermediate table must not have any other fields, which is a limitation of many-to-many associations in ORM.
The corresponding many-to-many association in ORM is as follows:
- 
Owning side: Book.authors- Java
- Kotlin
 Book.java@Entity
 public interface Book {
 @ManyToMany
 List<Author> authors();
 ...other code omitted...
 }Book.kt@Entity
 interface Book {
 @ManyToMany
 val authors: List<Author>
 ...other code omitted...
 }
- 
Inverse side (optional): Author.books- Java
- Kotlin
 Author.java@Entity
 public interface Author {
 @ManyToMany(mappedBy = "authors")
 List<Book> books();
 ...other code omitted...
 }Author.kt@Entity
 interface Author {
 @ManyToMany(mappedBy = "authors")
 val books: List<Book>
 ...other code omitted...
 }
Undisputed double many-to-one association
Let's look at the second scenario, first, look at a piece of DDL
create table order_(
    ...omit...
) engine=innodb;
create table product(
    ...omit...
) engine=innodb;
create table order_item(
    id bigint unsigned not null auto_increment primary key, 
    order_id bigint unsigned not null,
    product_id bigint unsigned not null,
    quantity int not null,
    unit_price numeric(10, 2) not null
) engine=innodb;
alter table order_item
    add constraint business_key_order_item
        unique(order_id, product_id)
;
alter table order_item
    add constraint fk_order_item__order
        foreign key(order_id)
            references order_(id)
;
alter table order_item
    add constraint fk_order_item__product
        foreign key(product_id)
            references product(id)
;
This is a classic order - order item - product association.
Among them, order-item looks a bit like the intermediate table of many-to-many, because it has two foreign keys: order_id pointing to the order_ table and product_id pointing to the product table.
However, order-item is not an intermediate table, because it has other business fields, quantity representing quantity of goods, and unit_price representing a snapshot of the price of goods at the time of order.
Fortunately, for the classic order - order item - product association, it is more intuitive to think of order_item as an independent entity, with two many-to-one associations pointing to order_ and product respectively;
Rather than viewing order-item as a join table and thinking there is a many-to-many relationship between order_ and product.
It is precisely because order_item is regarded as an independent entity that order_item uses an independent primary key.
We can map these three tables in ORM using two many-to-one associations
- 
OrderItem.orderandOrderItem.product- Java
- Kotlin
 OrderItem.java@Entity
 public interface OrderItem {
 @ManyToOne
 Order order();
 @ManyToOne
 Product product();
 int quantity();
 BigDecimal unitPrice();
 ...other code omitted...
 }OrderItem.kt@Entity
 interface OrderItem {
 @ManyToOne
 val order: Order
 @ManyToOne
 val product: Product
 val quantity: Int
 val unitPrice: BigDecimal
 ...other code omitted...
 }
- 
Order.itemsIn such systems, it is often necessary to get the detail list according to the order, so we define a one-to-many property Order.itemsas a mirror ofOrderItem.order.- Java
- Kotlin
 Order.java@Entity
 @Table(name = "ORDER_")
 public interface Order {
 @OneToMany(mappedBy = "order")
 List<OrderItem> items();
 ...other code omitted...
 }Order.kt@Entity
 @Table(name = "ORDER_")
 interface Order {
 @OneToMany(mappedBy = "order")
 val items: List<OrderItem>
 ...other code omitted...
 }
- 
Do not provide Product.itemsIn such systems, it is rarely necessary to get the detail list based on the product (if starting the analysis from the product side, it is usually a complex query rather than a simple association), so a one-way OrderItem.productassociation is sufficient.So there is no need to show the code for the Productentity.
The controversial, tangled scenario
In the previous text, we showed two business scenarios
- 
Scenario 1: The intermediate table book_author_mappingis very clean, with only two foreign key fields, and is naturally mapped to a many-to-many association
- 
Scenario 2: order_itemlooks like an intermediate table but is not an intermediate table, because in addition to the two foreign keys it also needs to have other business fields. Fortunately, people will choose to treat OrderItem as an independent entity and use two many-to-one associations to string the three entity types together.
Next, let's look at scenario 3, first look at the DDL:
create table student(
    ...omit...
)engine=innodb;;
create table course(
    ...omit...
) engine=innodb;
create table learning_link (
    id bigint unsigned not null auto_increment primary key,
    student_id bigint unsigned not null,
    course_id bigint unsigned not null,
    score int
) engine=innodb;
alter table learning_link
    add constraint pk_student_course_mapping
        primary key(student_id, course_id)
;
alter table learning_link
    add constraint fk_student_course_mapping__student
        foreign key(student_id)
            references student(id)
                on delete cascade
;
alter table learning_link
    add constraint fk_student_course_mapping__course
        foreign key(course_id)
            references course(id)
                on delete cascade
;
This piece of DDL represents the elective course system in schools. There is a many-to-many relationship between students and elective courses.
- 
For students, it is of course very important to know which courses they have chosen 
- 
For schools, it is also very important to know which students have chosen each course, because teacher resources and teaching venues need to be arranged based on this information 
That is, for the student entity and the course entity, associating with each other and querying each other is an important and high frequency operation. Therefore, abstracting bidirectional many-to-many associations between student and course is a very good choice.
Unfortunately, the elective relationship table learning_link has a score field that can be null, null indicating that the exam has not yet taken place, non-null indicating the score after the exam.
- 
Because of the existence of this field, the intermediate table is no longer clean, and cannot be simply mapped to the classic ORM many-to-many association. That is, this is not a simple scenario 1. 
- 
Of course, we can handle this problem using the method of scenario 2, treating learning_linkas an independent entity, and using two many-to-one associations to string the three entity types together.However, for a large part of the upper layer business, what they really care about is only the mutual association between studentandcourse, and they don't care about thescorefield of thelearning_linktable. (That is, the non-foreign key business fieldlearning_link.scoreof the intermediate table is far less important than the relevant fields in scenario 2). In this case, scenario 2's solution will bring a higher cognitive burden, and scenario 1's way of thinking is obviously simpler.
This scenario is actually the most tangled scenario in classic ORM, which cannot be simply mapped to many-to-many associations, while hoping that some upper layer business can adopt the many-to-many mentality to simplify the problem.
@ManyToManyView is a powerful tool designed for such scenarios.
Getting Started with ManyToManyView
For the discussed scenario 3 above, Jimmer gave a two-step solution.
- 
Basic association mapping: First, treat learning_linkas an independent entity according to scenario 2, and provide two many-to-one associations pointing tostudentandcourserespectively; Conversely,studentandcoursealso use one-to-many associations to referencelearning_link. That is, use two bidirectional one-to-many associations to string the three entity types together.
- 
Use @ManyToManyView:Then, on the basis of the first step, use @ManyToManyViewto quickly simulate the effect of scenario 1.
Basic relationship mapping
- 
LearningLink.studentandLearningLink.course- Java
- Kotlin
 LearningLink.java@Entity
 public interface LearningLink {
 @ManyToOne
 Student student(); ①
 @ManyToOne
 Course course(); ②
 Integer score();
 ...other code omitted...
 }LearningLink.kt@Entity
 interface LearningLink {
 @ManyToOne
 val student: Student ①
 @ManyToOne
 val course: Course ②
 val score: Int?
 ...other code omitted...
 }Where ① and ② will be referenced by subsequent code 
- 
Student.learningLinks- Java
- Kotlin
 Student.java@Entity
 public interface Student {
 @OneToMany(mappedBy = "student")
 List<LearningLink> learningLinks(); ③
 ...other code omitted...
 }Student.kt@Entity
 interface Student {
 @OneToMany(mappedBy = "student")
 val learningLinks: List<LearningLink> ③
 ...other code omitted...
 }Where ③ will be referenced by subsequent code 
- 
Course.learningLinks- Java
- Kotlin
 Course.java@Entity
 public interface Course {
 @OneToMany(mappedBy = "course")
 List<LearningLink> learningLinks(); ④
 ...other code omitted...
 }Course.kt@Entity
 interface Course {
 @OneToMany(mappedBy = "course")
 val learningLinks: List<LearningLink> ④
 ...other code omitted...
 }Where ④ will be referenced by subsequent code 
Using @ManyToManyView
- 
Simulate many-to-many association: Student.courses- Java
- Kotlin
 Student.java@Entity
 public interface Student {
 @ManyToManyView(
 prop = "learningLinks", ❶ -> ③
 deeperProp = "course" ❷ -> ②
 )
 List<Course> courses();
 // The one-to-many association `learningLinks`
 // has been declared in step 1
 @OneToMany(mappedBy = "student")
 List<LearningLink> learningLinks(); ③
 ...other code omitted...
 }Student.kt@Entity
 interface Student {
 @ManyToManyView(
 prop = "learningLinks", ❶ -> ③
 deeperProp = "course" ❷ -> ②
 )
 val courses: List<Course>
 // The one-to-many association `learningLinks`
 // has been declared in step 1
 @OneToMany(mappedBy = "student")
 val learningLinks: List<LearningLink> ③
 ...other code omitted...
 }The current property Student.coursescan be obtained in two steps:- 
❶ prop = "learningLinks"First, get all LearningLinkobjects through another property of the current entityStudent.learningLinks③
- 
❷ deeperProp = "course"For each LearningLinkobject obtained in the previous step,Coursecan be further obtained through theLearningLink.courseproperty ②tipSince the LearningLinkentity has only one many-to-one association pointing to theCourseentity, there is no ambiguity, sodeeperProp = "course"can be omitted here
 
- 
Simulate many-to-many association: Course.students- Java
- Kotlin
 Course.java@Entity
 public interface Course {
 @ManyToManyView(
 prop = "learningLinks", ❶ -> ④
 deeperProp = "student" ❷ -> ①
 )
 List<Student> students();
 // The one-to-many association `learningLinks` has been declared in step 1
 @OneToMany(mappedBy = "course")
 List<LearningLink> learningLinks(); ④
 ...other code omitted...
 }Course.kt@Entity
 interface Course {
 @ManyToManyView(
 prop = "learningLinks", ❶ -> ④
 deeperProp = "student" ❷ -> ①
 )
 val students: List<Student>
 // The one-to-many association `learningLinks` has been declared in step 1
 @OneToMany(mappedBy = "course")
 val learningLinks: List<LearningLink> ④
 ...other code omitted...
 }The current property Course.studentscan be obtained in two steps:- 
❶ prop = "learningLinks"First, get all LearningLinkobjects through another property of the current entityCourse.learningLinks④
- 
❷ deeperProp = "student"For each LearningLinkobject obtained in the previous step,Studentcan be further obtained through theLearningLink.studentproperty ①tipSince the LearningLinkentity has only one many-to-one association pointing to theStudententity, there is no ambiguity, sodeeperProp = "student"can be omitted here
 
The essence of @ManyToManyView
- 
The property declared by @ManyToManyViewdoes not maintain its own data, it is only a view of the original property, which proxies and wraps the collection returned by the original property.Take the Java example in this article Studentside as an example (theCourseside is the same).- 
Original association property: List<LearningLink> learningLinks(); 
- 
View association property: List<Course> courses(); 
 You can understand the ManyToManyView view property with this pseudocode: @Override
 public List<Course> courses() {
 return new ListProxy<>(
 this.learningLinks(),
 LearningLink::course
 );
 }The view association property returns a proxy collection that wraps the collection of the original property and transforms the elements of the original collection. It is obvious that the two are essentially the same and share the same data. 
- 
- 
When constructing entity objects, only the original property can be set, not the view property. This is different from @IdView. For @IdView, both the original property and the view property can be set. 
- 
Whether it is the original property or the view property, the preprocessor will generate code to match them, so both can be used in object fetchers and strongly typed SQL DSL.