Выборка данных из нескольких неймспейсов
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"
}
]
}
}
]
}'
Вложенный запрос с JOIN
Запрос с JOIN может содержать подзапросы с JOIN. Каждый вызов InnerJoin или LeftJoin может принимать в качестве параметра вложенный запрос на объединение данных из двух неймспейсов. Следующий за вызовом оператор On определяет условия объединения результата вложенного и родительского запросов:
type Book struct {
ID int `reindex:"id,,pk"`
Title string `reindex:"title"`
Price int `reindex:"price,tree"`
AuthorID int `reindex:"author_id"`
Authors []*Author `reindex:"authors,,joined"`
}
type Author struct {
ID int `reindex:"id,,pk"`
Name string `reindex:"name"`
Age int `reindex:"age,tree"`
LocationID int `reindex:"location_id"`
Locations []*Location `reindex:"locations,,joined"`
Books []*Book `reindex:"books,,joined"`
}
type Location struct {
ID int `reindex:"id,,pk"`
City string `reindex:"city"`
Code int `reindex:"code,tree"`
CountryID int `reindex:"country_id"`
Countries []*Country `reindex:"countries,,joined"`
}
type Country struct {
ID int `reindex:"id,,pk"`
Name string `reindex:"name"`
Active bool `reindex:"active"`
}
countries := db.Query("countries").
WhereBool("active", reindexer.EQ, true)
locations := db.Query("locations").
WhereInt("code", reindexer.GE, 1)
locations.InnerJoin(countries, "countries").
On("country_id", reindexer.EQ, "id")
authors := db.Query("authors").
WhereInt("age", reindexer.GE, 40)
authors.InnerJoin(locations, "locations").
On("location_id", reindexer.EQ, "id")
books := db.Query("books").
WhereInt("price", reindexer.GE, 10)
books.InnerJoin(authors, "authors").
On("author_id", reindexer.EQ, "id")
it := books.Exec()
defer it.Close()
for it.Next() {
book := it.Object().(*Book)
for _, author := range book.Authors {
for _, location := range author.Locations {
for _, country := range location.Countries {
fmt.Printf("%s, %s в %s, %s\n", book.Title, author.Name, location.City, country.Name)
}
}
}
}
countries_indexes = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
"index_type": "hash",
"is_pk": True,
},
{
"name": "active",
"json_paths": ["active"],
"field_type": "bool",
"index_type": "-",
},
]
for idx in countries_indexes:
db.index_add("countries", idx)
locations_indexes = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
"index_type": "hash",
"is_pk": True,
},
{
"name": "code",
"json_paths": ["code"],
"field_type": "int",
"index_type": "tree",
},
{
"name": "country_id",
"json_paths": ["country_id"],
"field_type": "int",
"index_type": "hash",
},
]
for idx in locations_indexes:
db.index_add("locations", idx)
authors_indexes = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
"index_type": "hash",
"is_pk": True,
},
{
"name": "age",
"json_paths": ["age"],
"field_type": "int",
"index_type": "tree",
},
{
"name": "location_id",
"json_paths": ["location_id"],
"field_type": "int",
"index_type": "hash",
},
]
for idx in authors_indexes:
db.index_add("authors", idx)
books_indexes = [
{
"name": "id",
"json_paths": ["id"],
"field_type": "int",
"index_type": "hash",
"is_pk": True,
},
{
"name": "price",
"json_paths": ["price"],
"field_type": "int",
"index_type": "tree",
},
{
"name": "author_id",
"json_paths": ["author_id"],
"field_type": "int",
"index_type": "hash",
},
]
for idx in books_indexes:
db.index_add("books", idx)
query_countries = db.new_query("countries").where(
"active", CondType.CondEq, True
)
query_locations = db.new_query("locations").where("code", CondType.CondGe, 1)
query_locations.inner_join(query_countries, "countries").on(
"country_id", CondType.CondEq, "id"
)
query_authors = db.new_query("authors").where("age", CondType.CondGe, 40)
query_authors.inner_join(query_locations, "locations").on(
"location_id", CondType.CondEq, "id"
)
query_books = db.new_query("books").where("price", CondType.CondGe, 10)
query_books.inner_join(query_authors, "authors").on(
"author_id", CondType.CondEq, "id"
)
result = query_books.execute()
for item in result:
print(item)
import ru.rt.restream.reindexer.annotations.Transient;
public static class Book {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "title")
private String title;
@Reindex(name = "price", type = IndexType.TREE)
private int price;
@Reindex(name = "author_id")
private int authorId;
@Transient
private List<Author> authors;
public Book() {
}
public Book(int id, String title, int price, int authorId, List<Author> authors) {
this.id = id;
this.title = title;
this.price = price;
this.authorId = authorId;
this.authors = authors;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
public int getAuthorId() {
return authorId;
}
public void setAuthorId(int authorId) {
this.authorId = authorId;
}
public List<Author> getAuthors() {
return authors;
}
public void setAuthors(List<Author> authors) {
this.authors = authors;
}
@Override
public String toString() {
return "Book{" +
"id=" + id +
", title='" + title + '\'' +
", price=" + price +
", authorId=" + authorId +
'}';
}
}
public static class Author {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "name")
private String name;
@Reindex(name = "age", type = IndexType.TREE)
private int age;
@Reindex(name = "location_id")
private int locationId;
@Transient
private List<Location> locations;
@Transient
private List<Book> books;
public Author() {
}
public Author(int id, String name, int age, int locationId, List<Location> locations) {
this.id = id;
this.name = name;
this.age = age;
this.locationId = locationId;
this.locations = locations;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public int getLocationId() {
return locationId;
}
public void setLocationId(int locationId) {
this.locationId = locationId;
}
public List<Location> getLocations() {
return locations;
}
public void setLocations(List<Location> locations) {
this.locations = locations;
}
public List<Book> getBooks() {
return books;
}
public void setBooks(List<Book> books) {
this.books = books;
}
@Override
public String toString() {
return "Author{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
", locationId=" + locationId +
'}';
}
}
public static class Location {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "city")
private String city;
@Reindex(name = "code", type = IndexType.TREE)
private int code;
@Reindex(name = "country_id")
private int countryId;
@Transient
private List<Country> countries;
public Location() {
}
public Location(int id, String city, int code, int countryId, List<Country> countries) {
this.id = id;
this.city = city;
this.code = code;
this.countryId = countryId;
this.countries = countries;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public int getCountryId() {
return countryId;
}
public void setCountryId(int countryId) {
this.countryId = countryId;
}
public List<Country> getCountries() {
return countries;
}
public void setCountries(List<Country> countries) {
this.countries = countries;
}
@Override
public String toString() {
return "Location{" +
"id=" + id +
", city='" + city + '\'' +
", code=" + code +
", countryId=" + countryId +
'}';
}
}
public static class Country {
@Reindex(name = "id", isPrimaryKey = true)
private int id;
@Reindex(name = "name")
private String name;
@Reindex(name = "active")
private boolean active;
public Country() {
}
public Country(int id, String name, boolean active) {
this.id = id;
this.name = name;
this.active = active;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
@Override
public String toString() {
return "Country{" +
"id=" + id +
", name='" + name + '\'' +
", active=" + active +
'}';
}
}
Query<Country> countryQuery = db.query("countries", Country.class)
.where("active", Condition.EQ, true);
Query<Location> locationQuery = db.query("locations", Location.class)
.where("code", Condition.GE, 1)
.innerJoin(countryQuery.on("country_id", Condition.EQ, "id"), "countries");
Query<Author> authorQuery = db.query("authors", Author.class)
.where("age", Condition.GE, 40)
.innerJoin(locationQuery.on("location_id", Condition.EQ, "id"), "locations");
Query<Book> booksQuery = db.query("books", Book.class)
.where("price", Condition.GE, 10)
.innerJoin(authorQuery.on("author_id", Condition.EQ, "id"), "authors");
ResultIterator<Book> books = booksQuery.execute();
while (books.hasNext()) {
Book book = books.next();
for (Author author : book.getAuthors()) {
for (Location location : author.getLocations()) {
for (Country country : location.getCountries()) {
System.out.printf("%s, %s в %s, %s\n",
book.getTitle(), author.getName(), location.getCity(), country.getName());
}
}
}
}
books.close();
В SQL-запросах вложенные JOIN-запросы помещают в скобки:
SELECT * FROM books
WHERE price >= 10
INNER JOIN (
SELECT * FROM authors
WHERE age >= 40
INNER JOIN (
SELECT * FROM locations
WHERE code >= 1
INNER JOIN (
SELECT * FROM countries
WHERE active = true
) ON locations.country_id = countries.id
) ON authors.location_id = locations.id
) ON books.author_id = authors.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "books",
"type": "select",
"filters": [
{
"field": "price",
"cond": "GE",
"value": [10]
},
{
"op": "AND",
"join_query": {
"namespace": "authors",
"type": "INNER",
"filters": [
{
"field": "age",
"cond": "GE",
"value": [40]
},
{
"op": "AND",
"join_query": {
"namespace": "locations",
"type": "INNER",
"filters": [
{
"field": "code",
"cond": "GE",
"value": [1]
},
{
"op": "AND",
"join_query": {
"namespace": "countries",
"type": "INNER",
"filters": [
{
"field": "active",
"cond": "EQ",
"value": [true]
}
],
"on": [
{
"left_field": "country_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
],
"on": [
{
"left_field": "location_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
],
"on": [
{
"left_field": "author_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}'
Данные присоединенного неймспейса укладываются в поле основного элемента с именем joined_<NS_NAME>, где <NS_NAME> — это имя целевого неймспейса. Такие поля представляют собой массивы объектов. Например, элемент из результатов запроса выше:
{
"price": 25,
"id": 1,
"title": "And Then There Were None",
"authorId": 10,
"joined_authors": [
{
"locationId": 20,
"name": "Agatha Christie",
"id": 10,
"age": 42,
"joined_locations": [
{
"code": 8,
"city": "London",
"id": 20,
"countryId": 30,
"joined_countries": [
{
"name": "United Kingdom",
"active": true,
"id": 30
}
]
}
]
}
]
}
JOIN-запросы могут комбинироваться с оператором MERGE. Если в основном запросе и в MERGE-подзапросе необходимы запросы с JOIN, то они помещаются в каждый из них по отдельности:
q1 := db.Query("books").
WhereInt("price", reindexer.GE, 10).
InnerJoin(db.Query("authors"), "authors").
On("author_id", reindexer.EQ, "id")
q2 := db.Query("books").
WhereInt("price", reindexer.LT, 10).
InnerJoin(db.Query("authors"), "authors").
On("author_id", reindexer.EQ, "id")
q1.Merge(q2)
q1 = db.new_query("books").where("price", CondType.CondGe, 10)
q1.inner_join(db.new_query("authors"), "authors").on(
"author_id", CondType.CondEq, "id"
)
q2 = db.new_query("books").where("price", CondType.CondLt, 10)
q2.inner_join(db.new_query("authors"), "authors").on(
"author_id", CondType.CondEq, "id"
)
q1.merge(q2)
Query<Author> mergeAuthors1 = db.query("authors", Author.class);
Query<Book> q1 = db.query("books", Book.class)
.where("price", Condition.GE, 10)
.innerJoin(mergeAuthors1.on("author_id", Condition.EQ, "id"), "authors");
Query<Author> mergeAuthors2 = db.query("authors", Author.class);
Query<Book> q2 = db.query("books", Book.class)
.where("price", Condition.LT, 10)
.innerJoin(mergeAuthors2.on("author_id", Condition.EQ, "id"), "authors");
Query<Book> merged = q1.merge(q2);
SELECT * FROM books
WHERE price >= 10
INNER JOIN authors ON books.author_id = authors.id
MERGE (
SELECT * FROM books
WHERE price < 10
INNER JOIN authors ON books.author_id = authors.id
)
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "books",
"type": "select",
"filters": [
{
"field": "price",
"cond": "GE",
"value": [10]
},
{
"op": "AND",
"join_query": {
"namespace": "authors",
"type": "INNER",
"on": [
{
"left_field": "author_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
],
"merge_queries": [
{
"namespace": "books",
"type": "select",
"filters": [
{
"field": "price",
"cond": "LT",
"value": [10]
},
{
"op": "AND",
"join_query": {
"namespace": "authors",
"type": "INNER",
"on": [
{
"left_field": "author_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}
]
}'
Цепочка JOIN-вызовов создает несколько joined-полей на одном уровне корневого неймспейса:
authors := db.Query("authors")
authors.InnerJoin(db.Query("locations"), "locations").
On("location_id", reindexer.EQ, "id").
InnerJoin(db.Query("books"), "books").
On("id", reindexer.EQ, "author_id")
query = (
db.new_query("authors")
.inner_join(db.new_query("locations"), "locations")
.on("location_id", CondType.CondEq, "id")
.inner_join(db.new_query("books"), "books")
.on("id", CondType.CondEq, "author_id")
)
Query<Location> flatLocations = db.query("locations", Location.class);
Query<Book> flatBooks = db.query("books", Book.class);
Query<Author> flatQuery = db.query("authors", Author.class)
.innerJoin(flatLocations.on("location_id", Condition.EQ, "id"), "locations")
.innerJoin(flatBooks.on("id", Condition.EQ, "author_id"), "books");
Выдача с результатом имеет плоскую структуру: joined_locations и joined_books находятся на одном уровне внутри authors. Элемент из результатов запроса выше имеет следующую структуру:
{
"locationId": 20,
"name": "Agatha Christie",
"id": 10,
"age": 42,
"joined_locations": [
{
"code": 8,
"city": "London",
"id": 20,
"countryId": 30
}
],
"joined_books": [
{
"price": 25,
"id": 1,
"title": "And Then There Were None",
"authorId": 10
},
{
"price": 5,
"id": 2,
"title": "Murder on the Orient Express",
"authorId": 10
}
]
}
Если в одном запросе несколько разных JOIN ссылаются на один и тот же неймспейс, к имени поля добавляется номер в порядке их следования (joined_1_authors и joined_2_authors):
{
"price": 25,
"id": 1,
"title": "And Then There Were None",
"authorId": 10,
"joined_1_authors": [
{
"locationId": 20,
"name": "Agatha Christie",
"id": 10,
"age": 42
}
],
"joined_2_authors": [
{
"locationId": 20,
"name": "Agatha Christie",
"id": 10,
"age": 42
}
]
}
Чтобы JOIN-запрос оказался вложенным, он должен быть сконструирован отдельно и передан в качестве параметра в родительский запрос:
authors := db.Query("authors")
locations := db.Query("locations")
locations.InnerJoin(db.Query("countries").WhereBool("active", reindexer.EQ, true), "countries").
On("country_id", reindexer.EQ, "id")
authors.InnerJoin(locations, "locations").
On("location_id", reindexer.EQ, "id")
authors = db.new_query("authors")
locations = db.new_query("locations")
locations.inner_join(
db.new_query("countries").where("active", CondType.CondEq, True), "countries"
).on("country_id", CondType.CondEq, "id")
authors.inner_join(locations, "locations").on(
"location_id", CondType.CondEq, "id"
)
Query<Country> countryQuery = db.query("countries", Country.class)
.where("active", Condition.EQ, true);
Query<Location> locationQuery = db.query("locations", Location.class)
.innerJoin(countryQuery.on("country_id", Condition.EQ, "id"), "countries");
Query<Author> authorQuery = db.query("authors", Author.class)
.innerJoin(locationQuery.on("location_id", Condition.EQ, "id"), "locations");
В SQL и HTTP вложенность выражается структурой запроса — вложенный запрос помещается в скобки (INNER JOIN (...)), либо во вложенный join_query внутри filters, как в первом примере.
InnerJoin и LeftJoin сохраняют функциональность независимо от глубины вложенности. В примере ниже в выдаче возвращаются все подходящие книги (books), так как для них используется LeftJoin. При этом список авторов (authors) внутри этих книг отфильтруется по их собственному вложенному условию InnerJoin с локациями (locations):
books := db.Query("books")
authors := db.Query("authors")
authors.InnerJoin(db.Query("locations").WhereInt("code", reindexer.GE, 100), "locations").
On("location_id", reindexer.EQ, "id")
books.LeftJoin(authors, "authors").
On("author_id", reindexer.EQ, "id")
it := books.Exec()
defer it.Close()
if err := it.Error(); err != nil {
panic(err)
}
for it.Next() {
book := it.Object().(*Book)
authorsSlice, err := it.JoinedObjects("authors")
if err != nil {
panic(err)
}
for i, rawAuthor := range authorsSlice {
author := rawAuthor.(*Author)
fmt.Println(book.Authors[i].ID, author.Locations)
}
}
Для сохранения результатов в структуре нужно сделать поле публичным и добавить тег reindex:"<field_name>,,joined", иначе библиотека проигнорирует эти данные.
Метод JoinedObjects(<field_name>) возвращает связанные объекты верхнего уровня, которые уже содержат все вложенные данные.
books = db.new_query("books")
authors = db.new_query("authors")
authors.inner_join(
db.new_query("locations").where("code", CondType.CondGe, 100), "locations"
).on("location_id", CondType.CondEq, "id")
books.left_join(authors, "authors").on(
"author_id", CondType.CondEq, "id"
)
result = books.execute()
for item in result:
print(item)
Query<Location> locationQuery = db.query("locations", Location.class)
.where("code", Condition.GE, 100);
Query<Author> authorQuery = db.query("authors", Author.class)
.innerJoin(locationQuery.on("location_id", Condition.EQ, "id"), "locations");
Query<Book> booksQuery = db.query("books", Book.class)
.leftJoin(authorQuery.on("author_id", Condition.EQ, "id"), "authors");
ResultIterator<Book> books = booksQuery.execute();
while (books.hasNext()) {
Book book = books.next();
System.out.printf("%s, авторов: %d\n", book.getTitle(),
book.getAuthors() == null ? 0 : book.getAuthors().size());
}
books.close();
SELECT * FROM books
LEFT JOIN (
SELECT * FROM authors
INNER JOIN (
SELECT * FROM locations
WHERE code >= 100
) ON authors.location_id = locations.id
) ON books.author_id = authors.id
curl --location --request POST 'http://127.0.0.1:9088/api/v1/db/testdb/query' \
--header 'Content-Type: application/json' \
--data-raw '{
"namespace": "books",
"type": "select",
"filters": [
{
"op": "AND",
"join_query": {
"namespace": "authors",
"type": "LEFT",
"filters": [
{
"op": "AND",
"join_query": {
"namespace": "locations",
"type": "INNER",
"filters": [
{
"field": "code",
"cond": "GE",
"value": [100]
}
],
"on": [
{
"left_field": "location_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
],
"on": [
{
"left_field": "author_id",
"right_field": "id",
"cond": "EQ"
}
]
}
}
]
}'
Вложенные JOIN-запросы:
- работают в любых
SELECT-запросах, включая запросы с операторомMERGE; - поддерживают логические операции
AND,ORиNOT; - не поддерживают сортировку по полям вложенных
JOIN-запросов.
Операции UPDATE и DELETE:
- поддерживают вложенные
INNER JOINтолько в качестве фильтров; - не возвращают объединенные данные неймспейсов.
Опции запроса (Explain, Debug, Strict и т. п.) достаточно передать в корневой запрос: они автоматически применяются ко всем подзапросам на любой глубине. Это относится к Joined-подзапросам, частям MERGE и обычным подзапросам.
Примечание
В шардированных неймспейсах каждый подзапрос должен либо обращаться к нешардированному неймспейсу, либо явно задавать условие по ключу шардирования. Все подзапросы одного запроса обязаны выбирать один и тот же шард.
Запрос с условиями в 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
]
}
]
}
]
}'
С информацией про объединение результатов при выборке из нескольких неймспейсов при полнотекстовом поиске можно ознакомиться в разделе «Объединение результатов полнотекстового поиска».