Project Query Results



Projections overview

What are projections:

  • A projection refers to the transformation of query results into a customized structure,
    modifying the shape of the data returned by the server.

  • Instead of retrieving the full document from the server and then picking relevant data from it on the client,
    you can request a subset of the data, specifying the document fields you want to get from the server.

  • The query can load related documents and have their data merged into the projection results.

  • Objects and arrays can be projected, fields can be renamed, and any calculations can be made within the projection.

  • Content from inner objects and arrays can be projected.
    An alias name can be given to the projected fields, and any calculations can be made within the projection.

When to use projections:

  • Projections allow you to tailor the query results specifically to your needs.
    Getting specific details to display can be useful when presenting data to users or populating user interfaces.
    Projection queries are also useful with subscriptions since all transformation work is done on the server side without having to send a lot of data over the wire.

  • Returning partial document data from the server reduces network traffic,
    as clients receive only relevant data required for a particular task, enhancing overall performance.

  • Savings can be significant if you need to show just a bit of information from a large document. For example:
    the size of the result when querying for all "Orders" documents where "Company" is "companies/65-A" is 19KB.
    Performing the same query and projecting only the "Employee" and "OrderedAt" fields results in only 5KB.

  • However, when you need to actively work with the complete set of data on the client side,
    then do use a query without a projection to retrieve the full document from the server.

Projections are not tracked by the session:

  • On the client side, the resulting projected entities returned by the query are Not tracked by the Session.

  • Any modification made to a projection entity will not modify the corresponding document on the server when SaveChanges is called.

Projections are the final stage in the query pipeline:

  • Projections are applied as the last stage in the query pipeline,
    after the query has been processed, filtered, sorted, and paged.

  • This means that the projection does Not apply to all the documents in the collection,
    but only to the documents matching the query predicate.

  • Within the projection you can only filter what data will be returned from the matching documents,
    but you cannot filter which documents will be returned. That has already been determined earlier in the query pipeline.

  • Only a single projection request can be made per query.

The cost of projections:

  • Queries in RavenDB do not allow any computation to occur during the query phase.
    However, you can perform any calculations inside the projection.

  • But while calculations within a projection are allowed, having a very complex logic can impact query performance.
    So RavenDB limits the total time it will spend processing a query and its projections.
    Exceeding this time limit will fail the query. This is configurable, see the following configuration keys:

Projection Methods

select, selectFields

Projections are commonly performed in PHP using the select and selectFields methods.
You can specify what fields from a document you want to retrieve and even provide complex expression.

Example - Projecting Individual Fields of the Document:

// request name, city and country for all entities from 'Companies' collection
$queryData = new QueryData(
    [ "Name", "Address.city", "Address.country"],
    [ "Name", "City", "Country"]
);

/** @var array<NameCityAndCountry> $results */
$results = $session
    ->query(Company::class)
    ->selectFields(NameCityAndCountry::class, $queryData)
    ->toList();
from Companies
select Name, Address.City as City, Address.Country as Country

Example - Projecting Arrays and Objects:

$queryData = new QueryData(
    [ "ShipTo", "Lines[].ProductName" ],
    [ "ShipTo", "Products" ]
);

/** @var array<ShipToAndProducts> $results */
$results = $session->query(Order::class)
    ->selectFields(ShipToAndProducts::class, $queryData)
    ->toList();
from Orders
select ShipTo, Lines[].ProductName as Products

Example - Projection with expression:

/** @var array<FullName> $results */
$results = $session->advanced()->rawQuery(FullName::class, "from Employees as e " .
    "select {" .
    "    FullName : e.FirstName + \" \" + e.LastName " .
    "}")
    ->toList();
from "Employees" as e
select {
    FullName: e.FirstName + " " + e.LastName
}

Example - Projection with Calculation:

/** @var array<Total> $results */
$results = $session->advanced()->rawQuery(Total::class, "from Orders as o " .
    "select { " .
    "    Total : o.Lines.reduce( " .
    "        (acc , l) => acc += l.PricePerUnit * l.Quantity, 0) " .
    "}")
    ->toList();
from "Orders" as o
select {
    Total : o.Lines.reduce(
        (acc , l) => acc += l.PricePerUnit * l.Quantity, 0)
}

Example - Projection Using a Loaded Document:

/** @var array<OrderProjection> $results */
$results = $session->advanced()->rawQuery(OrderProjection::class, "from Orders as o " .
    "load o.Company as c " .
    "select { " .
    "    CompanyName: c.Name," .
    "    ShippedAt: o.ShippedAt" .
    "}")
->toList();
from "Orders" as o
load o.Company as c
select {
	CompanyName: c.Name,
	ShippedAt: o.ShippedAt
}

Example - Projection with Dates:

/** @var array<EmployeeProjection> $results */
$results = $session->advanced()->rawQuery(EmployeeProjection::class, "from Employees as e " .
    "select { " .
    "    DayOfBirth : new Date(Date.parse(e.Birthday)).getDate(), " .
    "    MonthOfBirth : new Date(Date.parse(e.Birthday)).getMonth() + 1, " .
    "    Age : new Date().getFullYear() - new Date(Date.parse(e.Birthday)).getFullYear() " .
    "}")
->toList();
from "Employees" as e 
select { 
    DayOfBirth : new Date(Date.parse(e.Birthday)).getDate(), 
    MonthOfBirth : new Date(Date.parse(e.Birthday)).getMonth() + 1,
    Age : new Date().getFullYear() - new Date(Date.parse(e.Birthday)).getFullYear() 
}

Example - Projection with Raw JavaScript Code:

/** @var array<EmployeeProjection> $results */
$results = $session->advanced()->rawQuery(EmployeeProjection::class, "from Employees as e " .
    "select { " .
    "    Date : new Date(Date.parse(e.Birthday)), " .
    "    Name : e.FirstName.substr(0,3) " .
    "}")
->toList();
from "Employees" as e 
select {
    Date : new Date(Date.parse(e.Birthday)), 
    Name : e.FirstName.substr(0,3)
}

Example - Projection with Metadata:

/** @var array<Employee> $results */
$results = $session->advanced()->rawQuery(Employee::class, "from Employees as e " .
    "select {" .
    "     Name : e.FirstName, " .
    "     Metadata : getMetadata(e)" .
    "}")
    ->toList();
from "Employees" as e 
select {
     Name : e.FirstName, 
     Metadata : getMetadata(e)
}

ofType

ofType is a client-side projection that maps results that the server returns into a given type.
This may become useful when querying an index that contains fields that are not available in the mapped type.

Example

// query index 'Products_BySupplierName'
// return documents from collection 'Products' that have a supplier 'Norske Meierier'
// project them to 'Products'
/** @var array<Product> $results */
$results = $session->query(Products_BySupplierName_Result::class, Products_BySupplierName::class)
    ->whereEquals("Name", "Norske Meierier")
    ->ofType(Product::class)
    ->toList();
class Products_BySupplierName extends AbstractIndexCreationTask
{
}

class Products_BySupplierName_Result
{
}

Single projection per query

As of RavenDB v6.0, only a single projection request can be made per query. Attempting multiple projection executions in the same query will result in an exception.