【问题标题】:complex jpql query to build List<> object filtering with nested joins使用嵌套连接构建 List<> 对象过滤的复杂 jpql 查询
【发布时间】:2023-03-18 02:50:02
【问题描述】:

我需要根据分配给给定用户的角色构建Menu/Operation 数据结构,但我在我的应用程序中找不到正确的查询。让我们看看细节:

表格

我在MySQL 工作,但这不是问题,可以是任何DBMS。这些是我的查询中涉及的表

CREATE TABLE MENU (
    id_menu tinyint(3)  unsigned NOT NULL AUTO_INCREMENT,
    name    varchar(50)          NOT NULL,
    PRIMARY KEY (id_menu)
);

CREATE TABLE OPERATION (
    id_operation smallint(5) unsigned NOT NULL AUTO_INCREMENT,
    id_menu      tinyint(3)  unsigned NOT NULL,
    name         varchar(50)          NOT NULL,
    PRIMARY KEY (id_operation),
    CONSTRAINT fk_menu_operation FOREIGN KEY (id_menu) REFERENCES MENU (id_menu)
);

CREATE TABLE ROLE (
    role char(15) NOT NULL,
    PRIMARY KEY (role)
);

CREATE TABLE roles_operation (
    id_operation smallint(5) unsigned NOT NULL,
    role         char(15) NOT NULL,
    PRIMARY KEY (id_operation, role),
    CONSTRAINT fk_rolop_operation FOREIGN KEY (id_operation) REFERENCES OPERACION (id_operacion),
    CONSTRAINT fk_rolop_role FOREIGN KEY (role) REFERENCES ROL (role)
);

ROLE 表似乎没用,但我用它来模拟与其他表(如 USER)的关系,但这不是问题。

映射@Entity

@Entity
@Table(name = "MENU")
public class Menu implements Serializable {

    private static final long serialVersionUID = 2884031708620020329L;

    @Id
    @Column(name = "id_menu")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idMenu;

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

    @OneToMany(mappedBy = "menu")
    private List<Operation> operations;

    // gettters/setters, toString and other methods...
}

@Entity
@Table(name = "OPERATION")
public class Operation implements Serializable {

    private static final long serialVersionUID = -76328025481684179L;

    @Id
    @Column(name = "id_operation")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long idOperation;

    @ManyToOne(fetch = FetchType.EAGER)
    @JoinColumn(name = "id_menu")
    @NotNull
    private Menu menu;

    @Column(name = "name", unique = true)
    @NotNull
    private String name;

    @ManyToMany(fetch = FetchType.LAZY)
    @JoinTable(name = "roles_operation",
            joinColumns = {
                @JoinColumn(name = "id_operation", referencedColumnName = "id_operation")},
            inverseJoinColumns = {
                @JoinColumn(name = "role", referencedColumnName = "role")})
    private List<Role> allowedRoles;

    // gettters/setters, toString and other methods...
}

@Entity
@Table(name = "ROLE")
public class Role implements Serializable {

    private static final long serialVersionUID = -412065042708649504L;

    @Id
    @Column(name = "role")
    @Enumerated(EnumType.STRING)
    private RolEnum role; // Fixed values defined in the same class

    @ManyToMany(mappedBy = "roles")
    private List<User> users;  // The roles asigned to the user, doesn't matter here

    @ManyToMany(mappedBy = "allowedRoles")
    private List<Operation> operations;

    // gettters/setters, toString and other methods...
}

我的业务逻辑是“一个用户有一个或多个角色,一个角色被赋予一个或多个用户”(一个简单的@ManyToMany关系)。现在,给定一个用户,我需要查询,最好使用JPQL,以获得一个List&lt;Menu&gt;,并通过它的角色将操作分配给给定的User。使用SQL,我可以执行以下查询并获得我想要的结果:

select  m.name, o.name, r.role
from    MENU            m   inner join 
        OPERATION       o   on m.id_menu = o.id_menu inner join
        roles_operation ro  on o.id_operation = ro.id_operation inner join
        ROLE            r   on ro.role = r.role
where   r.rol in ('RoleA', 'RoleB') -- User's roles
order by 1, 2, 3;

问题是我不知道如何将其翻译成JPQL 语言。例如,如果我有以下结构:

示例菜单/操作/角色结构

