Выборка данных из нескольких неймспейсов
Reindexer может объединять документы из нескольких неймспейсов в один результат при выполнении запросов на выборку данных.
JOIN
Оператор Join используется для выборки данных из двух неймспейсов и включения этих данных в один результирующий набор.
Запрос с Join может содержать одно или несколько условий в On, соединенных с помощью операторов And (по умолчанию), Or или Not:
query := db.Query("items_with_join").
InnerJoin(db.Query("actors").
WhereBool("is_visible", reindexer.EQ, true), "actors").
On("actors_ids", reindexer.SET, "id").
Or().
On("actors_names", reindexer.SET, "name")
ResultIterator<Item> query = db.query("items_with_join", Actor.class)
.innerJoin(
db.query("actors", Actor.class)
.where("is_visible", Condition.EQ, true)
.on("actors_ids", Condition.SET, "id")
.or()
.on("actors_names", Condition.SET, "name");
"actors"
);
InnerJoin объединяет данные из двух неймспейсов, в которых есть совпадение на присоединяющихся полях в обоих неймспейсах.
LeftJoin возвращает все элементы из неймспейса на левой стороне ключевого слова LeftJoin, вместе со значениями из неймспейса на правой стороне, или ничего, если соответствующий элемент не существует.
Join — это псевдоним для LeftJoin.
InnerJoin может использоваться как условие в Where и комбинироваться с использованием логических операторов Or, And и Not:
query1 := db.Query("items_with_join").
WhereInt("id", reindexer.RANGE, []int{0, 100}).
Or().
InnerJoin(db.Query("actors").WhereString("name", reindexer.EQ, "ActorName"), "actors").
On("actors_ids", reindexer.SET, "id").
Or().
InnerJoin(db.Query("actors").WhereInt("id", reindexer.RANGE, []int{100, 200}), "actors").
On("actors_ids", reindexer.SET, "id")
query2 := db.Query("items_with_join").
WhereInt("id", reindexer.RANGE, []int{0, 100}).
Or().
OpenBracket().
InnerJoin(db.Query("actors").WhereString("name", reindexer.EQ, "ActorName"), "actors").
On("actors_ids", reindexer.SET, "id").
InnerJoin(db.Query("actors").WhereInt("id", reindexer.RANGE, []int{100, 200}), "actors").
On("actors_ids", reindexer.SET, "id").
CloseBracket()
Обратите внимание, что обычно оператор Or реализует логику сокращенных вычислений для условий: если предыдущее условие верно, то следующее не вычисляется.
Но в случае InnerJoin это работает иначе: в query1 (из примера выше) оба условия InnerJoin вычисляются, несмотря на результат WhereInt.
ResultIterator<Item> query1 = db.query("items_with_join", Actor.class)
.where("id", Condition.RANGE, 0, 100)
.or()
.innerJoin(
db.query("actors", Actor.class)
.where("name", Condition.EQ, "ActorName")
.on("actors_ids", Condition.SET, "id"),
"actors"
)
.or()
.innerJoin(
db.query("actors", Item.class)
.where("id", Condition.RANGE, 100, 200)
.on("actors_ids", Condition.SET, "id"),
"actors"
)
ResultIterator<Item> query2 = db.query("items_with_join", Actor.class)
.where("id", Condition.RANGE, 0, 100)
.or()
.openBracket()
.innerJoin(
db.query("actors", Actor.class)
.where("name", Condition.EQ, "ActorName")
.on("actors_ids", Condition.SET, "id"),
"actors"
)
.innerJoin(
db.query("actors", Actor.class)
.where(("id", Condition.RANGE, 100, 200")
.on("actors_ids", Condition.SET, "id"),
"actors"
)
.closeBracket();
Обратите внимание, что обычно оператор or реализует логику сокращенных вычислений для условий: если предыдущее условие верно, то следующее не вычисляется.
Но в случае innerJoin это работает иначе: в query1 (из примера выше) оба условия innerJoin вычисляются, несмотря на результат where.
Базовый запрос с JOIN
По умолчанию в Go для сохранения результатов Join используется поле, помеченное в тегах как joined и указанное в качестве второго аргумента в вызове InnerJoin/LeftJoin/Join.
В примере ниже данные из неймспейса "favorites" будут помещены в поле Favorites в объекте типа MediaItem.
type Favorite struct {
ID int `reindex:"id,,pk" json:"id"`
ContentID int `reindex:"content_id,tree" json:"content_id"`
}
type MediaItem struct {
ID int `reindex:"id,,pk" json:"id"`
Year int `reindex:"year,tree" json:"year"`
Favorites []*Favorite `reindex:"joined_favorites,,joined"`
}
query := db.Query("media_items").Where("year", reindexer.GT, 1990).
InnerJoin(db.Query("favorites"), "joined_favorites").
On("id", reindexer.SET, "content_id")
favorites = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
'is_pk': True,
},
{
"name": "content_id",
"json_paths": ["content_id"],
"field_type": "int",
"index_type": "tree"
}
]
for index in favorites:
db.index_add("favorites", index)
media_items = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
'is_pk': True,
},
{
"name": "year",
"json_paths": ["year"],
"field_type": "int",
"index_type": "tree",
}
]
for index in media_items:
db.index_add("media_items", index)
query2 = db.new_query("favorites")
query1 = (
db.new_query("media_items")
.where("year", CondType.CondGt, 1990)
.inner_join(query2, "joined_favorites")
.on("id", CondType.CondSet, "content_id")
)
В Java для сохранения результатов join используется поле, помеченное аннотацией @Transient и указанное в качестве второго аргумента в вызове innerJoin/leftJoin/join.
В примере ниже данные из неймспейса "favorites" будут помещены в поле joinedFavorites в объекте класса MediaItem.
import ru.rt.restream.reindexer.annotations.Transient;
public static class Favorite {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "content_id", type = IndexType.TREE)
private int contentId;
public Favorite() {
}
public Favorite(int id, int contentId) {
this.id = id;
this.contentId = contentId;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getContentId() {
return contentId;
}
public void setContentId(int contentId) {
this.contentId = contentId;
}
@Override
public String toString() {
return "Favorite{" +
"id=" + id +
", contentId=" + contentId +
'}';
}
}
public static class MediaItem {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "year", type = IndexType.TREE)
private int year;
@Transient
private List<Favorite> joinedFavorites;
public MediaItem() {
}
public MediaItem(int id, int year, List<Favorite> joinedFavorites) {
this.id = id;
this.year = year;
this.favorites = joinedFavorites;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public List<Favorite> getFavorites() {
return joinedFavorites;
}
public void setFavorites(List<Favorite> joinedFavorites) {
this.favorites = joinedFavorites;
}
@Override
public String toString() {
return "MediaItem{" +
"id=" + id +
", year=" + year +
", joinedFavorites=" + joinedFavorites +
'}';
}
}
// ...
Query<MediaItem> query = db.query("media_item", MediaItem.class)
.where("year", Condition.GT, 1990)
.innerJoin(
db.query("favorites", Favorite.class)
.on("id", Condition.SET, "content_id"),
"joinedFavorites");
SELECT * FROM media_items WHERE year > 1990 INNER JOIN favorites ON (favorites.content_id IN media_items.id)
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "media_items",
"type": "select",
"filters": [
{
"field": "year",
"cond": "GE",
"value": [
1990
],
"join_query": {
"namespace": "favorites",
"type": "INNER",
"on": [
{
"left_field": "id",
"right_field": "content_id",
"cond": "EQ"
}
]
}
}
]
}'
Запрос с условиями в joined-подзапросе
Для фильтрации по полям joined-неймспейсов условия фильтрации должны указываться внутри WHERE соответствующего подзапроса. Помимо различных условий фильтрации joined-подзапрос может содержать сортировки, LIMIT и OFFSET. Для SQL синтаксис в этом случае будет отличаться от стандартного.
type Service struct {
ID int `reindex:"id,,pk" json:"id"`
Year int `reindex:"year,tree" json:"year"`
MRF string `reindex:"mrf" json:"mrf"`
Data string `json:"data"`
}
type MediaItem struct {
ID int `reindex:"id,,pk" json:"id"`
Packages []int64 `reindex:"packages" json:"packages"`
Services []*Service `reindex:"joined_services,,joined"`
}
jquery := DB.Query("services").
Where("mrf", reindexer.EQ, "mos").
Sort("year", false).Limit(5)
query := DB.Query("media_items").
InnerJoin(jquery, "joined_services").
On("packages", reindexer.SET, "id")
services = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
'is_pk': True,
},
{
"name": "year",
"json_paths": ["year"],
"field_type": "int",
"index_type": "tree",
},
{
"name": "mrf",
"json_paths": ["mrf"],
"field_type": "string"
},
{
"name": "data",
"json_paths": ["data"],
"field_type": "string"
}
]
for index in services:
db.index_add("services", index)
media_items = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
'is_pk': True,
},
{
"name": "packages",
"json_paths": ["packages"],
"field_type": "int",
"is_array": True
}
]
for index in media_items:
db.index_add("media_items", index)
query2 = (
db.new_query("services")
.where("mrf", CondType.CondEq, "mos")
.sort("year", False)
.Limit(5)
)
query1 = (
db.new_query("media_items")
.inner_join(query2, "joined_services")
.on("packages", CondType.CondSet, "id")
)
import ru.rt.restream.reindexer.annotations.Transient;
public static class Service {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "year", type = IndexType.TREE)
private int year;
@Reindex(name = "mrf")
private String mrf;
@Reindex(name = "data")
private String data;
public Service() {
}
public Service(int id, int year, String mrf, String data) {
this.id = id;
this.year = year;
this.mrf = mrf;
this.data = data;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public String getMrf() {
return mrf;
}
public void setMrf(String mrf) {
this.mrf = mrf;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "Service{" +
"id=" + id +
", year=" + year +
", mrf=" + mrf +
", data=" + data +
'}';
}
}
public static class MediaItem {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "year", type = IndexType.TREE)
private int year;
@Reindex(name = "packages")
private List<Integer> packages;
@Transient
private List<Service> services;
public MediaItem() {
}
public MediaItem(int id, int year, List<Integer> packages, List<Service> services) {
this.id = id;
this.year = year;
this.packages = packages;
this.services = services;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
public List<Integer> getPackages() {
return packages;
}
public void setpackages(List<Integer> packages) {
this.packages = packages;
}
public List<Service> getServices() {
return services;
}
public void setServices(List<Service> services) {
this.services = services;
}
@Override
public String toString() {
return "MediaItem{" +
"id=" + id +
", year=" + year +
", packages=" + packages +
", services=" + services +
'}';
}
}
// ...
Query<Service> jquery = db.query("services", Service.class)
.where("mrf", Condition.EQ, "mos")
.sort("year", false)
.limit(5);
Query<MediaItem> query = db.query("media_item", MediaItem.class)
.innerJoin(
jquery
.on("packages", Condition.SET, "id"),
"favorites");
SELECT * FROM media_items INNER JOIN (SELECT * FROM services WHERE mrf = 'mos' ORDER BY year LIMIT 5) ON media_items.packages IN services.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "media_items",
"type": "select",
"filters": [
{
"join_query": {
"namespace": "services",
"type": "INNER",
"filters": [
{
"field": "mrf",
"cond": "EQ",
"value": [
"mos"
]
}
],
"sort": [
{
"field": "year",
"desc": false
}
],
"limit": 5,
"on": [
{
"left_field": "packages",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}'
InnerJoin может быть также использован в качестве обычного условия фильтрации без присоединения документов (например, в ситуациях, когда требуется проверка условий по полям другого неймспейса).
Для этого требуется установить LIMIT равный 0 у joined-подзапроса:
jquery := DB.Query("services").
Where("mrf", reindexer.EQ, "mos").
Limit(0)
query := DB.Query("media_items").
InnerJoin(jquery, "joined_services").
On("packages", reindexer.SET, "id")
query2 = (
db.new_query("services")
.where("mrf", CondType.CondEq, "mos")
.Limit(0)
)
query1 = (
db.new_query("media_items")
.inner_join(query2, "joined_services")
.on("packages", CondType.CondSet, "id")
)
Query<Service> jquery = db.query("services", Service.class)
.where("mrf", Condition.EQ, "mos")
.sort("year", false)
.limit(5);
Query<MediaItem> query = db.query("media_item", MediaItem.class)
.innerJoin(
jquery
.on("packages", Condition.SET, "id"),
"favorites");
SELECT * FROM media_items INNER JOIN (SELECT * FROM services WHERE mrf = 'mos' LIMIT 0) ON media_items.packages IN services.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "media_items",
"type": "select",
"filters": [
{
"join_query": {
"namespace": "services",
"type": "INNER",
"filters": [
{
"field": "mrf",
"cond": "EQ",
"value": [
"mos"
]
}
],
"limit": 0,
"on": [
{
"left_field": "packages",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}'
LIMIT внутри joined-подзапроса отвечает за максимальное количество документов joined-неймспейса, которое может быть присоединено к каждому документу из основного неймспейса. Например, при запросе:
SELECT * FROM media_items INNER JOIN (SELECT * FROM services LIMIT 2) ON media_items.packages IN services.id
в результирующем joined-поле будет не более двух документов, даже если по условию ON media_items.packages IN services.id их должно было быть больше.
OFFSET внутри joined-подзапроса на данный момент игнорируется.
Joinable-интерфейс и JoinHandler
Joinable-интерфейс
При использовании псевдоиндекса с опцией joined (как в примере ниже), Go-коннектор создаёт объектное представление приджойненных данных в виде слайса указателей, используя рефлексию.
type Actor struct {
ID int `reindex:"id"`
Name string `reindex:"name"`
IsVisible bool `reindex:"is_visible"`
}
type ItemWithJoin struct {
ID int `reindex:"id"`
Name string `reindex:"name"`
ActorsIDs []int `reindex:"actors_ids"`
ActorsNames []int `reindex:"actors_names"`
Actors []*Actor `reindex:"joined_actors,,joined"`
}
query := db.Query("items_with_join").
Join(db.Query("actors").
WhereBool("is_visible", reindexer.EQ, true), "joined_actors").
On("actors_ids", reindexer.SET, "id")
it := query.Exec()
Чтобы избежать использования рефлексии, можно создать структуру, которая реализует интерфейс Joinable.
Это увеличивает общую производительность Join на 10-20% и уменьшает количество аллокаций, так как в этом случае коннектору не требуется извлекать поле структуры, в которое нужно положить приджойненные данные:
// Реализация Joinable-интерфейса.
// Join добавляет к объекту `ItemWithJoin` элементы из присоединенного неймспейса.
// При вызове Joinable interface можно передать дополнительную контекстную переменную для реализации дополнительной логики в Join.
func (item *ItemWithJoin) Join(field string, subitems []interface{}, context interface{}) {
// В field будет передана строка, использованная в качестве второго аргумента Join при формировании Query
switch field {
case "joined_actors":
for _, joinItem := range subitems {
item.Actors = append(item.Actors, joinItem.(*Actor))
}
}
}
При использовании Joinable-интерфейса нет необходимости указывать joined-псевдоиндекс в Go-структуре.
JoinHandler
JoinHandler — это альтернатива для Joinable-интерфейса, которая также позволяет увеличить производительность при выполнении запросов с Join.
JoinHandler представляет собой функтор, который выполняет обработку joined-документов и должен вернуть true или false:
- Если
JoinHandlerвернетfalse, это значит, что он берет на себя всю ответственность заJoin; - Если
JoinHandlerвернетtrue, это значит, что он выполнит только часть работы, необходимой при соединении. Для выполнения остальной работы будет использована стратегия автоматического соединения.
Стратегия автоматического соединения может реализовываться одним из следующих образов:
- С использованием метода
Joinдля выполнения запроса (в случае, если для типа реализован Joinable-интерфейс); - С использованием рефлексии (если Joinable-интерфейс не реализован).
Пример использования JoinHandler:
query := db.Query("items_with_join").
JoinHandler("joined_actors", func(field string, item interface{}, subitems []interface{}) (isContinue bool) {
switch field {
case "joined_actors":
for _, joinItem := range subitems {
item.Actors = append(item.Actors, joinItem.(*Actor))
}
}
return false
}).
Join(db.Query("actors").
WhereBool("is_visible", reindexer.EQ, true), "actors").
On("actors_ids", reindexer.SET, "id")
it := query.Exec()
Эквивалентная запись (JoinHandler должен прикрепляться к основному запросу, а не к joined-подзапросам):
query := db.Query("items_with_join").
JoinHandler("joined_actors", func(field string, item interface{}, subitems []interface{}) bool {
// В field будет передана строка, использованная в качестве второго аргумента Join при формировании Query.
// В случае с JoinHandler она всегда будет совпадать с именем поля, переданным при регистрации JoinHandler'а.
for _, joinItem := range subitems {
item.Actors = append(item.Actors, joinItem.(*Actor))
}
return false
})
jquey := db.Query("actors").WhereBool("is_visible", reindexer.EQ, true), "actors")
it := query.Join(jquey, "joined_actors").On("actors_ids", reindexer.SET, "id").Exec()
Запрет рефлексии в запросах с Join
Чтобы запретить использование рефлексии при выполнении запросов с Join, при создании подключения к базе данных передайте опцию reindexer.WithStrictJoinHandlers().
Пример:
db := reindexer.NewReindex("cproto://127.0.0.1:6534/mydb", reindexer.WithCreateDBIfMissing(), reindexer.WithStrictJoinHandlers())
Она включает дополнительную проверку, благодаря которой запрещается рефлексия в запросах с Join.
С включенной опцией, если для подзапроса на задан JoinHandler и нет Joined-интерфейса, он будет возвращать ошибку.
ANTI-JOIN
Reindexer не поддерживает ANTI JOIN напрямую, однако полностью эквивалентную выборку можно получить, используя комбинацию NOT и INNER JOIN.
query := db.Query("items_with_join")
.Not()
.InnerJoin(
db.Query("actors")
.WhereBool("is_visible", reindexer.EQ, true)
.Limit(0),
"actors")
.On("id", reindexer.EQ, "id")
query2 = (
db.new_query("actors")
.where("is_visible", CondType.CondEq, True)
.limit(0)
)
query1 = (
db.new_query("items")
.op_not()
.inner_join(query2, "actors")
.on("id", CondType.CondEq, "id")
)
SELECT * FROM items_with_join WHERE NOT INNER JOIN (SELECT * FROM actors WHERE is_visible = true) ON items_with_join.id = actors.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "items_with_join",
"type": "select",
"filters": [
{
"op": "NOT",
"filters": [
{
"join_query": {
"type": "inner",
"namespace": "actors",
"filters": [
{
"field": "is_visible",
"cond": "EQ",
"value": [
true
]
}
],
"limit": 0,
"on": [
{
"left_field": "id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}
]
}'
MERGE
Для выборки данных из нескольких неймспейсов может использоваться оператор MERGE.
Пример:
query := db.Query("test_namespace").
Where("year", reindexer.EQ, 2022)
query2 := db.Query("test_namespace2").
Where("price", reindexer.LE, 1000)
query.Merge(query2)
query = (
db.new_query("test_namespace")
.where("year", CondType.CondEq, 2022)
)
query2 = (
db.new_query("test_namespace2")
.where("price", CondType.CondLe, 1000)
)
query.merge(query2)
Namespace<Item> testNamespace = db.openNamespace("test_namespace", NamespaceOptions.defaultOptions(), Item.class);
Query<Item> query = testNamespace.query()
.where("year", Condition.EQ, 2022);
Query<Item> query2 = testNamespace.query()
.where("price", Condition.LE, 1000);
query.merge(query2);
SELECT * FROM test_namespace WHERE year = 2022 MERGE (SELECT * FROM test_namespace2 WHERE price <=1000)
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "test_namespace",
"type": "select",
"filters": [
{
"field": "year",
"cond": "EQ",
"value": [
2022
]
}
],
"merge_queries": [
{
"namespace": "test_namespace2",
"type": "select",
"filters": [
{
"field": "price",
"cond": "LE",
"value": [
1000
]
}
]
}
]
}'
С информацией про объединение результатов при выборке из нескольких неймспейсов при полнотекстовом поиске можно ознакомиться в разделе «Объединение результатов полнотекстового поиска».