Row Level роль и пагинация

Добрый день! Столкнулись с ошибкой при нажатии кнопки подсчета числа записей
image
image
Она возникает, если к пользователю привязана Row-level роль с политикой на сущность (эту или связанную) с Predicate.
Подскажите, пожалуйста, что можно с этим сделать? Самостоятельно считать количество записей не хотелось бы, потому что это лишает смысла Row-level роли, т.к. все запросы с этими ограничениями придется писать для каждого экрана заново.

java.lang.IllegalStateException: Unsupported entity type class java.util.Vector
	at io.jmix.core.entity.EntityPreconditions.checkEntityType(EntityPreconditions.java:25)
	at io.jmix.data.impl.EntityAttributesEraserImpl.collectErasingReferences(EntityAttributesEraserImpl.java:54)
	at io.jmix.data.impl.DataStoreInMemoryCrudListener.entityLoading(DataStoreInMemoryCrudListener.java:104)
	at io.jmix.core.datastore.DataStoreEntityLoadingEvent.sendTo(DataStoreEntityLoadingEvent.java:83)
	at io.jmix.core.datastore.AbstractDataStore.fireEvent(AbstractDataStore.java:343)
	at io.jmix.core.datastore.AbstractDataStore.getCount(AbstractDataStore.java:191)
	at io.jmix.core.impl.UnconstrainedDataManagerImpl.getCount(UnconstrainedDataManagerImpl.java:125)
	at io.jmix.ui.component.pagination.data.AbstractPaginationDataBinder.getCount(AbstractPaginationDataBinder.java:97)
	at io.jmix.ui.component.impl.AbstractPagination.getTotalCount(AbstractPagination.java:306)
	at io.jmix.ui.component.impl.SimplePaginationImpl.onLinkClick(SimplePaginationImpl.java:278)
	at jdk.internal.reflect.GeneratedMethodAccessor301.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at com.vaadin.event.ListenerMethod.receiveEvent(ListenerMethod.java:709)
	at com.vaadin.event.EventRouter.fireEvent(EventRouter.java:399)
	at com.vaadin.event.EventRouter.fireEvent(EventRouter.java:363)
	at com.vaadin.server.AbstractClientConnector.fireEvent(AbstractClientConnector.java:1216)
	at com.vaadin.ui.Button.fireClick(Button.java:384)
	at com.vaadin.ui.Button$1.click(Button.java:57)
	at jdk.internal.reflect.GeneratedMethodAccessor284.invoke(Unknown Source)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:568)
	at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:153)
	at com.vaadin.server.ServerRpcManager.applyInvocation(ServerRpcManager.java:115)
	at com.vaadin.server.communication.ServerRpcHandler.handleInvocation(ServerRpcHandler.java:442)
	at com.vaadin.server.communication.ServerRpcHandler.handleInvocations(ServerRpcHandler.java:407)
	at com.vaadin.server.communication.ServerRpcHandler.handleRpc(ServerRpcHandler.java:275)
	at com.vaadin.server.communication.UidlRequestHandler.synchronizedHandleRequest(UidlRequestHandler.java:83)
	at com.vaadin.server.SynchronizedRequestHandler.handleRequest(SynchronizedRequestHandler.java:40)
	at com.vaadin.server.VaadinService.handleRequest(VaadinService.java:1636)
	at com.vaadin.server.VaadinServlet.service(VaadinServlet.java:465)
	at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:209)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at io.jmix.core.impl.logging.LogMdcFilter.doFilterInternal(LogMdcFilter.java:28)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:337)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:115)
	at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:81)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:122)
	at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:116)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:126)
	at org.springframework.security.web.session.SessionManagementFilter.doFilter(SessionManagementFilter.java:81)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:109)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:106)
	at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:97)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:149)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:147)
	at org.springframework.security.web.session.ConcurrentSessionFilter.doFilter(ConcurrentSessionFilter.java:125)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:103)
	at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:89)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90)
	at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:112)
	at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:82)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:55)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:346)
	at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:221)
	at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:186)
	at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:354)
	at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:267)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:117)
	at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:178)
	at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:153)
	at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)
	at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)
	at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:481)
	at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:130)
	at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)
	at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
	at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
	at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)
	at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
	at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:926)
	at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1791)
	at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)
	at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
	at java.base/java.lang.Thread.run(Thread.java:833)
  • еще вопрос про Row-level роли: нет ли какой-то возможности использовать там distinct? Join’ы, которые нужны для фильтрации данных, приводят к задвоению записей в таблицах, а предикаты на read во-первых очень сильно снижают производительность, а во-вторых приводят к той же боли с пагинацией