- Menu1
    - Operation1.1
        - RoleA
    - Operation1.2
        -RoleA
        -RoleB 
    - Operation1.3
        -RoleC
- Menu2
    - Operation2.1
        - RoleA 
    - Operation2.2
        - RoleD
    - Operation2.3
        - RoleB

而用户有RoleARoleB,我必须得到以下结果(我使用的是类似JSON的格式,但这些都是Java对象)

{
     Menu{name=Menu1, 
         operations=[Operation{name=Operation1.1, role=[RoleA]}, 
                     Operation{name=Operation1.2, role=[RoleA, RoleB]}
         ]
    },
    Menu{name=Menu2, 
         operations=[Operation{name=Operation2.1, role=[RoleA]}, 
                     Operation{name=Operation2.3, role=[RoleB]}
         ]
    }
}

谁能帮我写这个查询?我已经尝试过使用JPQL

public List<Menu> buildUserMenu(User user) {
    // Extracts roles from user
    List<Role.RolEnum> roles = user.getRoles().stream()
            .map(rol -> rol.getRol())
            .collect(Collectors.toList());

    Query query = entityManager.createQuery(
            "SELECT m FROM "
            + "Menu m INNER JOIN "
            + "m.operations o INNER JOIN "
            + "o.allowedRoles r "
            + "WHERE r.role in :roles")
            .setParameter("roles", roles);

    return query.getResultList();
}

但我得到了完整的结构(如上面的示例结构)。 JPQL 中的查询应该是什么来构建我需要的 List&lt;Menu&gt; 对象?

注意:

如果有人有其他解决方案,例如使用 Criteria API,请告诉我该怎么做。老实说,我不太清楚如何使用该 API。

提前感谢您的回答。


失败的测试

测试类

public class MenuOperationRepositoryTest extends BaseTestRepository {

    // The class to test
    private MenuOperationRepository menuOperationRepository;

    @Before
    public void initTestCase() {
        // Build EntityManager and some other utility classes,
        // load some static data in the test DB (trust me, this works fine)
        initTestDB(); // this method is part of BaseTestRepository class

        // manual dependency injection (also working correctly)
        menuOperationRepository = new MenuOperationRepository();
        menuOperationRepository.entityManager = em;
        menuOperationRepository.logger = LoggerFactory.getLogger(MenuOperationRepository.class);

        // Add some data to DB specific for this class
        // The parameters are static methods that returns dummy data
        addMenuOperations(menu1(), operation11RoleA(), operation12RoleB(), operation13RoleC());
        addMenuOperations(menu2(), operation21RoleB(), operation22RoleA(), operation());
    }

    @After
    public void finishTestCase() {
        // Closes em and emf (EntityManagerFactory)
        closeEntityManager();
    }

    @Test
    public void buildMenuWithOperationsFilteredByRoles() {
        // userWithId() generates a dummy User object with Id as if saved in the DB
        // userRoleARoleB() generates a dummy object like menu1() and has RoleA and RoleB
        List<Menu> result = menuOperationRepository.buildUserMenu(userWithId(userRoleARoleB(), 1L));

        // the assertion methods are statically imported from org.junit.Assert and org.hamcrest.CoreMatchers

        // I should be getting menu1() and menu2()
        assertThat(result.size(), is(equalTo(2)));
        // menu1() should contain operation11RoleA() and operation12RoleB()
        assertThat(result.get(0).getOperations().size(), is(equalTo(2)));
        // menu2() should contain operation21RoleB() and operation22RoleA()
        assertThat(result.get(1).getOperations().size(), is(equalTo(2)));
    }

    /*
     *   HELPER METHODS
     */
    private void addMenuOperations(Menu menu, Operation... operations) {
        List<Operacion> operationList = Arrays.asList(operations);

        // This is a utility class which manages the connections manually
        // I use this in my tests only, in the server this is done automatically
        // This works correctly
        dbCommandExecutor.executeCommand(() -> {
            em.persist(menu);
            operationList.forEach((op) -> {
                op.setMenu(menu);
                em.persist(op);
            });
            return null;
        });
    }
}

要测试的类

public class RepositorioMenuOperacion {

    @PersistenceContext(unitName = "sigeaPU")
    EntityManager entityManager;

    @Inject
    Logger logger;

