see on GitHub

Handling document relationships

One of the design principles that RavenDB adheres to is the idea that documents are independent, meaning all data required to process a document is stored within the document itself. However, this doesn't mean there should not be relations between objects.

There are valid scenarios where we need to define relationships between objects. By doing so, we expose ourselves to one major problem: whenever we load the containing entity, we are going to need to load data from the referenced entities too (unless we are not interested in them). While the alternative of storing the whole entity in every object graph it is referenced in seems cheaper at first, this proves to be quite costly in terms of database resources and network traffic.

RavenDB offers three elegant approaches to solve this problem. Each scenario will need to use one or more of them. When applied correctly, they can drastically improve performance, reduce network bandwidth and speedup development.


Denormalization

The easiest solution is to denormalize the data within the containing entity, forcing it to contain the actual value of the referenced entity in addition (or instead) of the foreign key.

Take this JSON document for example:

// Order document with id: orders/1234
{ 
    "Customer": {
        "Name": "Itamar",
        "Id": "customers/2345"
    },
    Items: [
        { 
        "Product": { 
            "Id": "products/1234",
            "Name": "Milk",
            "Cost": 2.3
            },
        "Quantity": 3
        }
    ]
}

As you can see, the Order document now contains denormalized data from both the Customer and the Product documents, which are saved elsewhere in full. Note we won’t have copied all the customer properties into the order; instead we just clone the ones that we care about when displaying or processing an order. This approach is called denormalized reference.

The denormalization approach avoids many cross document lookups and results in only the necessary data being transmitted over the network, but it makes other scenarios more difficult. For example, consider the following entity structure as our start point:

@QueryEntity
public static class Order {
  private String customerId;
  private UUID[] supplierIds;
  private Referral referral;
  private List<LineItem> lineItems;
  private double totalPrice;

  public String getCustomerId() {
    return customerId;
  }
  public void setCustomerId(String customerId) {
    this.customerId = customerId;
  }
  public UUID[] getSupplierIds() {
    return supplierIds;
  }
  public void setSupplierIds(UUID[] supplierIds) {
    this.supplierIds = supplierIds;
  }
  public Referral getReferral() {
    return referral;
  }
  public void setReferral(Referral referral) {
    this.referral = referral;
  }
  public List<LineItem> getLineItems() {
    return lineItems;
  }
  public void setLineItems(List<LineItem> lineItems) {
    this.lineItems = lineItems;
  }
  public double getTotalPrice() {
    return totalPrice;
  }
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }
}

public class Customer {
  private String id;
  private String name;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
}

If we know that whenever we load an Order from the database we will need to know the customer's name and address, we could decide to create a denormalized Order.Customer field, and store those details in the directly in the Order object. Obviously, the password and other irrelevant details will not be denormalized:

@QueryEntity
public class DenormalizedCustomer {
  private String id;
  private String name;
  private String address;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public String getAddress() {
    return address;
  }
  public void setAddress(String address) {
    this.address = address;
  }
}

There wouldn’t be a direct reference between the Order and the Customer. Instead, Order holds a DenormalizedCustomer, which contains the interesting bits from Customer that we need whenever we process Order objects.

But, what happens when the user's address is changed? We will have to perform an aggregate operation to update all orders this customer has made. And what if the customer has a lot of orders or changes their address frequently? Keeping these details in sync could become very demanding on the server. What if another process that works with orders needs a different set of customer properties? The DenormalizedCustomer will need to be expanded, possibly to the point that the majority of the customer record is cloned.

Tip

Denormalization is a viable solution for rarely changing data, or for data that must remain the same despite the underlying referenced data changing over time.


Includes

Includes feature addresses the limitations of denormalization. Instead of one object containing copies of the properties from another object, it is only necessary to hold a reference to the second object. Then server can be instructed to pre-load the referenced document at the same time that the root object is retrieved. We can do this using:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
Order order = session
  .include(o.customerId)
  .load(Order.class, "orders/1234");