Очень ждем ответа, заранее спасибо

Виктория, здравствуйте!

По стэктрейсу похоже на багу, но на простом примере мне не удалось ее воспроизвести.
Не могли бы вы приложить какой-нибудь тестовый проект, где это воспроизводится?

Из того что можно предложить сейчас как воркэраунд - это переопределить бин io.jmix.data.impl.DataStoreInMemoryCrudListener, в нем переопределить метод entityLoading, скопировав реализацию из платформенного бина, но изменив следующую часть:

        //...
        for (Object entity : event.getResultEntities()) {
            if (!crudContext.isReadPermitted(entity)) {
                log.debug("Reading entity {} is not permitted by access constraints", entity);
                event.excludeEntity(entity);
            } else {
                if (entity instanceof Entity) { // changed: to avoid passing Vector to entities list
                    entities.add(entity);
                } // changed
            }
        }
        //...

Чтобы дать более точное решение и завести issue нужно больше информации для воспроизведения проблемы.

По поводу distinct - в Row-level ролях такой возможности нет, но может использование left или inner join-ов позволит построить запрос без задвоения записей?
Если же нет, то можно попробовать указывать distinct в экранах и местах загрузки этой сущности.

С уважением,
Дмитрий

На тестовом воспроизвелось не совсем точно так же, но тоже с глюком:
image
(видимо, тут не ошибка, а хоть какое-то число, т.к. условие более простое)
0 всего, плюс при переходе по >> не дает вернуться обратно, а если пользоваться переходом не на последнюю, а на следующую страницу, тогда всё ок.
RowLevelRoles2.zip (856 Байт)
RowLevelError2.zip (170.1 КБ)
Прикладываю проект и роль (ограничение на сущность Notes)
Воспроизводится, если Predicate использовать для Read (видимо, это вообще нехорошо и лучше так не делать, но раз уж такая возможность есть…)

И тут же, уже без привязки к ролям, замечено странное поведение при сортировке:
Вот так вообще без сортировки:
image

А вот с сортировкой:
1)
image
и 2)
image

А те ребята, которые были заполнены, куда-то делись… Наверное, остались на других страницах (null всегда лезет вперед?)

Это действительно бага, спасибо, что обнаружили ее! Создан issue.
Предыдущий воркэраунд работать не будет, вместо этого нужно переопределять JpaDataStore, чтобы поправить проблему в проекте (см. FixedJpaDataStore и RowLevelErrorApplication):
RowLevelError_fixed.zip (395.8 КБ)

Воспроизводится, если Predicate использовать для Read (видимо, это вообще нехорошо и лучше так не делать, но раз уж такая возможность есть…)

По поводу Predicate для Read - это возможно, но для большого количества сущностей этого лучше не делать. При наличии такого предиката у пользователя все затронутые им запросы будут грузить из бд все сущности, а потом отфильтровывать неподходящие, что, как вы уже заметили, сильно снижает производительность (особенно, для count-запросов, превращающихся в loadAll-запросы).

Поэтому лучше постараться переписать предикат через JPQL policy, если, конечно, речь не о какой-нибудь справочной сущности в несколько десятков экземпляров.

при переходе по >> не дает вернуться обратно, а если пользоваться переходом не на последнюю, а на следующую страницу, тогда всё ок

Проблем с переходом по страницам не обнаружилось после исправления работы предиката - видимо, были вызваны некорректным числом общего количества записей.

И тут же, уже без привязки к ролям, замечено странное поведение при сортировке

Да, это “nulls first”. Такое поведение зависит от БД и на данный момент фреймворком не переопределяется. C HSQL тоже с этим столкнулся, но уже Postgres работает лучше: ставит пустые значения или в начале, или в конце в зависимости от порядка сортировки.