    public List<Menu> buildUserMenu(User user) {
        logger.debug("buildUserMenu({})", user);

        // Extracts roles from user object, this works fine according to the log trace
        List<Rol.RolEnum> roles = usuario.getRoles().stream()
                .map(rol -> rol.getRol())
                .collect(Collectors.toList());
        logger.debug("Extracted roles: {}", roles);

        // The query I'm using and that I posted previously
        Query query = entityManager.createQuery(
                "SELECT m FROM "
                + "Menu m INNER JOIN "
                + "m.operations o INNER JOIN "
                + "o.allowedRoles r "
                + "WHERE r.role in :roles")
                .setParameter("roles", roles);

        List<Menu> menuList = query.getResultList();
        logger.info("Menu List found");
        menuList.forEach(menu -> logger.info(menu.toString()));

        return menuList;
    }
}

我认为这给出了问题的全貌。我需要重写查询或在查询后过滤menuList,但我无法解决。我在查询后尝试过滤,但我得到一个ConcurrentModificationException,无法确切说明原因。

如果您需要有关我的应用程序的更多数据,请告诉我。

【问题讨论】:

  • 如果您对原生查询感到满意,请使用EntityManager.createNativeQuery
  • 这就是重点,我对原生查询感到不舒服,如果我将来想迁移到另一个数据库(例如 postgres)怎么办?我不想将业务逻辑(用 Java 编写)与数据层逻辑(现在是 MySQL)结合起来。此外,到目前为止,我一直在使用JPQL 检索数据并使用 Java 管理逻辑,但我一直坚持这一点。这是我期望在我的应用程序中拥有的最复杂的查询......此外,这是一个了解更多关于JPA 的好机会,你不觉得吗?
  • 如果您的数据库设置正确(FK 等)并且您使用的是 Eclipse,那么为您的数据库生成 DAO 和实体是一件简单的事情。默认情况下,所有连接都可以通过延迟加载获得
  • @ScaryWombat 我的实体类是正确的(经过测试),我不需要生成DAO,我的问题是过滤(WHERE 子句,我想)我的结果列表从查询执行中获取。我将添加我的失败测试,​​以便您全面了解问题
  • @ScaryWombat,检查添加的部分

标签: java jpql criteria-api


【解决方案1】:

您的查询很好,但它没有返回您想要的。它返回包含至少一个允许操作的所有菜单。并且由于它返回菜单,并且菜单具有操作,返回的菜单包含它们的所有操作。因为这就是关联:它不包含特定于给定查询的条目。它包含菜单的操作。

您需要的不是返回菜单,而是返回允许的操作。然后,您可以使用 ManyToOne 关联获取他们的菜单:

select o from Operation o
join o.allowedRoles role
where role role in :roles

这只会返回用户允许的操作。

【讨论】:

  • 好的,就是这样,谢谢。还有一件事:一旦我得到List&lt;Operation&gt; 集合,我应该如何将它转换为List&lt;Menu&gt;?也许您已经想到了一个简单而优雅的方式来做到这一点。
  • operations.stream().map(Operation::getMenu).distinct().collect(Collectors.toList())
  • 抱歉,我们遗漏了一些东西。显然,在调用 operations.stream().. 时会创建另一个查询,并返回包含所有操作的菜单,就像我的原始查询一样。
  • 从操作中获取菜单并不是加载菜单操作的内容,除非关联是急切的,或者您从 Menu.equals() 方法访问操作。菜单的操作很可能已加载,因为您的测试会打印菜单,而 Menu.toString() 方法会打印操作。当然,这需要加载菜单的操作。但是您似乎再次对菜单中的所有操作感到惊讶。那总是会发生的。通过从菜单中获取操作,您将永远不会拥有菜单操作的子集。
  • 允许用户访问的操作是查询返回的操作。不是查询返回的操作菜单中的那些。就像如果我问“你家的男孩是什么?”,你会说“约翰和杰克”。然后我会问约翰和杰克“你爸爸是谁?他们会回答“迈克”。然后我会问迈克“你的孩子是谁?”,迈克会说“约翰、杰克、玛丽和劳拉”。看,Mike 的孩子就是 Mike 的孩子,所有的孩子,不管你是否从查询男孩开始。
猜你喜欢
  • 2019-07-22
  • 2022-06-15
  • 2011-12-24
  • 1970-01-01
  • 2015-03-02
  • 1970-01-01
  • 1970-01-01
  • 2014-07-13
  • 1970-01-01
相关资源
最近更新 更多