Выборка данных из нескольких неймспейсов
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")
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
.
Базовый запрос с 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")
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")
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")
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"
}
]
}
}
]
}'
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
.
На текущий момент скобки после NOT являются обязательными, это поведение будет изменено позднее.
query := db.Query("items_with_join").
Not().
OpenBracket(). // Здесь необходимо поставить скобки, чтобы операция NOT сработала для InnerJoin
InnerJoin(
db.Query("actors").
WhereBool("is_visible", reindexer.EQ, true).
Limit(0),
"actors").
On("id", reindexer.EQ, "id")
CloseBracket()
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.QE, 2022)
query2 := db.Query("test_namespace2").
Where("price", reindexer.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
]
}
]
}
]
}'
С информацией про объединение результатов при выборке из нескольких неймспейсов при полнотекстовом поиске можно ознакомиться в разделе «Объединение результатов полнотекстового поиска».