Relations in the class model

Up until now, the class Product only contains scalar properties. Genome allows you to map interconnected object graphs, where an object instance can point to one or more related objects. This section is covered in Step2, found under {Genome installation folder}\Samples\GettingStarted\Step2.

Extending a sample domain model

Let us extend the sample domain model to include some relations in the object graph:

 

Sample domain model

The model above contains the following singular object relations:

Additionally, the following 1:n relations exist in the model:

Finally, the model contains some arbitrary queries for Product:

To have an n:m relationship as well, let us create two additional classes:

Employee to Territory n:m relationship

Employee defines a Collection of Territories it is assigned to.

Territory in turn has a collection of assigned Employees.

Mapping relations

Genome provides a set of standard patterns to map relationships in the object graph. In addition, the LINQ mapping feature of Genome allows you to map relationships and arbitrary queries in the object graph. Properties of the domain model that have been mapped with LINQ or OQL can be reused in other LINQ (or OQL) mappings, thereby improving the code’s readability and maintainability.

Standard relation patterns and collection patterns mapping

The following standard relation patterns can be mapped with Genome:

Name

NearObjectReference

Description

Represents a readable and writeable singular object reference from class A to class B.

Representation
in class model

Property of type B on class A.

Representation
in database

Foreign key field(s) in table of A pointing to table of B.

Example

public abstract class OrderDetail : Persistent

{

    public abstract Order Order
    { get; set; }

}

 

<Type name="OrderDetail">

  <Member name="Order">

    <NearObjectReference/>

  </Member>

</Type>

 

OrderDetail has a property Order that points to the order that the detail belongs to.

 

Name

OneToManyCollection

Description

Represents a 1:n relationship between two classes (A:B)

Representation
in class model

Property of Collection<B> on class A.

This mapping feature expects that there is a NearObjectReference mapped in class B pointing to A.

Collection<B> allows elements of B to be explicitly added and removed. Alternatively, the collection can be configured to track newly created instances of B to enlist them automatically in the corresponding collection.

Representation
in database

Query that loads all instances of B whose foreign key field(s) point to this A.

Example

public abstract class Order : Persistent

{

    public abstract Collection<OrderDetail> Items
    { get; }

}

 

<Type name="Order">   

  <Member name="Items">

    <OneToManyCollection/>

  </Member>

</Type>

 

Order has a property Items that retrieves all OrderDetails that point to this order. Note that the property has a getter only, as items are added and removed through the operations of the collection.

 

Name

ManyToManyCollection

Description

Represents an n:m relationship between two classes (A:B).

Representation
in class model

Property of Collection<B> on class A.

Property of Collection<A> on class B.

The relationship can also be implemented and mapped in a single direction (omitting Collection<A> in B).

This mapping feature can optionally use an existing association class AB in the class model, whose instances point to the corresponding instances of A and B to associate them. The mapping feature can also hide the association class from the class model.

Collection<T> allows you to explicitly add and remove elements of T. When working with an explicit association class AB, the collection can be configured to track newly created instances of AB to enlist them automatically in the corresponding collection.

Representation
in database

Connection table AB with foreign keys to A and B.

Query that loads all instances of B that have an entry pointing to this A in the association table AB (and vice versa if mapped in both directions).

Example

public abstract class Employee : Persistent

{

    public abstract Collection<Territory> Territories
    { get; }

}

 

public abstract class Territory : Persistent

{

    public abstract Collection<Employee> Employees
    { get; }

}

 

<Type name="Employee">

  <Member name="Territories">

    <ManyToManyCollection>

      <Automatic/>

    </ManyToManyCollection>

  </Member>

</Type>

 

<Type name="Territory">

  <Member name="Employees">

    <ManyToManyCollection>

      <Reverse/>

    </ManyToManyCollection>

  </Member>

</Type>

 

Employee has a collection of associated territories. Each territory has a collection of associated employees.

Note that the properties have a getter only, as items are added and removed through the operations of the collection.

 

Arbitrary relationship mappings

The following arbitrary class relationships can be mapped:

 

Name

Arbitrary relationship to a set of objects

Description

Represents an arbitrary relationship from class A to B, expressed by an LINQ query.

Representation
in class model

Property of Set<B> on class A.

The query is mapped by an <Linq/> element in the mapping file.

Representation
in database

There is no specific pattern of representation for this relationship in the database, as it is expressed purely by a custom query mapped with LINQ.

