Strange datagrid behavior with composite keys in JMIX v2.1.0 and 2.1.1

I am using a composite key in a composition M:N relationship, but the datagrid behavior is very strange.
Following the steps indicated in this topic I cannot get it to work correctly.

This is an example:
image

Book

package com.company.compositeprimarykey.entity;

import io.jmix.core.entity.annotation.JmixGeneratedValue;
import io.jmix.core.metamodel.annotation.Composition;
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;

import java.util.Set;

@JmixEntity
@Table(name = "CPK_BOOK")
@Entity(name = "cpk_Book")
public class Book {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private Long id;

    @InstanceName
    @Column(name = "TITLE", nullable = false)
    private String title;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    @Composition
    @OneToMany(mappedBy = "book")
    private Set<BookAuthor> comments;

    public Set<BookAuthor> getComments() {
        return comments;
    }

    public void setComments(Set<BookAuthor> comments) {
        this.comments = comments;
    }
}

Author

package com.company.compositeprimarykey.entity;

import io.jmix.core.entity.annotation.JmixGeneratedValue;
import io.jmix.core.metamodel.annotation.InstanceName;
import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;

import java.util.Set;

@JmixEntity
@Table(name = "CPK_AUTHOR")
@Entity(name = "cpk_Author")
public class Author {
    @JmixGeneratedValue
    @Column(name = "ID", nullable = false)
    @Id
    private Long id;

    @InstanceName
    @Column(name = "NAME", nullable = false)
    private String name;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @OneToMany(mappedBy = "author")
    private Set<BookAuthor> comments;

    public Set<BookAuthor> getComments() {
        return comments;
    }

    public void setComments(Set<BookAuthor> comments) {
        this.comments = comments;
    }
}

BookAuthorKey

package com.company.compositeprimarykey.entity;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Embeddable;
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotNull;

import java.util.Objects;

@JmixEntity(name = "cpk_BookAuthorKey")
@Embeddable
public class BookAuthorKey {
    @Column(name = "BOOK_ID", nullable = false)
    @NotNull
    private Long bookId;

    @Column(name = "AUTHOR_ID", nullable = false)
    @NotNull
    private Long authorId;

    public Long getBookId() {
        return bookId;
    }

    public void setBookId(Long bookId) {
        this.bookId = bookId;
    }

    public Long getAuthorId() {
        return authorId;
    }

    public void setAuthorId(Long authorId) {
        this.authorId = authorId;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        BookAuthorKey that = (BookAuthorKey) o;

        if (!Objects.equals(bookId, that.bookId)) return false;
        return Objects.equals(authorId, that.authorId);
    }

    @Override
    public int hashCode() {
        int result = bookId != null ? bookId.hashCode() : 0;
        result = 31 * result + (authorId != null ? authorId.hashCode() : 0);
        return result;
    }
}

BookAuthor

package com.company.compositeprimarykey.entity;

import io.jmix.core.metamodel.annotation.JmixEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;

@JmixEntity
@Table(name = "CPK_BOOK_AUTHOR")
@Entity(name = "cpk_BookAuthor")
public class BookAuthor {
    @EmbeddedId
    @AttributeOverrides({
            @AttributeOverride(name = "bookId", column = @Column(name = "BOOK_ID")),
            @AttributeOverride(name = "authorId", column = @Column(name = "AUTHOR_ID"))
    })
    private BookAuthorKey id;

    public BookAuthorKey getId() {
        return id;
    }

    public void setId(BookAuthorKey id) {
        this.id = id;
    }

    @MapsId("bookId")
    @JoinColumn(name = "BOOK_ID", nullable = false)
    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Book book;

    @MapsId("authorId")
    @JoinColumn(name = "AUTHOR_ID", nullable = false)
    @NotNull
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Author author;

    @Column(name = "TEXT", nullable = true)
    private String text;

    public Book getBook() {
        return book;
    }

    public void setBook(Book book) {
        this.book = book;
    }

    public Author getAuthor() {
        return author;
    }

    public void setAuthor(Author author) {
        this.author = author;
    }

    public String getText() {
        return text;
    }

    public void setText(String text) {
        this.text = text;
    }
}

Database Schema

CREATE TABLE CPK_BOOK(
	ID BIGINT NOT NULL,
	TITLE VARCHAR(255) NOT NULL,
	CONSTRAINT CPK_BOOK PRIMARY KEY (ID)
);

CREATE TABLE CPK_AUTHOR(
	ID BIGINT NOT NULL,
	NAME VARCHAR(255) NOT NULL,
	CONSTRAINT CPK_AUTHOR PRIMARY KEY (ID)
);

CREATE TABLE CPK_BOOK_AUTHOR(
	BOOK_ID BIGINT NOT NULL,
	AUTHOR_ID BIGINT NOT NULL,
	TEXT VARCHAR(255),
	CONSTRAINT PK_I PRIMARY KEY (BOOK_ID,AUTHOR_ID),
	CONSTRAINT FK_BOOKAUTHOR_BOOK FOREIGN KEY (BOOK_ID) REFERENCES CPK_BOOK(ID) ON UPDATE CASCADE ON DELETE NO ACTION,	
	CONSTRAINT FK_BOOKAUTHOR_AUTHOR FOREIGN KEY (AUTHOR_ID) REFERENCES CPK_AUTHOR(ID) ON UPDATE CASCADE ON DELETE NO ACTION
);

The views have been generated automatically with “IntelliJ IDEA Community”.

I have 3 authors created, A1, A2 and A3. I create a book, B1, and add an author’s text, B1A1. At the moment it works well.

image

Now I am trying to add another text to book B1. Text B1A2 by author A2. I press the create button and instead of creating it modifies the previous one.

image

image

image

When I save the book and edit it again, the two authors’ texts appear.

image

Now the create button works. I try to add another text, B1A3, but when I save it does not appear in the grid.

image

image

If I save the book and edit it again the new text B1A3 appears.

image

I’ve tried several ways, even without @Composition, but the result is the same.

Furthermore, if you continue creating, editing or deleting texts, the behavior becomes more strange. For example, several lines appear with the same text.

Does JMIX V2.1.x support composite keys? I am doing something wrong?

Thanks.

The problem with using this model in standard views for editing compositions is that BookAuthorKey fields are not updated when you select Book and Author for the BookAuthor entity. So the key for the new entity is invalid and causes problems for the automatic working of DataContext.

You can fix it by setting the key values in the entity setters:

@JmixEntity
@Table(name = "BOOK_AUTHOR")
@Entity
public class BookAuthor {
    @EmbeddedId
    @AttributeOverrides({
            @AttributeOverride(name = "bookId", column = @Column(name = "BOOK_ID")),
            @AttributeOverride(name = "authorId", column = @Column(name = "AUTHOR_ID"))
    })
    private BookAuthorKey id;

    // ...

    public void setBook(Book book) {
        this.book = book;
        this.id.setBookId(book == null ? null : book.getId());
    }

    public void setAuthor(Author author) {
        this.author = author;
        this.id.setAuthorId(author == null ? null : author.getId());
    }