Search code examples
mysqlspring-bootjooq

Null objects while using Coalesce and duplicate values while joining


[
  {
    "permissions": [
      {
        "name": "CREATE",
        "id": 1
      },
      {
        "name": "DELETE",
        "id": 4
      }
    ],
    "roles": [
      {
        "name": "ADMIN",
        "permission": [
          {
            "name": "CREATE",
            "id": 1
          },
          {
            "name": "UPDATE",
            "id": 2
          },
          {
            "name": "GET",
            "id": 3
          },
          {
            "name": "DELETE",
            "id": 4
          }
        ],
        "id": 1
      },
      {
        "name": "ADMIN",
        "permission": [
          {
            "name": "CREATE",
            "id": 1
          },
          {
            "name": "UPDATE",
            "id": 2
          },
          {
            "name": "GET",
            "id": 3
          },
          {
            "name": "DELETE",
            "id": 4
          }
        ],
        "id": 1
      }
    ],
    "id": 1,
    "username": "raj@100"
  },
  {
    "permissions": [
      {
        "name": null,
        "id": null
      }
    ],
    "roles": [
      {
        "name": "USER",
        "permission": [
          {
            "name": "GET",
            "id": 3
          }
        ],
        "id": 3
      }
    ],
    "id": 2,
    "username": "ram145"
  }
]

As you can see from the above output the in roles the ADMIN is repeated twice and in the second users has no permissions so he should have an empty array but the output is with the permission object with all its values empty

This is the jooq statement which is executed :

public Object findAllUsers(String role, String permission) {
        SelectOnConditionStep<Record1<JSON>> query = dslContext.select(
                jsonObject(
                    key("id").value(USER.ID),
                    key("fullName").value(USER.FULL_NAME),
                    key("username").value(USER.USERNAME),
                    key("email").value(USER.EMAIL),
                    key("mobile").value(USER.MOBILE),
                    key("isActive").value(USER.IS_ACTIVE),
                    key("lastLoggedIn").value(USER.LAST_LOGGED_IN),
                    key("profileImage").value(USER.PROFILE_IMAGE),
                    key("roles").value(
                        coalesce(
                            jsonArrayAgg(
                                jsonObject(
                                    key("id").value(ROLE.ID),
                                    key("name").value(ROLE.NAME),
                                    key("permission").value(
                                        coalesce(
                                            select(
                                                jsonArrayAgg(
                                                    jsonObject(
                                                        key("id").value(PERMISSION.ID),
                                                        key("name").value(PERMISSION.NAME)
                                                    )
                                                )
                                            ).from(ROLE_PERMISSION)
                                                .leftJoin(PERMISSION)
                                                .on(PERMISSION.ID.eq(ROLE_PERMISSION.PERMISSION_ID))
                                                .where(ROLE_PERMISSION.ROLE_ID.eq(ROLE.ID))
                                                .orderBy(PERMISSION.NAME.asc()),
                                            jsonArray()
                                        )
                                    )
                                )
                            ),
                            jsonArray()
                        )
                    ),
                    key("permissions").value(
                        coalesce(
                            jsonArrayAgg(
                                jsonObject(
                                    key("id").value(PERMISSION.ID),
                                    key("name").value(PERMISSION.NAME)
                                )
                            ),
                            jsonArray()
                        )
                    )
                )
            ).from(USER)
            .leftJoin(USER_ROLE).on(USER.ID.eq(USER_ROLE.USER_ID))
            .leftJoin(ROLE).on(USER_ROLE.ROLE_ID.eq(ROLE.ID))
            .leftJoin(USER_PERMISSION).on(USER.ID.eq(USER_PERMISSION.USER_ID))
            .leftJoin(PERMISSION).on(USER_PERMISSION.PERMISSION_ID.eq(PERMISSION.ID));
        if (role != null) {
            query.where(ROLE.NAME.eq(role));
        }
        if (permission != null) {
            query.where(PERMISSION.NAME.eq(role));
        }
        return query.groupBy(USER.ID)
            .orderBy(USER.ID.asc())
            .fetch().into(JSONObject.class);
    }

Is there any way to fix this problem?


Solution

  • Why the duplicates?

    Your join graph creates a cartesian product between the two "nested collections" ROLE and PERMISSION. You can't remove that cartesian product with GROUP BY alone, that works only if you join a single to-many relationship.

    Instead, you can write subqueries like this (you already did this correctly for the ROLE_PERMISSION relationship):

    dslContext.select(jsonObject(
        key("id").value(USER.ID),
        key("username").value(USER.USERNAME),
        key("roles").value(coalesce(field(
            select(jsonArrayAgg(jsonObject(
                key("id").value(ROLE.ID),
                key("name").value(ROLE.NAME),
                key("permission").value(coalesce(field(
                    select(coalesce(jsonArrayAgg(jsonObject(
                        key("id").value(PERMISSION.ID),
                        key("name").value(PERMISSION.NAME)
                    )), jsonArray()))
                    .from(ROLE_PERMISSION)
                    .join(PERMISSION)
                    .on(PERMISSION.ID.eq(ROLE_PERMISSION.PERMISSION_ID))
                    .where(ROLE_PERMISSION.ROLE_ID.eq(ROLE.ID))
                    .orderBy(PERMISSION.NAME.asc())
                ), jsonArray()))
            )))
            .from(USER_ROLE)
            .join(ROLE)
            .on(USER_ROLE.ROLE_ID.eq(ROLE.ID))
            .where(USER_ROLE.USER_ID.eq(USER.ID))
        ), jsonArray())),
        key("permissions").value(coalesce(field(
            select(coalesce(jsonArrayAgg(jsonObject(
                key("id").value(PERMISSION.ID),
                key("name").value(PERMISSION.NAME)
            )))
            .from(USER_PERMISSION)
            .join(PERMISSION)
            .on(USER_PERMISSION.PERMISSION_ID.eq(PERMISSION.ID))
            .where(USER_PERMISSION.USER_ID.eq(USER.ID))
        ), jsonArray()))
    ))
    .from(USER)
    .orderBy(USER.ID.asc())
    .fetch().into(JSONObject.class);
    

    Join vs semi join

    After you edited your question to become a slightly different question, the point you were trying to make is that you want to filter the USER table by some ROLE or PERMISSION that they must have. You can't achieve this with JOIN alone (unless you're happy with the duplicates). The answer I gave doesn't change. If you're joining multiple to-many relationships, you'll get cartesian products.

    So, instead, why not semi join them? Either with jOOQ's synthetic SEMI JOIN syntax, or manually using EXISTS or IN, e.g.

    .where(role != null
        ? exists(selectOne()
              .from(USER_ROLE)
              .where(USER_ROLE.role().NAME.eq(role))
          )
        : noCondition()
    )
    .and(permission != null
        ? exists(selectOne()
              .from(USER_PERMISSION)
              .where(USER_PERMISSION.permission().NAME.eq(permission))
          )
        : noCondition()
    )
    

    This is using the implicit join syntax, which is optional, but I think it does simplify your query.