Example

public abstract class Product : Persistent

{

    public abstract Set<Product> AlsoOrderedProducts
    { get; }

}

 

<Type name="Product">

<Member name="AlsoOrderedProducts" >

    <Linq><![CDATA[

      (from od in DataDomain.Extent<OrderDetail>()

      where od.Order.Items
          .Select(i => i.Product).Contains(this)

      select od.Product).Distinct()

    ]]></Linq>

</Member>

</Type>

 

Product has a property called AlsoOrderedProducts which returns all other Products bought by Customers in the same Order.

Note that the property has a getter only, as the property represents the read only results of a query.

 

Name

Arbitrary relationship to a single object

Description

Represents a read only singular association between class A and B expressed by an arbitrary LINQ query.

Representation
in class model

Property of B on class A.

The query is mapped by an <Linq/> element in the mapping file.

Representation
in database

There is no specific pattern of representation for this relationship in the database, as it is expressed purely by a custom query mapped with LINQ.

Example

public abstract class Product : Persistent

{

    public abstract Product TopAlsoOrderedProcut
    { get; }

}

 

<Type name="Product">

  <Member name="TopAlsoOrderedProduct" >

    <Linq><![CDATA[

      (from p in AlsoOrderedProducts

      orderby p.OrderCount

      select p).FirstOrDefault()

    ]]></Linq>

  </Member>

</Type>

Product has a property called TopOrderedProducts which returns the most often ordered product of the set of AlsoOrderedProducts.

Note that the property has a getter only, as the property represents the read only results of a query.

Note that the LINQ mapping reuses the properties AlsoOrderedProducts and OrderCount of product, which are also mapped with LINQ. As you can see, composing LINQ expressions from existing ones mapped in the domain model can improve query logic readability and maintainability.

 

Scalar LINQ mappings

Genome also supports returning scalar values from an LINQ query which are associated with the object instance and hence mapped as an LINQ property of the instance.

 

Name

Arbitrary scalar value

Description

Returns an arbitrary scalar value from an LINQ query associated with the current instance of the object.

Representation
in class model

Property of scalar type on class A.

The query is mapped by an <Linq/> element in the mapping file.

Representation
in database

There is no specific pattern of representation for this relationship in the database, as it is expressed purely by a custom query mapped with LINQ.

Example

public abstract class Product : Persistent

{

    public abstract int OrderCount { get; }

}

 

<Type name="Product">

  <Member name="OrderCount" >

    <Linq><![CDATA[

      DataDomain.Extent<OrderDetail>().

      Where(od => od.Product == this).

      Sum(od => (int?)od.Quantity) ?? 0

    ]]></Linq>

  </Member>

</Type>

 

Product has a property called OrderCount which returns the sum of all orders for this Product.

Note that the property has a getter only, as the property represents the read only results of a query.

 

Name

Set of scalar values

Description

Returns a set of scalar values from an arbitrary LINQ query associated with the current instance of the object.

Representation
in class model

Property of Set<T> on class A where T is a scalar type.

The query is mapped by an <Linq/> element in the mapping file.

Representation
in database

There is no specific pattern of representation for this relationship in the database, as it is expressed purely by a custom query mapped with LINQ.

Example

public abstract class Product : Persistent

{

    public abstract Set<int> LastTenOrderedQuantities
    { get; }

}

 

<Type name="Product">

  <Member name="LastTenOrderedQuantities" >

    <Linq><![CDATA[

      (from od in DataDomain.Extent<OrderDetail>()

      where od.Product == this

      orderby od.Order.OrderDate descending

      select od.Quantity).Take(10)

    ]]></Linq>

  </Member>

</Type>

 

Product has a property called LastTenOrderedQuantities which returns the product quantity ordered in the last ten orders as a set of int.

Note that the property has a getter only, as the property represents the read only results of a query.

 

 

Name

Array of scalar values

Description

Returns an array of scalar values from an arbitrary LINQ query associated with the current instance of the object.

Representation
in class model

Property of T[] on class A where T is a scalar type.

The query is mapped by an <Linq/> element in the mapping file.

Representation
in database

There is no specific pattern of representation for this relationship in the database, as it is expressed purely by a custom query mapped with LINQ.

Example

public abstract class Product : Persistent

{

  public abstract int[] LastTenOrderedQuantitiesArray
    { get; }

}

 

