Перейти к содержанию

Запрос связанных данных

Фильтр

Фильтры реализуют интерфейс:

import "entgo.io/ent/dialect/sql"

type Filter interface {
    HasQueries() bool
    QueryAsync(ctx context.Context) ([]func(selector *sql.Selector), error)
}

Метод HasQueries() должен вернуть true, если структура содержит запроса для фильтрации (в ряде случаев их может не быть).

Метод QueryAsync() формирует набор предикатов, то есть методов, которые дополняют запрос WHERE. Нейминг async в данном случае отражает факт того, что фильтр может состоять из нескольких критериев, каждый из которых будет формироваться одновременно с остальными.

Фильтром может являться любая структура, которая по пришедшим извне значениям фильтрации сможет вернуть корректные предикаты.

Например, значения для фильтра транспортных операций описываются так:

message TransportOperationsFilter {
  optional string number = 1;
  optional string objectNumber = 2;
  repeated string vehicleKindUids = 3;
  repeated string driverUids = 4;
  repeated string responsibleUserUids = 5;
  repeated TransportOperationsFilterDate dates = 6;
  repeated string statusUids = 7;
}

message TransportOperationsFilterDate {
  optional google.protobuf.Timestamp start = 1;
  optional google.protobuf.Timestamp end = 2;
}

В данном случае, фильтрация может происходить по номеру транспортной операции, номеру объекта перевозки (контейнера), идентификаторам типов транспортных средств, идентификаторам ответственных менеджеров, наборам дат и идентификаторам актуальных статусов транспортной операции.

В случае, если все значения будут указаны, то QueryAsync() подготовит нужные предикаты параллельно выполняя запросы.

Реализация

Несмотря на кажущуюся простоту, реализация фильтров имеет ряд сложностей.

Например, для транспортной операции доступен фильтр по идентификаторам типов транспортных средств. Это осложнено тем, что:

  • Тип ТС (транспортного средства) является свойством объекта транспортировки, то есть связанной сущности для транспортных операций;
  • Сами типы транспортных средств находятся в JSONB-поле transport_operations.vehicles.

Из-за чего метод формирования предикатов в зависимости от идентификаторов типа ТС содержит запрос связанных сущностей и нетривиальное дополнения для SQL-запроса по JSONB-полю:

func (f *Filter) AddByVehicleKindUIDs(uids []string) error {
    query := func(ctx context.Context) error {
        fields := []string{collector.FieldCargoUID}
        items, err := f.client(ctx).TransportationObject.Query().
            Select(fields...).
            Where(transportationobject.VehicleKindUIDIn(uids...)).
            All(ctx)
        if err != nil {
            return err
        }
        cargoUIDs := []any{}
        for _, item := range items {
            cargoUIDs = append(cargoUIDs, item.CargoUID)
        }
  
        predicate := func(selector *entSQl.Selector) {
            from := `jsonb_array_elements("vehicles") with ordinality "vehicles_array"("item", "position")`
            if !selectorHasExpr(selector, from) {
                selector.AppendFromExpr(entSQl.Expr(from))
            }
            selector.Where(entSQl.In(`"vehicles_array"."item"->>'transportation_object_uid'`, cargoUIDs...))
        }
  
        f.mutex.Lock()
        f.predicates = append(f.predicates, predicate)
        f.mutex.Unlock()
        return nil
    }
  
    f.queries = append(f.queries, query)
  
    return nil
}

Такой метод порождает SQL-запрос вида:

SELECT /* ... */
FROM
    "transport_operations",
    jsonb_array_elements("vehicles") 
        with ordinality "vehicles_array"("item", "position")
WHERE /* ... AND */ 
    "vehicles_array"."item"->>'transportation_object_uid' 
        IN ($2, $3, $4, $5, $6, $7, $8, $9, $10)
ORDER BY "transport_operations"."updated_at" DESC 
LIMIT 10

Примеры реализации фильтров можно посмотреть в файлах internal/data/repository/*/filter.go