From ee532ceecc1987157cf3d6d03513ef8bce941e4f Mon Sep 17 00:00:00 2001 From: Stanislav Varganov Date: Fri, 14 Aug 2026 18:32:59 +0300 Subject: [PATCH] Nested Join for reindexer-java --- builtin-adapter/BuiltinAdapter.cpp | 4 +- .../java/ru/rt/restream/reindexer/Query.java | 61 ++-- .../reindexer/binding/QueryResultReader.java | 7 +- .../builtin/BuiltinRequestContext.java | 4 +- .../binding/cproto/PhysicalConnection.java | 4 +- .../reindexer/connector/JoinTest.java | 329 ++++++++++++++++++ 6 files changed, 379 insertions(+), 30 deletions(-) diff --git a/builtin-adapter/BuiltinAdapter.cpp b/builtin-adapter/BuiltinAdapter.cpp index eac37126..a4b5e0d9 100644 --- a/builtin-adapter/BuiltinAdapter.cpp +++ b/builtin-adapter/BuiltinAdapter.cpp @@ -102,7 +102,9 @@ JNIEXPORT jobject JNICALL Java_ru_rt_restream_reindexer_binding_builtin_BuiltinA reindexer_string dsn = rx_string(env, path); reindexer_string vers = rx_string(env, version); reindexer_error error = reindexer_connect(rx, dsn, ConnectOpts(), vers, BindingCapabilities( - kBindingCapabilityResultsWithShardIDs + kBindingCapabilityQrIdleTimeouts + | kBindingCapabilityResultsWithShardIDs + | kBindingCapabilityIncarnationTags | kBindingCapabilityComplexRank | kBindingCapabilityQueryFormatV2)); env->ReleaseStringUTFChars(path, reinterpret_cast(dsn.p)); diff --git a/src/main/java/ru/rt/restream/reindexer/Query.java b/src/main/java/ru/rt/restream/reindexer/Query.java index e2885835..4423d31e 100644 --- a/src/main/java/ru/rt/restream/reindexer/Query.java +++ b/src/main/java/ru/rt/restream/reindexer/Query.java @@ -199,6 +199,8 @@ public enum Condition { private final List joinFields = new ArrayList<>(); + private Query lastJoinQuery; + private final List> mergeQueries = new ArrayList<>(); private final List> namespaces = new ArrayList<>(); @@ -296,6 +298,11 @@ public Query leftJoin(Query joinQuery, String field) { } private Query join(Query joinQuery, String field, int joinType) { + if (root != null) { + @SuppressWarnings("unchecked") + Query rootQuery = (Query) root; + return rootQuery.join(joinQuery, field, joinType); + } logBuilder.join(joinQuery.logBuilder, joinType); if (joinQuery.root != null) { throw new IllegalStateException("query.join call on already joined query. You should create new Query"); @@ -311,6 +318,7 @@ private Query join(Query joinQuery, String field, int joinType) { joinQuery.root = this; joinQueries.add(joinQuery); joinFields.add(field); + lastJoinQuery = joinQuery; return this; } @@ -323,12 +331,13 @@ private Query join(Query joinQuery, String field, int joinType) { * @return the {@link Query} for further customizations */ public Query on(String joinField, Condition condition, String joinIndex) { - logBuilder.on(nextOperation, joinField, condition.code, joinIndex); - buffer.putVarUInt32(QUERY_JOIN_ON); - buffer.putVarUInt32(nextOperation); - buffer.putVarUInt32(condition.code); - buffer.putVString(joinField); - buffer.putVString(joinIndex); + Query target = lastJoinQuery != null ? lastJoinQuery : this; + target.logBuilder.on(nextOperation, joinField, condition.code, joinIndex); + target.buffer.putVarUInt32(QUERY_JOIN_ON); + target.buffer.putVarUInt32(nextOperation); + target.buffer.putVarUInt32(condition.code); + target.buffer.putVString(joinField); + target.buffer.putVString(joinIndex); nextOperation = OP_AND; return this; } @@ -1317,27 +1326,23 @@ public byte[] bytes() { } private byte[] toSubQueryBytes(int formatVersion) { - byte[] queryBytes = getQueryBytes(formatVersion); - if (formatVersion == QUERY_FORMAT_V2 || hasNestedJoins()) { - ByteBuffer copy = new ByteBuffer(queryBytes); - copy.putVarUInt32(QUERY_END); - copy.putVarUInt32(0); - copy.putVarUInt32(0); - return copy.bytes(); + if (formatVersion == QUERY_FORMAT_V2) { + return serializeQuery(formatVersion); + } + if (!joinQueries.isEmpty() || !mergeQueries.isEmpty()) { + throw new IllegalStateException("Join and merge queries in subquery are not supported by QueryFormatV1"); } - return queryBytes; + return getQueryBytes(formatVersion); } private byte[] toExecutableBytes() { int formatVersion = reindexer.getBinding().queryFormatVersion(); - ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); - queryBuffer.putVarUInt32(QUERY_END); if (formatVersion == QUERY_FORMAT_V2) { - appendJoinQueries(queryBuffer, new ArrayList<>(), formatVersion); - appendMergeQueries(queryBuffer, new ArrayList<>(), formatVersion); - } else { - appendJoinQueriesV1(queryBuffer, false); + return serializeQuery(formatVersion); } + ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); + queryBuffer.putVarUInt32(QUERY_END); + appendJoinQueriesV1(queryBuffer, false); return queryBuffer.bytes(); } @@ -1367,6 +1372,14 @@ private void appendMergeQueries(ByteBuffer target, List> t } } + private byte[] serializeQuery(int formatVersion) { + ByteBuffer queryBuffer = new ByteBuffer(getQueryBytes(formatVersion)); + queryBuffer.putVarUInt32(QUERY_END); + appendJoinQueries(queryBuffer, new ArrayList<>(), formatVersion); + appendMergeQueries(queryBuffer, new ArrayList<>(), formatVersion); + return queryBuffer.bytes(); + } + private void appendQuery(ByteBuffer target, Query query, int queryJoinType, List> targetNamespaces, int formatVersion) { if (queryJoinType != MERGE) { @@ -1382,7 +1395,7 @@ private void appendQuery(ByteBuffer target, Query query, int queryJoinType, } private void appendJoinQueriesV1(ByteBuffer target, boolean appendNamespaces) { - if (hasNestedJoins()) { + if (hasNestedQueries()) { throw new IllegalStateException("Nested joins are not supported by QueryFormatV1"); } for (Query joinQuery : joinQueries) { @@ -1410,14 +1423,14 @@ private void appendMergeQueriesV1(ByteBuffer target) { } } - private boolean hasNestedJoins() { + private boolean hasNestedQueries() { for (Query joinQuery : joinQueries) { - if (!joinQuery.joinQueries.isEmpty() || joinQuery.hasNestedJoins()) { + if (!joinQuery.joinQueries.isEmpty() || !joinQuery.mergeQueries.isEmpty() || joinQuery.hasNestedQueries()) { return true; } } for (Query mergeQuery : mergeQueries) { - if (mergeQuery.hasNestedJoins()) { + if (!mergeQuery.mergeQueries.isEmpty() || mergeQuery.hasNestedQueries()) { return true; } } diff --git a/src/main/java/ru/rt/restream/reindexer/binding/QueryResultReader.java b/src/main/java/ru/rt/restream/reindexer/binding/QueryResultReader.java index 62aa2779..4fbcddc5 100644 --- a/src/main/java/ru/rt/restream/reindexer/binding/QueryResultReader.java +++ b/src/main/java/ru/rt/restream/reindexer/binding/QueryResultReader.java @@ -33,6 +33,7 @@ import static ru.rt.restream.reindexer.binding.Consts.QUERY_RESULT_RANK_FORMAT; import static ru.rt.restream.reindexer.binding.Consts.QUERY_RESULT_SHARDING_VERSION; import static ru.rt.restream.reindexer.binding.Consts.QUERY_RESULT_SHARD_ID; +import static ru.rt.restream.reindexer.binding.Consts.QUERY_FORMAT_V1; import static ru.rt.restream.reindexer.binding.Consts.QUERY_FORMAT_V2; import static ru.rt.restream.reindexer.binding.Consts.RANK_FORMAT_SINGLE_FLOAT; import static ru.rt.restream.reindexer.binding.Consts.RESULTS_FORMAT_MASK; @@ -60,7 +61,11 @@ public class QueryResultReader { * @return the {@link QueryResult} to use */ public QueryResult read(byte[] rawQueryResult) { - return read(rawQueryResult, Consts.QUERY_FORMAT_V1); + ByteBuffer buffer = new ByteBuffer(rawQueryResult).rewind(); + int queryFormatVersion = buffer.getVarUInt() == QUERY_FORMAT_V2 + ? QUERY_FORMAT_V2 + : QUERY_FORMAT_V1; + return read(rawQueryResult, queryFormatVersion); } /** diff --git a/src/main/java/ru/rt/restream/reindexer/binding/builtin/BuiltinRequestContext.java b/src/main/java/ru/rt/restream/reindexer/binding/builtin/BuiltinRequestContext.java index 6bc7cc24..49bc1965 100644 --- a/src/main/java/ru/rt/restream/reindexer/binding/builtin/BuiltinRequestContext.java +++ b/src/main/java/ru/rt/restream/reindexer/binding/builtin/BuiltinRequestContext.java @@ -22,8 +22,6 @@ import ru.rt.restream.reindexer.binding.RequestContext; import ru.rt.restream.reindexer.util.NativeUtils; -import static ru.rt.restream.reindexer.binding.Consts.QUERY_FORMAT_V2; - /** * A request context which is holds a {@link QueryResult}, * the {@link #fetchResults(int, int)} method is NOOP since Builtin does not support it. @@ -54,7 +52,7 @@ public BuiltinRequestContext(ReindexerResponse response) { } } QueryResultReader reader = new QueryResultReader(); - queryResult = reader.read(rawQueryResult, QUERY_FORMAT_V2); + queryResult = reader.read(rawQueryResult); queryResult.setResultsPtr(resultsPtr); } diff --git a/src/main/java/ru/rt/restream/reindexer/binding/cproto/PhysicalConnection.java b/src/main/java/ru/rt/restream/reindexer/binding/cproto/PhysicalConnection.java index 1bb0e7f1..73b91bb8 100644 --- a/src/main/java/ru/rt/restream/reindexer/binding/cproto/PhysicalConnection.java +++ b/src/main/java/ru/rt/restream/reindexer/binding/cproto/PhysicalConnection.java @@ -53,6 +53,7 @@ import static ru.rt.restream.reindexer.binding.Consts.BINDING_CAPABILITY_COMPLEX_RANK; import static ru.rt.restream.reindexer.binding.Consts.BINDING_CAPABILITY_NAMESPACE_INCARNATIONS; import static ru.rt.restream.reindexer.binding.Consts.BINDING_CAPABILITY_QUERY_FORMAT_V2; +import static ru.rt.restream.reindexer.binding.Consts.BINDING_CAPABILITY_QR_IDLE_TIMEOUTS; import static ru.rt.restream.reindexer.binding.Consts.BINDING_CAPABILITY_RESULTS_WITH_SHARD_IDS; import static ru.rt.restream.reindexer.binding.Consts.DEF_APP_NAME; import static ru.rt.restream.reindexer.binding.Consts.QUERY_FORMAT_V1; @@ -144,7 +145,8 @@ public PhysicalConnection(String host, int port, String user, String password, S -1, // expectedClusterID REINDEXER_VERSION, getAppName(), - BINDING_CAPABILITY_RESULTS_WITH_SHARD_IDS + BINDING_CAPABILITY_QR_IDLE_TIMEOUTS + | BINDING_CAPABILITY_RESULTS_WITH_SHARD_IDS | BINDING_CAPABILITY_COMPLEX_RANK | BINDING_CAPABILITY_NAMESPACE_INCARNATIONS | BINDING_CAPABILITY_QUERY_FORMAT_V2); diff --git a/src/test/java/ru/rt/restream/reindexer/connector/JoinTest.java b/src/test/java/ru/rt/restream/reindexer/connector/JoinTest.java index 0c093eef..77074b45 100644 --- a/src/test/java/ru/rt/restream/reindexer/connector/JoinTest.java +++ b/src/test/java/ru/rt/restream/reindexer/connector/JoinTest.java @@ -28,16 +28,22 @@ import ru.rt.restream.reindexer.ResultIterator; import ru.rt.restream.reindexer.annotations.Reindex; import ru.rt.restream.reindexer.annotations.Transient; +import ru.rt.restream.reindexer.binding.Consts; import ru.rt.restream.reindexer.db.DbBaseTest; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assumptions.assumeTrue; import static ru.rt.restream.reindexer.Query.Condition.EQ; import static ru.rt.restream.reindexer.Query.Condition.RANGE; import static ru.rt.restream.reindexer.Query.Condition.SET; @@ -837,6 +843,303 @@ public void testMultipleJoinsSameFieldAccumulateResults() { assertThat(result.joinedActors.get(1).name, is("second")); } + @Test + public void testNestedJoin() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + db.openNamespace("roles", NamespaceOptions.defaultOptions(), Role.class); + + ItemWithJoin item = new ItemWithJoin(); + item.id = 1; + item.name = "item"; + item.actorsIds = Collections.singletonList(10); + db.upsert("items_with_join", item); + + Actor actor = new Actor(); + actor.id = 10; + actor.name = "actor"; + actor.visible = true; + db.upsert("actors", actor); + + Role role = new Role(); + role.id = 100; + role.name = "actor"; + db.upsert("roles", role); + + Query actorQuery = db.query("actors", Actor.class) + .innerJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + + ResultIterator items = db.query("items_with_join", ItemWithJoin.class) + .where("id", EQ, 1) + .innerJoin(actorQuery, "joinedActors") + .on("actorsIds", SET, "id") + .execute(); + + assertThat(items.hasNext(), is(true)); + ItemWithJoin result = items.next(); + assertThat(result.joinedActors.size(), is(1)); + Actor joinedActor = result.joinedActors.get(0); + assertThat(joinedActor.id, is(10)); + assertThat(joinedActor.name, is("actor")); + assertThat(joinedActor.joinedRoles.size(), is(1)); + assertThat(joinedActor.joinedRoles.get(0).id, is(100)); + assertThat(joinedActor.joinedRoles.get(0).name, is("actor")); + } + + @Test + public void testNestedJoinWithLeftAndInnerCombinations() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + db.openNamespace("roles", NamespaceOptions.defaultOptions(), Role.class); + + ItemWithJoin itemWithRole = new ItemWithJoin(); + itemWithRole.id = 1; + itemWithRole.actorsIds = Collections.singletonList(10); + db.upsert("items_with_join", itemWithRole); + + ItemWithJoin itemWithoutRole = new ItemWithJoin(); + itemWithoutRole.id = 2; + itemWithoutRole.actorsIds = Collections.singletonList(20); + db.upsert("items_with_join", itemWithoutRole); + + Actor actorWithRole = new Actor(); + actorWithRole.id = 10; + actorWithRole.name = "actor-with-role"; + db.upsert("actors", actorWithRole); + + Actor actorWithoutRole = new Actor(); + actorWithoutRole.id = 20; + actorWithoutRole.name = "actor-without-role"; + db.upsert("actors", actorWithoutRole); + + Role role = new Role(); + role.id = 100; + role.name = "actor-with-role"; + db.upsert("roles", role); + + Query actorWithInnerRoleQuery = db.query("actors", Actor.class) + .innerJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + + List leftWithInnerNestedItems = db.query("items_with_join", ItemWithJoin.class) + .sort("id", false) + .leftJoin(actorWithInnerRoleQuery, "joinedActors") + .on("actorsIds", SET, "id") + .toList(); + + assertThat(leftWithInnerNestedItems.size(), is(2)); + assertThat(leftWithInnerNestedItems.get(0).joinedActors.size(), is(1)); + assertThat(leftWithInnerNestedItems.get(0).joinedActors.get(0).joinedRoles.size(), is(1)); + assertThat(leftWithInnerNestedItems.get(1).joinedActors.size(), is(0)); + + Query actorWithLeftRoleQuery = db.query("actors", Actor.class) + .leftJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + + List innerWithLeftNestedItems = db.query("items_with_join", ItemWithJoin.class) + .sort("id", false) + .innerJoin(actorWithLeftRoleQuery, "joinedActors") + .on("actorsIds", SET, "id") + .toList(); + + assertThat(innerWithLeftNestedItems.size(), is(2)); + assertThat(innerWithLeftNestedItems.get(0).joinedActors.size(), is(1)); + assertThat(innerWithLeftNestedItems.get(0).joinedActors.get(0).joinedRoles.size(), is(1)); + assertThat(innerWithLeftNestedItems.get(1).joinedActors.size(), is(1)); + assertThat(innerWithLeftNestedItems.get(1).joinedActors.get(0).joinedRoles.size(), is(0)); + } + + @Test + public void testMergeWithNestedJoins() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + db.openNamespace("roles", NamespaceOptions.defaultOptions(), Role.class); + + ItemWithJoin firstItem = new ItemWithJoin(); + firstItem.id = 1; + firstItem.actorsIds = Collections.singletonList(10); + db.upsert("items_with_join", firstItem); + + ItemWithJoin secondItem = new ItemWithJoin(); + secondItem.id = 2; + secondItem.actorsIds = Collections.singletonList(20); + db.upsert("items_with_join", secondItem); + + Actor firstActor = new Actor(); + firstActor.id = 10; + firstActor.name = "first"; + db.upsert("actors", firstActor); + + Actor secondActor = new Actor(); + secondActor.id = 20; + secondActor.name = "second"; + db.upsert("actors", secondActor); + + Role firstRole = new Role(); + firstRole.id = 100; + firstRole.name = "first"; + db.upsert("roles", firstRole); + + Role secondRole = new Role(); + secondRole.id = 200; + secondRole.name = "second"; + db.upsert("roles", secondRole); + + Query firstActorQuery = db.query("actors", Actor.class) + .innerJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + Query secondActorQuery = db.query("actors", Actor.class) + .innerJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + + List items = db.query("items_with_join", ItemWithJoin.class) + .where("id", EQ, 1) + .innerJoin(firstActorQuery, "joinedActors") + .on("actorsIds", SET, "id") + .merge(db.query("items_with_join", ItemWithJoin.class) + .where("id", EQ, 2) + .innerJoin(secondActorQuery, "joinedActors") + .on("actorsIds", SET, "id")) + .toList(); + + Map itemsById = new HashMap<>(); + for (ItemWithJoin item : items) { + itemsById.put(item.id, item); + } + + assertThat(itemsById.size(), is(2)); + assertThat(itemsById.get(1).joinedActors.get(0).joinedRoles.get(0).name, is("first")); + assertThat(itemsById.get(2).joinedActors.get(0).joinedRoles.get(0).name, is("second")); + } + + @Test + public void testNestedJoinWithDepthTwo() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + db.openNamespace("roles", NamespaceOptions.defaultOptions(), Role.class); + db.openNamespace("permissions", NamespaceOptions.defaultOptions(), Permission.class); + + ItemWithJoin item = new ItemWithJoin(); + item.id = 1; + item.actorsIds = Collections.singletonList(10); + db.upsert("items_with_join", item); + + Actor actor = new Actor(); + actor.id = 10; + actor.name = "actor"; + db.upsert("actors", actor); + + Role role = new Role(); + role.id = 100; + role.name = "actor"; + db.upsert("roles", role); + + Permission permission = new Permission(); + permission.id = 1000; + permission.name = "actor"; + db.upsert("permissions", permission); + + Query roleQuery = db.query("roles", Role.class) + .innerJoin(db.query("permissions", Permission.class), "joinedPermissions") + .on("name", EQ, "name"); + Query actorQuery = db.query("actors", Actor.class) + .innerJoin(roleQuery, "joinedRoles") + .on("name", EQ, "name"); + + ItemWithJoin result = db.query("items_with_join", ItemWithJoin.class) + .where("id", EQ, 1) + .innerJoin(actorQuery, "joinedActors") + .on("actorsIds", SET, "id") + .getOne(); + + Actor joinedActor = result.joinedActors.get(0); + Role joinedRole = joinedActor.joinedRoles.get(0); + assertThat(joinedRole.id, is(100)); + assertThat(joinedRole.joinedPermissions.size(), is(1)); + assertThat(joinedRole.joinedPermissions.get(0).id, is(1000)); + } + + @Test + public void testCannotQuerySelectJoinWithMerge() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + + ItemWithJoin item = new ItemWithJoin(); + item.id = 1; + item.actorsIds = Collections.singletonList(10); + db.upsert("items_with_join", item); + + Actor actor = new Actor(); + actor.id = 10; + actor.name = "actor"; + db.upsert("actors", actor); + + Query actorQuery = db.query("actors", Actor.class) + .merge(db.query("actors", Actor.class)); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> readAll(db.query("items_with_join", ItemWithJoin.class) + .innerJoin(actorQuery, "joinedActors") + .on("actorsIds", SET, "id") + .execute())); + + assertThat(exception.getMessage(), containsString("MERGEs nested into the JOINs are not supported")); + } + + @Test + public void testCannotQuerySelectSubqueryWithJoin() { + assumeQueryFormatV2(); + + db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); + db.openNamespace("actors", NamespaceOptions.defaultOptions(), Actor.class); + db.openNamespace("roles", NamespaceOptions.defaultOptions(), Role.class); + + ItemWithJoin item = new ItemWithJoin(); + item.id = 1; + db.upsert("items_with_join", item); + + Actor actor = new Actor(); + actor.id = 1; + actor.name = "actor"; + db.upsert("actors", actor); + + Query subQuery = db.query("actors", Actor.class) + .select("id") + .innerJoin(db.query("roles", Role.class), "joinedRoles") + .on("name", EQ, "name"); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> readAll(db.query("items_with_join", ItemWithJoin.class) + .where("id", SET, subQuery) + .execute())); + + assertThat(exception.getMessage(), containsString("Join cannot be in subquery")); + } + + private void assumeQueryFormatV2() { + assumeTrue(db.getBinding().queryFormatVersion() == Consts.QUERY_FORMAT_V2, + "Nested joins require QueryFormatV2"); + } + + private void readAll(ResultIterator iterator) { + try (ResultIterator closeableIterator = iterator) { + while (closeableIterator.hasNext()) { + closeableIterator.next(); + } + } + } + @Test public void testJoinInWherePartWithBrackets() { db.openNamespace("items_with_join", NamespaceOptions.defaultOptions(), ItemWithJoin.class); @@ -1038,6 +1341,32 @@ public static class Actor { @Reindex(name = "is_visible") private boolean visible; + + @Transient + private List joinedRoles; + } + + @Setter + @Getter + public static class Role { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "name") + private String name; + + @Transient + private List joinedPermissions; + } + + @Setter + @Getter + public static class Permission { + @Reindex(name = "id", isPrimaryKey = true) + private Integer id; + + @Reindex(name = "name") + private String name; } @Setter