<Type name="Product">

  <Member name="LastTenOrderedQuantitiesArray" >

    <Linq><![CDATA[

      LastTenOrderedQuantities.ToArray()

    ]]></Linq>

  </Member>

</Type>

 

Product has a property called LastTenOrderedQuantitiesArray which returns the product quantity ordered in the last ten orders as array of int.

Note that the property has a getter only, as the property represents the read only results of a query.

 

The difference between Set<T> and T[] is that Genome loads T[] as soon as the mapped property is accessed, while Set<T> is only loaded when it is enumerated.

Other LINQ mappings

Genome also supports returning new transient types from an LINQ query as well as returning arbitrary object[] or arrays of persistent types. These possibilities will be discussed later.

Implementing and mapping a domain model

Extending Product

First, let’s add the new properties to the Product class:

public abstract class Product : Persistent

{

   

    public abstract Product TopAlsoOrderedProduct { get; }

    public abstract Set<Product> AlsoOrderedProducts { get; }

    public abstract Set<int> LastTenOrderQuantities { get; }

    public abstract int[] LastTenOrderQuantitiesArray { get; }

    public abstract int OrderCount { get; }    

}

Additional Properties on Business/Product.cs

Implementing Customer

After that, let us implement the Customer class:

using System;

using TechTalk.Genome;

 

namespace Business

{

    public abstract class Customer : Persistent

    {

        public abstract Guid Id { get; set; }   

        public abstract string Name { get; set; }

        public abstract Set<Order> Orders { get; }

        public Customer(string name)

        {

            Id = Guid.NewGuid();

            Name = name;

        }

    }

}

Business/Customer.cs

Customer has a Set of Orders. Note that Customer uses a client-side generated GUID which is initialised in the constructor as primary key identity.

Implementing Order and OrderDetail

Next we create the Order class:

using System;

using TechTalk.Genome;

 

namespace Business

{

    public abstract class Order : Persistent

    {

        public abstract int Id { get; }

        public abstract DateTime OrderDate { get; set; }

        public abstract Customer Customer { get; set; }

        public abstract Collection<OrderDetail> Items { get; }

        public Order()

        {

            OrderDate = DateTime.Now;

        }

    }

}

Business/Order.cs

The OrderDetail class associates Products with Orders:

using TechTalk.Genome;

 

namespace Business

{

    public abstract class OrderDetail : Persistent

    {

        public abstract Order BelongsToOrder { get; }

        public abstract Product Product { get; set; }

        public abstract int Quantity { get; set; }

       

        public OrderDetail(Product product, int quantity)

        {

            Product = product;

            Quantity = quantity;

        }

    }

}

Business/OrderDetail.cs

Note that BelongsToOrder is read only, as the OrderDetail is added to the Order.Items collection which sets the BelongsToOrder property of the OrderDetail.

Implementing Employee and Territory

Finally, let us implement Employee and Territory to have the n:m relationship in the class model:

using System;

using TechTalk.Genome;

 

namespace Business

{

    public abstract class Employee : Persistent

    {

        public abstract Guid Id { get; set; }

        public abstract string Name { get; set; }

        public abstract Collection<Territory> Territories { get; }

        public Employee(string name)

        {

            Id = Guid.NewGuid();

            Name =name;

        }

    }

}

Business/Employee.cs

using System;

using TechTalk.Genome;

 

namespace Business

{

    public abstract class Territory : Persistent

    {

        public abstract Guid Id { get; set; }

        public abstract string Name { get; set; }    

        public abstract Collection<Employee> Employees { get; }

        public Territory(string name)

        {

            Id = Guid.NewGuid();

            Name = name;

        }

    }

}

Business/Territory.cs

Both classes use a client-side generated GUID which is initialised in the constructor as primary key identity.

Forward engineering additional properties of Product to the mapping file

To map the calculated properties, navigate to each of the properties of Product and select “Map as LINQ” in the “Genome Mapping” context menu:

Genome context menu in the C# source code editor for mapping a property in LINQ

The forward engineering wizard creates an empty LINQ property mapping where you can fill in the corresponding query. Due to compatibility you can always choose OQL as an equivalent form of property mapping, however OQL support may discontinue in the future versions of Genome.

Switching between mapping file and source code

You can conveniently switch between mapping file and source code (and vice versa) by selecting “Go to member” or “Go to type” in the Genome Mapping context menu in the mapping file:

