Выборка данных из нескольких неймспейсов

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
          ]
        }
      ]
    }
  ]
}'

С информацией про объединение результатов при выборке из нескольких неймспейсов при полнотекстовом поиске можно ознакомиться в разделе «Объединение результатов полнотекстового поиска».