N+1 и долгая сериализация объекта

Добрый день.
Очередной вопрос при миграции с 1.1.3 на 2.7.4.

После миграции, возникла проблема с производительностью. Запрос через rest аддон, который выполнялся десятки миллисекунд, стал выполняться более секунды.

image

Сущности:

@JmixEntity
@Table(name = "MED_WORKPLACE", indexes = {
        @Index(name = "IDX_MED_WORKPLACE_CODE", columnList = "CODE"),
        @Index(name = "IDX_MED_WORKPLACE_POSITION_CODE", columnList = "POSITION_CODE"),
        @Index(name = "IDX_MED_WORKPLACE_MAIN_DEPARTMENT_CODE", columnList = "MAIN_DEPARTMENT_CODE"),
        @Index(name = "IDX_MED_WORKPLACE_ORGANIZATION_CODE", columnList = "ORGANIZATION_CODE"),
        @Index(name = "IDX_MED_WORKPLACE_CONDITION_CLASS", columnList = "CONDITION_CLASS_ID"),
        @Index(name = "IDX_MED_WORKPLACE_WORK_CONDITION_TYPE", columnList = "WORK_CONDITION_TYPE_ID"),
        @Index(name = "IDX_MED_WORKPLACE_WORK_CONDITION_SUBTYPE", columnList = "WORK_CONDITION_SUBTYPE_ID"),
        @Index(name = "IDX_MED_WORKPLACE_POSITION", columnList = "POSITION_ID")
})
@Entity(name = "med_Workplace")
@Getter
@Setter
public class Workplace extends BaseEntity implements HasPeriod, HasOrgStructureProperties {
    @InstanceName
    @Column(name = "CODE", nullable = false)
    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity/Workplace.code.validation.NotNull}")
    private String code;

    @Column(name = "HR_CODES", length = 1024)
    private String hrCodes;

    @Column(name = "NAME")
    private String name;

    @JoinColumn(name = "CONDITION_CLASS_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private ConditionClass conditionClass;

    @Column(name = "WORK_CONDITION_CARD_NUMBER")
    private String workConditionCardNumber;

    @Column(name = "WORK_CONDITION_DATE")
    private LocalDate workConditionDate;

    @JoinColumn(name = "WORK_CONDITION_TYPE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private WorkConditionType workConditionType;

    @JoinColumn(name = "WORK_CONDITION_SUBTYPE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private WorkConditionSubtype workConditionSubtype;

    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.workplace/Workplace.positionCode.validation.NotNull}")
    @Column(name = "POSITION_CODE", nullable = false)
    private String positionCode;

    @Column(name = "POSITION_NAME")
    private String positionName;

    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.workplace/Workplace.departmentCode.validation.NotNull}")
    @Column(name = "DEPARTMENT_CODE", nullable = false)
    private String departmentCode;

    @Column(name = "DEPARTMENT_NAME")
    private String departmentName;

    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.workplace/Workplace.mainDepartmentCode.validation.NotNull}")
    @Column(name = "MAIN_DEPARTMENT_CODE", nullable = false)
    private String mainDepartmentCode;

    @Column(name = "MAIN_DEPARTMENT_NAME")
    private String mainDepartmentName;

    @Column(name = "ORGANIZATION_CODE")
    private String organizationCode;

    @Column(name = "ORGANIZATION_NAME")
    private String organizationName;

    @JoinColumn(name = "POSITION_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private Position position;

    @EmbeddedParameters(nullAllowed = false)
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "since", column = @Column(name = "PERIOD_SINCE")),
            @AttributeOverride(name = "until", column = @Column(name = "PERIOD_UNTIL"))
    })
    private Period period;

    @OrderBy("period.since")
    @OneToMany(mappedBy = "workplace")
    private List<WorkplaceFactor> factors;

    @OneToMany(mappedBy = "workplace")
    private List<EmployeeWorkplace> employees;

    @Override
    public Period getPeriod() {
        return period;
    }

    @Override
    public void setPeriod(Period period) {
        this.period = period;
    }

    public List<WorkplaceFactor> getActualFactors(LocalDate date) {
        return factors.stream()
                .filter(workplaceFactor -> workplaceFactor.getPeriod().isIncluded(date))
                .toList();
    }

    public List<EmployeeWorkplace> getActualEmployees(LocalDate date) {
        return employees.stream()
                .filter(employeeWorkplace -> employeeWorkplace.getPeriod().isIncluded(date))
                .toList();
    }
}
@JmixEntity
@Table(name = "MED_WORKPLACE_FACTOR", indexes = {
        @Index(name = "IDX_MED_WORKPLACE_FACTOR_WORKPLACE", columnList = "WORKPLACE_ID"),
        @Index(name = "IDX_MED_WORKPLACE_FACTOR_FACTOR", columnList = "FACTOR_ID")
})
@Entity(name = "med_WorkplaceFactor")
@Getter
@Setter
public class WorkplaceFactor extends BaseEntity implements HasHarmfulFactor, HasPeriod {
    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.factor/WorkplaceFactor.workplace.validation.NotNull}")
    @JoinColumn(name = "WORKPLACE_ID", nullable = false)
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private Workplace workplace;

    @JoinColumn(name = "FACTOR_ID", nullable = false)
    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.workplace/WorkplaceFactor.factor.validation.NotNull}")
    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    private HarmfulFactor factor;

    @EmbeddedParameters(nullAllowed = false)
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "since", column = @Column(name = "PERIOD_SINCE", nullable = false)),
            @AttributeOverride(name = "until", column = @Column(name = "PERIOD_UNTIL"))
    })
    private Period period;

    @Override
    public Period getPeriod() {
        return period;
    }

    @Override
    public void setPeriod(Period period) {
        this.period = period;
    }

    @Override
    public HarmfulFactor getFactor() {
        return factor;
    }

    @InstanceName
    @DependsOnProperties({"id"})
    public String getInstanceName(MetadataTools metadataTools) {
        return metadataTools.format(getId());
    }
}
@JmixEntity
@Table(name = "MED_HARMFUL_FACTOR", indexes = {
        @Index(name = "IDX_MED_HARMFUL_FACTOR_GROUP", columnList = "GROUP_ID"),
        @Index(name = "IDX_MED_HARMFUL_FACTOR_TYPE", columnList = "TYPE_ID"),
        @Index(name = "IDX_MED_HARMFUL_FACTOR_RECORD_TYPE", columnList = "RECORD_TYPE_ID")
})
@Entity(name = "med_HarmfulFactor")
@Getter
@Setter
public class HarmfulFactor extends BaseEntity implements HasPeriod {
    @JoinColumn(name = "GROUP_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private FactorGroup group;

    @JoinColumn(name = "TYPE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private FactorType type;

    @JoinColumn(name = "RECORD_TYPE_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private FactorRecordType recordType;

    @Column(name = "CODE")
    private String code;

    @InstanceName
    @Column(name = "NAME", nullable = false, length = 1024)
    @NotNull(message = "{msg://ru.digitalatom.medicalexamination.entity.factor/HarmfulFactor.name.validation.NotNull}")
    private String name;

    @Column(name = "SHORT_NAME", length = 1024)
    private String shortName;

    @OnDeleteInverse(DeletePolicy.DENY)
    @JoinColumn(name = "PERIODICITY_ID")
    @ManyToOne(fetch = FetchType.LAZY)
    private Periodicity periodicity;

    @EmbeddedParameters(nullAllowed = false)
    @Embedded
    @AttributeOverrides({
            @AttributeOverride(name = "since", column = @Column(name = "PERIOD_SINCE")),
            @AttributeOverride(name = "until", column = @Column(name = "PERIOD_UNTIL"))
    })
    private Period period;

    @OnDeleteInverse(DeletePolicy.UNLINK)
    @JoinTable(name = "MED_HARMFUL_FACTOR_EXAMINATION_LINK",
            joinColumns = @JoinColumn(name = "HARMFUL_FACTOR_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "EXAMINATION_ID", referencedColumnName = "ID"))
    @OrderBy("name")
    @ManyToMany
    private List<Examination> examinations;

    @OnDeleteInverse(DeletePolicy.UNLINK)
    @JoinTable(name = "MED_HARMFUL_FACTOR_ADDITIONAL_EXAMINATION_LINK",
            joinColumns = @JoinColumn(name = "HARMFUL_FACTOR_ID", referencedColumnName = "ID"),
            inverseJoinColumns = @JoinColumn(name = "ADDITIONAL_EXAMINATION_ID", referencedColumnName = "ID"))
    @ManyToMany
    private List<AdditionalExamination> additional;

    @Override
    public Period getPeriod() {
        return period;
    }

    @Override
    public void setPeriod(Period period) {
        this.period = period;
    }
}

Фетч-план:

    <fetchPlan name="workplace-full-rest"
               class="ru.digitalatom.medicalexamination.entity.workplace.Workplace"
               extends="_base">
        <property name="factors" fetchPlan="_base">
            <property name="factor" fetchPlan="_base"/>
        </property>
    </fetchPlan>

По логу видно, что имеется проблема N+1 (которой кстати не было в 1.1.3, запрос выполнялся через IN), но даже при этом запросы выполняются быстро, потом видимо идёт сериализация, которая и занимает львиную долю времени.

2026-02-19T16:14:35.384+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 2009942312> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, DEPARTMENT_CODE, DEPARTMENT_NAME, HR_CODES, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, MAIN_DEPARTMENT_CODE, MAIN_DEPARTMENT_NAME, NAME, ORGANIZATION_CODE, ORGANIZATION_NAME, POSITION_CODE, POSITION_NAME, VERSION, WORK_CONDITION_CARD_NUMBER, WORK_CONDITION_DATE, PERIOD_SINCE, PERIOD_UNTIL, CONDITION_CLASS_ID, POSITION_ID, WORK_CONDITION_SUBTYPE_ID, WORK_CONDITION_TYPE_ID FROM MED_WORKPLACE WHERE ((ID = ?) AND (DELETED_DATE IS NULL))
	bind => [7a727958-93b3-e248-883e-a145a6e07d2b]
2026-02-19T16:14:35.385+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 2009942312> [1 ms] spent
2026-02-19T16:14:35.386+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 419678543> SELECT ID, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, VERSION, PERIOD_SINCE, PERIOD_UNTIL, FACTOR_ID, WORKPLACE_ID FROM MED_WORKPLACE_FACTOR WHERE ((WORKPLACE_ID = ?) AND (DELETED_DATE IS NULL)) ORDER BY PERIOD_SINCE ASC
	bind => [7a727958-93b3-e248-883e-a145a6e07d2b]
2026-02-19T16:14:35.387+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 419678543> [1 ms] spent
2026-02-19T16:14:35.389+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 265832580> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [5935b891-9d04-421a-9e7d-bcafb2f293e2]
2026-02-19T16:14:35.390+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 265832580> [1 ms] spent
2026-02-19T16:14:35.391+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 576886058> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [c4b74469-38e1-4172-b24b-a1cf5922e0b1]
2026-02-19T16:14:35.391+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 576886058> [0 ms] spent
2026-02-19T16:14:35.391+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 879602484> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [6457ad74-3d9b-10e3-c562-1a1fa5c44077]
2026-02-19T16:14:35.393+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 879602484> [2 ms] spent
2026-02-19T16:14:35.393+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 186806203> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [ba3d886f-a08b-4552-96f1-0d923c7698ba]
2026-02-19T16:14:35.394+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 186806203> [1 ms] spent
2026-02-19T16:14:35.396+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 1152572432> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [462742f7-3777-6ff2-535b-bccae0e50d82]
2026-02-19T16:14:35.397+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 1152572432> [1 ms] spent
2026-02-19T16:14:35.398+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 1540453175> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [4d17664e-e48a-40c1-9f96-eaa212acaa8b]
2026-02-19T16:14:35.398+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 1540453175> [0 ms] spent
2026-02-19T16:14:35.399+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 790788988> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [2ad1c5d4-abc7-41b8-827a-d1b8e1f1513a]
2026-02-19T16:14:35.400+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 790788988> [1 ms] spent
2026-02-19T16:14:35.400+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 475581716> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [c9553cf0-5666-45df-8720-c503a30ee167]
2026-02-19T16:14:35.401+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 475581716> [1 ms] spent
2026-02-19T16:14:35.403+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 472643597> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [4dd89a0f-ed7b-001b-7f35-a052f274058a]
2026-02-19T16:14:35.404+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 472643597> [1 ms] spent
2026-02-19T16:14:35.404+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 888594786> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [bcce02e5-ccd9-4a45-bda2-32a309f8111b]
2026-02-19T16:14:35.405+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 888594786> [1 ms] spent
2026-02-19T16:14:35.405+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 47181998> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [b95e0fd1-110b-92b1-b25c-90395d0bfe0a]
2026-02-19T16:14:35.405+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 47181998> [0 ms] spent
2026-02-19T16:14:35.406+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 37111297> SELECT ID, CODE, CREATED_BY, CREATED_DATE, DELETED_BY, DELETED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE, NAME, SHORT_NAME, VERSION, PERIOD_SINCE, PERIOD_UNTIL, GROUP_ID, PERIODICITY_ID, RECORD_TYPE_ID, TYPE_ID FROM MED_HARMFUL_FACTOR WHERE ((ID = ?) AND (0=0))
	bind => [e1d90825-70d2-4f8f-98c8-018793690474]
2026-02-19T16:14:35.406+03:00 DEBUG 5568 --- [nio-8081-exec-8] eclipselink.logging.sql                  : <t 2110891703, conn 37111297> [0 ms] spent