Genome context menu in Xml mapping editor of the DataDomain schema project for navigating to C# source code

Navigation also works in the other direction, from the source code to the mapping file.

Alternatively, you can use a keyboard shortcut (default: Ctrl-Alt-G) which brings you to the corresponding member or type.

Mapping Product.AlsoOrderedProducts

public abstract Set<Product> AlsoOrderedProducts { get; }

Additional property AlsoOrderedProducts in Business/Product.cs

<Type name="Product">

<Member name="AlsoOrderedProducts" >

    <Linq><![CDATA[

      (from od in DataDomain.Extent<OrderDetail>()

      where od.Order.Items
          .Select(i => i.Product).Contains(this)

      select od.Product).Distinct()

    ]]></Linq>

</Member>

</Type>

LINQ mapping for AlsoOrderedProducts in Business.Mapping/Product.xml

The LINQ above selects all OrderDetails which belong to an Order where the Product list of all Items contain this Product. The resulting OrderDetails are then projected to a distinct Set of Products.

Mapping Product.TopAlsoOrderedProducts

public abstract Product TopAlsoOrderedProduct { get; }

Additional property TopAlsoOrderedProduct in Business/Product.cs

<Member name="TopAlsoOrderedProduct" >

    <Linq><![CDATA[

      (from p in AlsoOrderedProducts

      orderby p.OrderCount

      select p).FirstOrDefault()

    ]]></Linq>

</Member>

LINQ mapping for TopAlsoOrderedProduct in Business.Mapping/Product.xml

The query above reuses the previous definition of AlsoOrderedProducts, orders it by their OrderCount (specified below) and returns the first Product of the result if exists.

Mapping Product.OrderCount

public abstract int OrderCount { get; }    

Additional property OrderCount in Business/Product.cs

<Member name="OrderCount" >

    <Linq><![CDATA[

      DataDomain.Extent<OrderDetail>().

      Where(od => od.Product == this).

      Sum(od => (int?)od.Quantity) ?? 0

    ]]></Linq>

</Member>

LINQ mapping for OrderCount in Business.Mapping/Product.xml

The code above finds all OrderDetails that reference this Product and then sums the result’s Quantities. Since Sum<> would return null on an empty set, we have to specify int? as the working type and finally decide what to do with a null value. Sum’s behaviour is deliberately designed so it resembles the SQL sum function.

Mapping Product.LastTenOrderQuantities

public abstract Set<int> LastTenOrderQuantities { get; }

Additional property LastTenOrderQuantities in Business/Product.cs

<Member name="LastTenOrderedQuantities" >

    <Linq><![CDATA[

      (from od in DataDomain.Extent<OrderDetail>()

      where od.Product == this

      orderby od.Order.OrderDate descending

      select od.Quantity).Take(10)

    ]]></Linq>

</Member>

LINQ mapping for LastTenOrderQuantities in Business.Mapping/Product.xml

The query above selects the last ten OrderDetails for this Product and keeping only the Quantity of the containing OrderDetail, resulting a Set of int.

Mapping Product.LastTenOrderArray

public abstract int[] LastTenOrderQuantitiesArray { get; }

Additional property LastTenOrderQuantitiesArray in Business/Product.cs

<Member name="LastTenOrderQuantitiesArray" >

    <Linq>

        <![CDATA[

            LastTenOrderQuantities.ToArray()

        ]]>

    </Linq>

</Member>

LINQ mapping for LastTenOrderQuantitiesArray in Business.Mapping/Product.xml

The query above reuses LastTenOrderQuantities and returns an array of int instead of a Set of int.

Mapping Customer

Customer can also be forward engineered:

<Type name="Customer">

  <RootInheritance/>

  <PrimaryKey>

    <Key>Id</Key>

  </PrimaryKey>

  <Sealed/>

  <CodeGeneratedProxy/>

  <Member name="Id">

    <PersistentField/>

  </Member>

  <Member name="Orders">

    <Linq><![CDATA[

      DataDomain.Extent<Order>().

      Where(o => o.Customer == this)

    ]]></Linq>

  </Member>

  <Member name="Name">

    <PersistentField />

  </Member>

</Type>

Business.Mapping/Customer.xml

After forward engineering, you need to fill in the LINQ tag to enumerate all Orders of the current Customer.

Mapping Order