// this will not require querying the server!
Customer customer = session.load(Customer.class, order.getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" } , new String[] { "CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

Above we are asking RavenDB to retrieve the Order "orders/1234" and at the same time "include" the Customer referenced by the Order.CustomerId property. The second call to load() is resolved completely client side (i.e. without a second request to the RavenDB server) because the relevant Customer object has already been retrieved (this is the full Customer object not a denormalized version).

There is also a possibility to load multiple documents:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
Order[] orders = session
  .include(o.customerId)
  .load(Order.class, Arrays.asList("orders/1234", "orders/4321"));

for (Order order : orders) {
  // this will not require querying the server!
  Customer customer = session.load(Customer.class, order.getCustomerId());
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234", "orders/4321" }, new String[] { "CustomerId" });

List<RavenJObject> orders = result.getResults();
List<RavenJObject> customers = result.getIncludes();

You can also use Includes with queries:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
List<Order> orders = session
  .query(Order.class, Orders_ByTotalPrice.class)
  .customize(new DocumentQueryCustomizationFactory().include(o.customerId))
  .where(o.totalPrice.gt(100))
  .toList();

for (Order order : orders) {
  // this will not require querying the server!
  Customer customer = session.load(Customer.class, order.getCustomerId());
}
QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
List<Order> orders = session
  .advanced()
  .documentQuery(Order.class, Orders_ByTotalPrice.class)
  .include(o.customerId)
  .whereGreaterThan(o.totalPrice, 100.0)
  .toList();

for (Order order : orders) {
  // this will not require querying the server!
  Customer customer = session
    .load(Customer.class, order.getCustomerId());
}
IndexQuery query = new IndexQuery("TotalPrice_Range:{Ix100 TO NULL}");
QueryResult result = store
  .getDatabaseCommands()
  .query("Orders/ByTotalPrice", query, new String[] { "CustomerId" });

List<RavenJObject> orders = result.getResults();
Collection<RavenJObject> customers = result.getIncludes();
public static class Orders_ByTotalPrice extends AbstractIndexCreationTask {
  public Orders_ByTotalPrice() {
    map =
     " from order in docs.Orders " +
     " select new                " +
     " {                         " +
     "     order.TotalPrice      " +
     " }; ";
  }
}

Under the hood, this works because RavenDB has two channels through which it can return information in response to a load request. The first is the Results channel, through which the root object retrieved by the load() method call is returned. The second is the Includes channel, through which any included documents are sent back to the client. Client side, those included documents are not returned from the load() method call, but they are added to the session unit of work, and subsequent requests to load them are served directly from the session cache, without requiring any additional queries to the server.

One to many includes

Include can be used with a many to one relationship. In the above classes, an Order has a property SupplierIds which contains an array of references to Supplier documents. The following code will cause the suppliers to be pre-loaded:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
Order order = session
  .include(o.supplierIds)
  .load(Order.class, "orders/1234");

for (UUID supplierId : order.getSupplierIds()) {
  // this will not require querying the server!
  Supplier supplier = session.load(Supplier.class, supplierId);
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" }, new String[] { "SupplierIds" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

Again, the calls to load() within the for loop will not require a call to the server as the Supplier objects will already be loaded into the session cache.

Multi-loads are also possible:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
Order[] orders = session
  .include(o.supplierIds)
  .load(Order.class, "orders/1234", "orders/4321");

for (Order order : orders) {
  for (UUID supplierId : order.getSupplierIds()) {
    // this will not require querying the server!
    Supplier supplier = session.load(Supplier.class, supplierId);
  }
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234", "orders/4321" }, new String[] { "SupplierIds" });

List<RavenJObject> orders = result.getResults();
List<RavenJObject> customers = result.getIncludes();

Secondary level includes

An Include does not need to work only on the value of a top level property within a document. It can be used to load a value from a secondary level. In the classes above, the Order contains a Referral property which is of the type:

@QueryEntity
public class Referral {
  private String customerId;
  private double commissionPercentage;

  public String getCustomerId() {
    return customerId;
  }
  public void setCustomerId(String customerId) {
    this.customerId = customerId;
  }
  public double getCommissionPercentage() {
    return commissionPercentage;
  }
  public void setCommissionPercentage(double commissionPercentage) {
    this.commissionPercentage = commissionPercentage;
  }
}

This class contains an identifier for a Customer. The following code will include the document referenced by that secondary level identifier:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
QHandlingDocumentRelationships_Referral r = QHandlingDocumentRelationships_Referral.referral;
Order order = session
  .include(o.referral.customerId)
  .load(Order.class, "orders/1234");

// this will not require querying the server!
Customer customer = session.load(Customer.class, order.getReferral().getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" }, new String[] { "Referral.CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

Alternative way is to provide string based path:

Order order = session
  .include("Referral.CustomerId")
  .load(Order.class, "orders/1234");

// this will not require querying the server!
Customer customer = session.load(Customer.class, order.getReferral().getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" }, new String[] { "Referral.CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

This secondary level include will also work with collections. The Order.LineItems property holds a collection of LineItem objects which each contain a reference to a Product:

@QueryEntity
public class LineItem {
  private UUID productId;
  private String name;
  private int quantity;

  public UUID getProductId() {
    return productId;
  }
  public void setProductId(UUID productId) {
    this.productId = productId;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public int getQuantity() {
    return quantity;
  }
  public void setQuantity(int quantity) {
    this.quantity = quantity;
  }
}

The Product documents can be included using this syntax:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
Order order = session
  .include(o.lineItems.select().productId)
  .load(Order.class, "orders/1234");

for (LineItem lineItem : order.getLineItems()) {
  // this will not require querying the server!
  Product product = session.load(Product.class, lineItem.getProductId());
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String [] { "orders/1234" }, new String[] { "LineItems.,ProductId" });

RavenJObject order = result.getResults().get(0);
RavenJObject product = result.getIncludes().get(0);

when you want to load multiple documents.

The select() within the Include tells RavenDB which property of secondary level objects to use as a reference.

select()

select() method is only available for List<?> fields. It isn't available for arrays.

Conventions

When using string-based includes like:

QHandlingDocumentRelationships_Order o = QHandlingDocumentRelationships_Order.order;
QHandlingDocumentRelationships_Referral r = QHandlingDocumentRelationships_Referral.referral;
Order order = session
  .include(o.referral.customerId)
  .load(Order.class, "orders/1234");

// this will not require querying the server!
Customer customer = session.load(Customer.class, order.getReferral().getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" }, new String[] { "Referral.CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

you must remember to follow certain rules that must apply to the provided string path:

  1. Dots are used to separate properties e.g. "Referral.CustomerId" in example above means that our Order contains property Referral and that property contains another property called CustomerId.
  2. Commas are used to indicate that property is a collection type e.g. List. So if our Order will have a list of LineItems and each LineItem will contain ProductId property then we can create string path as follows: "LineItems.,ProductId".
  3. Prefixes are used to indicate id prefix for non-string identifiers. e.g. if our CustomerId property in "Referral.CustomerId" path is an integer then we should add customers/ prefix so the final path would look like "Referral.CustomerId(customers/)" and for our collection example it would be "LineItems.,ProductId(products/)" if the ProductId would be a non-string type.

Note

Prefix is not required for string identifiers, because they contain it by default.

Learning string path rules may be useful when you will want to query database using HTTP API.

curl -X GET "http://localhost:8080/databases/Northwind/queries/?include=LineItems.,ProductId(products/)&id=orders/1"

ValueType identifiers

The above Include samples assume that the Id property being used to resolve a reference is a string and it contains the full identifier for the referenced document (e.g. the CustomerId property will contain a value such as "customers/5678"). Include can also work with Value Type identifiers. Given these entities:

@QueryEntity
public class Order2 {
  private int customerId;
  private UUID[] supplierIds;
  private Referral referral;
  private List<LineItem> lineItems;
  private double totalPrice;

  public int getCustomerId() {
    return customerId;
  }
  public void setCustomerId(int customerId) {
    this.customerId = customerId;
  }
  public UUID[] getSupplierIds() {
    return supplierIds;
  }
  public void setSupplierIds(UUID[] supplierIds) {
    this.supplierIds = supplierIds;
  }
  public Referral getReferral() {
    return referral;
  }
  public void setReferral(Referral referral) {
    this.referral = referral;
  }
  public List<LineItem> getLineItems() {
    return lineItems;
  }
  public void setLineItems(List<LineItem> lineItems) {
    this.lineItems = lineItems;
  }
  public double getTotalPrice() {
    return totalPrice;
  }
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }
}

public class Customer2 {
  private int id;
  private String name;

  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 class Referral2 {
  private int customerId;
  private double commissionPercentage;

  public int getCustomerId() {
    return customerId;
  }
  public void setCustomerId(int customerId) {
    this.customerId = customerId;
  }
  public double getCommissionPercentage() {
    return commissionPercentage;
  }
  public void setCommissionPercentage(double commissionPercentage) {
    this.commissionPercentage = commissionPercentage;
  }
}

The samples above can be re-written as follows:

QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
Order2 order = session
  .include(Customer2.class, o.customerId)
  .load(Order2.class, "order2s/1234");

// this will not require querying the server!
Customer2 customer = session.load(Customer2.class, order.getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "order2s/1234" }, new String[] { "CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
List<Order2> orders = session
  .query(Order2.class, Order2s_ByTotalPrice.class)
  .customize(new DocumentQueryCustomizationFactory().include(Customer2.class, o.customerId))
  .where(o.totalPrice.gt(100))
  .toList();

for (Order2 order : orders) {
  // this will not require querying the server!
  Customer2 customer = session.load(Customer2.class, order.getCustomerId());
}
QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
List<Order2> orders = session
  .advanced()
  .documentQuery(Order2.class, Order2s_ByTotalPrice.class)
  .include("CustomerId")
  .whereGreaterThan(o.totalPrice, 100.0)
  .toList();

for (Order2 order : orders) {
  // this will not require querying the server!
  Customer2 customer = session.load(Customer2.class, order.getCustomerId());
}
QueryResult result = store
  .getDatabaseCommands()
  .query("Order2s/ByTotalPrice",
    new IndexQuery("TotalPrice_Range:{Ix100 TO NULL}"),
    new String[] { "CustomerId" });

List<RavenJObject> orders = result.getResults();
Collection<RavenJObject> customers = result.getIncludes();
public class Order2s_ByTotalPrice extends AbstractIndexCreationTask {
  public Order2s_ByTotalPrice() {
    map =
     " from order in docs.Orders " +
     " select new                " +
     " {                         " +
     "     order.TotalPrice      " +
     " };";
  }
}

QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
Order2 order = session
  .include(Supplier.class, o.supplierIds)
  .load(Order2.class, "order2s/1234");

for (UUID supplierId : order.getSupplierIds()) {
  // this will not require querying the server!
  Supplier supplier = session.load(Supplier.class, supplierId);
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "order2s/1234" }, new String[] { "SupplierIds" });

RavenJObject order = result.getResults().get(0);
List<RavenJObject> suppliers = result.getIncludes();

QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
Order2 order = session
  .include(Customer2.class, o.referral.customerId)
  .load(Order2.class, "order2s/1234");

// this will not require querying the server!
Customer2 customer = session.load(Customer2.class, order.getReferral().getCustomerId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "order2s/1234" }, new String[] { "Referral.CustomerId" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

QHandlingDocumentRelationships_Order2 o = QHandlingDocumentRelationships_Order2.order2;
Order2 order = session
  .include(Product.class, o.lineItems.select().productId)
  .load(Order2.class, "orders/1234");

for (LineItem lineItem : order.getLineItems()) {
  // this will not require querying the server!
  Product product = session.load(Product.class, lineItem.getProductId());
}
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "order2s/1234" }, new String[] { "LineItems.,ProductId" });

RavenJObject order = result.getResults().get(0);
List<RavenJObject> products = result.getIncludes();

The class parameter to include specifies which document collection the reference is pointing to. RavenDB will combine the name of the collection with the value of the reference property to find the full identifier of the referenced document. For example, from the first example, if the value of the Order.CustomerId property is 56, client will include the document with an Id of customer2s/56 from the database. The session.load(Customer2.class, Number id) method will be passed the value 56 and will look for then load the document customer2s/56 from the session cache.


Dictionary includes

Dictionary keys and values can also be used when doing includes. Consider following scenario:

@QueryEntity
public static class Person {
  private String id;
  private String name;
  private Map<String, String> attributes;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public Map<String, String> getAttributes() {
    return attributes;
  }
  public void setAttributes(Map<String, String> attributes) {
    this.attributes = attributes;
  }
}

Map<String, String> attributes = new HashMap<String, String>();
Person child = new Person();
child.setId("people/1");
child.setName("John Doe");
attributes.put("Mother", "people/2");
attributes.put("Father", "people/3");
child.setAttributes(attributes);
session.store(child);

Person mother = new Person();
mother.setId("people/2");
mother.setName("Helen Doe");
mother.setAttributes(new HashMap<String, String>());
session.store(mother);

Person father = new Person();
father.setId("people/3");
father.setName("George Doe");
father.setAttributes(new HashMap<String, String>());

Now we want to include all documents that are under dictionary values:

QHandlingDocumentRelationships_Person p = QHandlingDocumentRelationships_Person.person;
Person person = session
  .include(p.attributes.values())
  .load(Person.class, "people/1");

Person mother = session
  .load(Person.class, person.getAttributes().get("Mother"));

Person father = session
  .load(Person.class, person.getAttributes().get("Father"));

assertEquals(1, session.advanced().getNumberOfRequests());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "people/1"}, new String[] { "Attributes.$Values"});

RavenJObject include1 = result.getIncludes().get(0);
RavenJObject include2 = result.getIncludes().get(1);

You can also include values from dictionary keys:

QHandlingDocumentRelationships_Person p = QHandlingDocumentRelationships_Person.person;
Person person = session
  .include(p.attributes.keys())
  .load(Person.class, "people/1");
store.getDatabaseCommands()
  .get(new String[] { "people/1"}, new String[] { "Attributes.$Keys" });

Complex types

If values in dictionary are more complex e.g.

@QueryEntity
public static class PersonWithAttribute {
  private String id;
  private String name;
  private Map<String, Attribute> attributes;

  public String getId() {
    return id;
  }
  public void setId(String id) {
    this.id = id;
  }
  public String getName() {
    return name;
  }
  public void setName(String name) {
    this.name = name;
  }
  public Map<String, Attribute> getAttributes() {
    return attributes;
  }
  public void setAttributes(Map<String, Attribute> attributes) {
    this.attributes = attributes;
  }
}

@QueryEntity
public static class Attribute {
  private String ref;

  public String getRef() {
    return ref;
  }
  public void setRef(String ref) {
    this.ref = ref;
  }
}

Attribute motherAttr = new Attribute();
motherAttr.setRef("people/2");

Attribute fatherAttr = new Attribute();
fatherAttr.setRef("people/3");

Map<String, Attribute> attributes = new HashMap<String, HandlingDocumentRelationships.Attribute>();
attributes.put("Mother", motherAttr);
attributes.put("Father", fatherAttr);

PersonWithAttribute person = new PersonWithAttribute();
person.setId("people/1");
person.setName("John Doe");
person.setAttributes(attributes);
session.store(person);

Person mother = new Person();
mother.setId("people/2");
mother.setName("Helen Doe");
mother.setAttributes(new HashMap<String, String>());
session.store(mother);

Person father = new Person();
father.setId("people/3");
father.setName("George Doe");
father.setAttributes(new HashMap<String, String>());

We can do includes on specific properties also:

QHandlingDocumentRelationships_PersonWithAttribute p =
  QHandlingDocumentRelationships_PersonWithAttribute.personWithAttribute;
PersonWithAttribute person = session
  .include(p.attributes.values().select().ref)
  .load(PersonWithAttribute.class, "people/1");

Person mother = session
  .load(Person.class, person.getAttributes().get("Mother").getRef());

Person father = session
  .load(Person.class, person.getAttributes().get("Father").getRef());

assertEquals(1, session.advanced().getNumberOfRequests());
MultiLoadResult result = store.getDatabaseCommands()
  .get(new String[] { "people/1"}, new String[] { "Attributes.$Values,Ref" });

RavenJObject include1 = result.getIncludes().get(0);
RavenJObject include2 = result.getIncludes().get(1);


Combining Approaches

It is possible to combine the above techniques. Using the DenormalizedCustomer from above and creating an order that uses it:

@QueryEntity
public class Order3 {
  private DenormalizedCustomer customer;
  private String[] supplierIds;
  private Referral referral;
  private LineItem[] lineItems;
  private double totalPrice;

  public DenormalizedCustomer getCustomer() {
    return customer;
  }
  public void setCustomer(DenormalizedCustomer customer) {
    this.customer = customer;
  }
  public String[] getSupplierIds() {
    return supplierIds;
  }
  public void setSupplierIds(String[] supplierIds) {
    this.supplierIds = supplierIds;
  }
  public Referral getReferral() {
    return referral;
  }
  public void setReferral(Referral referral) {
    this.referral = referral;
  }
  public LineItem[] getLineItems() {
    return lineItems;
  }
  public void setLineItems(LineItem[] lineItems) {
    this.lineItems = lineItems;
  }
  public double getTotalPrice() {
    return totalPrice;
  }
  public void setTotalPrice(double totalPrice) {
    this.totalPrice = totalPrice;
  }
}

We have the advantages of a denormalization, a quick and simple load of an Order and the fairly static Customer details that are required for most processing. But we also have the ability to easily and efficiently load the full Customer object when necessary using:

QHandlingDocumentRelationships_Order3 o = QHandlingDocumentRelationships_Order3.order3;
Order3 order = session
  .include(Customer.class, o.customer.id)
  .load(Order3.class, "orders/1234");

// this will not require querying the server!
Customer customer = session.load(Customer.class, order.getCustomer().getId());
MultiLoadResult result = store
  .getDatabaseCommands()
  .get(new String[] { "orders/1234" }, new String[] { "Customer.Id" });

RavenJObject order = result.getResults().get(0);
RavenJObject customer = result.getIncludes().get(0);

This combining of denormalization and Includes could also be used with a list of denormalized objects.

It is possible to use Include on a query against a Live Projection. Includes are evaluated after the TransformResults has been evaluated. This opens up the possibility of implementing Tertiary Includes (i.e. retrieving documents that are referenced by documents that are referenced by the root document).

Whilst RavenDB can support Tertiary Includes, before resorting to them you should re-evaluate your document model. Needing to use Tertiary Includes can be an indication that you are designing your documents along "Relational" lines.


Summary

There are no strict rules as to when to use which approach, but the general idea is to give it a lot of thought, and consider the implications each approach has.

As an example, in an e-commerce application it might be better to denormalize product names and prices into an order line object, since you want to make sure the customer sees the same price and product title in the order history. But the customer name and addresses should probably be references, rather than denormalized into the order entity.

For most cases where denormalization is not an option, Includes are probably the answer.