The forward engineering wizard can automatically map Order fully. Having specified again a server-generated identity, we need to set up the NativeIdGenerator property for the identity field:

<Type name="Order">

        <RootInheritance/>

        <PrimaryKey>

    <Key>Id</Key>

        </PrimaryKey>

  <Sealed/>

  <CodeGeneratedProxy/>

 

  <Member name="Id">

    <PersistentField NativeIdGenerator="true"/>

  </Member>

  <Member name="OrderDate">

    <PersistentField/>

  </Member>

  <Member name="Customer">

    <NearObjectReference/>

  </Member>

  <Member name="Items">

    <OneToManyCollection/>

  </Member>

</Type>

Business.Mapping/Order.xml

Mapping OrderDetail

For OrderDetail, the forward engineering wizard cannot automatically detect the primary key properties of the class. The primary key is composed of Order and Product and has to be specified accordingly:

<Type name="OrderDetail">

  <RootInheritance/>

  <PrimaryKey>

    <Key>BelongsToOrder</Key>

    <Key>Product</Key>

  </PrimaryKey>

  <Sealed/>

  <CodeGeneratedProxy/>

 

  <Member name="Order">

    <NearObjectReference/>

  </Member>

  <Member name="Product">

    <NearObjectReference/>

  </Member>

  <Member name="Quantity">

    <PersistentField/>

  </Member>

</Type>

Business.Mapping/OrderDetail.xml

Mapping Employee

After creating the mapping file for Employee by forward engineering, the collection mapping has to be updated to ManyToManyCollection:

<Type name="Employee">

        <RootInheritance/>

        <PrimaryKey>

    <Key>Id</Key>

        </PrimaryKey>

  <Sealed/>

  <CodeGeneratedProxy/>

 

  <Member name="Id">

    <PersistentField/>

  </Member>

  <Member name="Name">

    <PersistentField />

  </Member>

  <Member name="Territories">

    <ManyToManyCollection>

      <Automatic />

    </ManyToManyCollection>

  </Member>

</Type>

Business.Mapping/Employee.xml

Mapping Territory

Similarly, the forward engineered mapping of Territory needs to be configured to point back to the same connection table as employee is using for the association:

<Type name="Territory">

        <RootInheritance/>

        <PrimaryKey>

    <Key>Id</Key>

        </PrimaryKey>

  <Sealed/>

  <CodeGeneratedProxy/>

 

  <Member name="Id">

    <PersistentField/>

  </Member>

  <Member name="Name">

    <PersistentField/>

  </Member>

  <Member name="Employees">

    <ManyToManyCollection>

      <Reverse/>

    </ManyToManyCollection>

  </Member>

</Type>

Business.Mapping/Territory.xml

Creating test data

To perform queries on the domain model, let us implement a method which creates some simple test data:

public static void CreateTestData()

{

    Helper.InitialiseDatabase();

    using(Context.Push(LocalContext.Create()))

    {

        Product[] products = {
            Helper.dd.New<Product>("Genome Evaluation", 0.0m),

            Helper.dd.New<Product>("Genome Community", 0.0m),

            Helper.dd.New<Product>("Genome Express", 299.0m),

            Helper.dd.New<Product>("Genome Professional",

                                   1800.0m),

            Helper.dd.New<Product>("Genome service hour",

                                   125.0m),

            Helper.dd.New<Product>("Genome database adapter",

                                   5000.0m),

                             };

 

        Helper.dd.New<Customer>("Jim");

        Helper.dd.New<Customer>("Jane");

        Helper.dd.New<Customer>("John");

        Helper.dd.New<Customer>("Mary");

 

        Context.CommitCurrent();

 

        int orderCount;

        Random randGen = new Random(0);

 

        foreach (Customer customer in Helper.dd.Extent<Customer>())

        {

            orderCount = randGen.Next(5);

            for(int i=0; i<orderCount; i++)

            {

                Order o = Helper.dd.New<Order>();

                o.Customer = customer;

                o.Items.Add(Helper.dd.New<OrderDetail>(

                              products[0],

                              1

                            ));

                o.Items.Add(Helper.dd.New<OrderDetail>(

                              products[randGen.Next(5)+1],

                              randGen.Next(10) + 1

                            ));       

            }

 

        }

 

        Context.CommitCurrent();

 

    }

}

CreateTestData() in Client/Program.cs



Working with persistent objects (Create, Update, Delete) Inheritance