Relationships Between Objects

  • 18 minutes to read

XPO supports three types of relationships between objects. The type of a relationship that is created depends upon how related objects are defined.

  • One-to-Many Relationships
  • One-to-One Relationships
  • Many-to-Many Relationships

One-to-Many Relationships

A one-to-many relationship is the most common type of relationship. In this relationship, a persistent object of type A can have many associated objects of type B, but an object of type B can have only one associated object of type A.

For example, orders can be associated with a specific customer by creating a relationship between the Orders property in the Customer object (the primary key) and the Customer property in the Order object (the foreign key). As a result, each customer can have multiple orders.

One-to-Many Relations

The data model can be defined as shown below.

public class Customer : XPObject {
    public string ContactName {
        get { return fContactName; }
        set { SetPropertyValue(nameof(ContactName), ref fContactName, value); }
    }
    string fContactName;

    public string Phone {
        get { return fPhone; }
        set { SetPropertyValue(nameof(Phone), ref fPhone, value); }
    }
    string fPhone;

    // ...
    // Apply the Association attribute to mark the Orders property 
    // as the "many" end of the Customer-Orders association.
    [Association]
    public XPCollection<Order> Orders { 
        get { return GetCollection<Order>(nameof(Orders)); }
    }
}

public class Order : XPObject {
    public string ProductName {
        get { return fProductName; }
        set { SetPropertyValue(nameof(ProductName), ref fProductName, value); }
    }
    string fProductName;

    public DateTime OrderDate {
        get { return fOrderDate; }
        set { SetPropertyValue(nameof(OrderDate), ref fOrderDate, value); }
    }
    DateTime fOrderDate;

    // ...
    // Apply the Association attribute to mark the Customer property 
    // as the "one" end of the Customer-Orders association.
    [Association]
    public Customer Customer {
        get { return fCustomer; }
        set { SetPropertyValue(nameof(Customer), ref fCustomer, value); }
    }
    Customer fCustomer;

}
IMPORTANT

Do not modify the XPCollection property declaration demonstrated above. Manipulating the collection or introducing any additional settings within the declaration may cause unpredictable behavior.

Here, the Association attribute (AssociationAttribute) is applied to the Customer.Orders and Order.Customer properties to identify both ends of the association. When one object relates to other objects, the "many" end of the association must be defined by an XPCollection or XPCollection<T> class.

Note that to be concise, we omitted all the getter/setter code that is required to track changes made to persistent properties. Otherwise, the Order.Customer property declaration code should look as follows.

public class Order : XPObject {
    // ...
    Customer fCustomer;
    [Association]
    public Customer Customer {
        get { return fCustomer; }
        set { SetPropertyValue(nameof(Customer), ref fCustomer, value); }
    }
}

To learn more about tracking changes in persistent objects, refer to Unit of Work.

One-to-One Relationships

Let's consider a simple example of a One-to-One relationship, when both classes that participate in the relationship have properties with a reference to the instance of their opposite class. It's necessary to write extra code within the property's setter method for each class that participates in the relationship to ensure the relationship's integrity, i.e., when a new object is assigned to a reference property, the reference to the previous object instance should be cleared and the assigned object should reference the current one.

This technique can be implemented as shown in the following code example.

// Represents the Building class which refers to the building's owner.
public class Building : XPObject {
    Person owner = null;
    public Person Owner {
        get { return owner; }
        set {
            if(owner == value)
                return;

            // Store a reference to the former owner.
            Person prevOwner = owner;
            owner = value;

            if(IsLoading) return;

            // Remove an owner's reference to this building, if exists.
            if(prevOwner != null && prevOwner.House == this)
                prevOwner.House = null;

            // Specify that the building is a new owner's house.
            if(owner != null)
                owner.House = this;
            OnChanged(nameof(Owner));
        }
    }
}

// Represents the Person class which refers to the person's house.
public class Person : XPObject {
    Building house = null;
    public Building House {
        get { return house; }
        set {
            if(house == value)
                return;

            // Store a reference to the person's former house.
            Building prevHouse = house;
            house = value;

            if(IsLoading) return;

            // Remove a reference to the house's owner, if the person is its owner.
            if(prevHouse != null && prevHouse.Owner == this)
                prevHouse.Owner = null;

            // Specify the person as a new owner of the house.
            if(house != null)
                house.Owner = this;

            OnChanged(nameof(House));
        }
    }
}

Many-to-Many Relationships

This section demonstrates how to create many-to-many relationships in databases created by XPO. If you are using XPO to map to existing databases, refer to the approach described in the Generating Persistent Objects for Existing Data Tables topic.

XPO can handle Many-to-Many relationship between objects. In the following code example, the Location class might contain several Departments and every Department can in turn span several Locations.

// Represents the Location class that contains its name and information 
// about the departments at the location.
public class Location: XPObject {
    public Location(Session session) : base(session) { }
    public string Name {
        get { return fName; }
        set { SetPropertyValue(nameof(Name), ref fName, value); }
    }
    string fName;

    // Apply the Association attribute to mark the Departments property 
    // as the many end of the LocationsDepartments association.
    [Association("LocationsDepartments")]
    public XPCollection<Department> Departments { 
        get { return GetCollection<Department>(nameof(Departments)); }
    }
}

// Represents the Department class that contains its name 
// and references all the locations of the department's offices.
public class Department: XPObject {
    public Department(Session session) : base(session) { }
    public string Name {
        get { return fName; }
        set { SetPropertyValue(nameof(Name), ref fName, value); }
    }
    string fName;

    // Apply the Association attribute to mark the Locations property 
    // as the many end of the LocationsDepartments association.
    [Association("LocationsDepartments")]
    public XPCollection<Location> Locations { 
        get { return GetCollection<Location>(nameof(Locations)); }
    }
}
IMPORTANT

Do not modify the XPCollection property declaration demonstrated above. Manipulating the collection or introducing any additional settings within the declaration may cause unpredictable behavior.

To create data use the following code.

using DevExpress.Xpo;

// ...
public partial class Form1 : Form {
    UnitOfWork uw;
    public Form1() {
        InitializeComponent();
        uw = new UnitOfWork();
        CreateData(uw);
    }
    private void CreateData(UnitOfWork uw) {
        Department dep = new Department(uw);
        dep.Name = "Department A";
        Location loc = new Location(uw);
        loc.Name = "USA";
        dep.Locations.Add(loc);
        loc = new Location(uw);
        loc.Name = "UK";
        dep.Locations.Add(loc);
        uw.CommitChanges();
    }
}

The code listed above is sufficient for XPO to work properly: it will generate all necessary intermediate tables and relationships.

To use the association's name as the name of a junction table, set the AssociationAttribute.UseAssociationNameAsIntermediateTableName property to true.

Association Attribute Specifics

The AssociationAttribute takes the following arguments.

  • The type of an object being linked to.

    This argument can be omitted for the "one" end (Order.Customer) because it is already specified by the property type. In addition, you can omit this argument if an XPCollection<T> is used for the "many" end (Customer.Orders), because the associated object type is specified by T.

    
    public class Customer : XPObject {
        // ...
        [Association]
        public XPCollection<Order> Orders { 
            get { return GetCollection<Order>(nameof(Orders)); }
        }
    }
    
    public class Order : XPObject {
        // ...
        Customer fCustomer;
        [Association]
        public Customer Customer {
            get { return fCustomer; }
            set { SetPropertyValue(nameof(Customer), ref fCustomer, value); }
        }
    }
    
  • The name of the association.

    You can optionally provide a name to mark association ends for your convenience. To accomplish this, use the same name for both association ends.

    
    public class Customer : XPObject {
        // ...
        [Association("Customer-Orders")]
        public XPCollection<Order> Orders { 
            get { return GetCollection<Order>(nameof(Orders)); }
        }
    }
    
    public class Order : XPObject {
        // ...
        Customer fCustomer;
        [Association("Customer-Orders")]
        public Customer Customer {
            get { return fCustomer; }
            set { SetPropertyValue(nameof(Customer), ref fCustomer, value); }
        }
    }
    

    Normally, XPO automatically resolves one-to-many association ends by the types being linked - so you don't have to provide association names in most cases. If a type is used in more than one association and cannot be singled out for the one end, an association name should be provided. Consider the following example of a class used to store a folder hierarchy.

    
    public class Folder : XPObject {
        // ...
        Folder fParent;
        [Association]
        public Folder Parent {
            get { return fParent; }
            set { SetPropertyValue(nameof(Parent), ref fParent, value); }
        }
    
        [Association]
        public XPCollection<Folder> Children {
            get { return GetCollection<Folder>(nameof(Children)); }
        }
    }
    

    The Folder class is used in one association, so there is no need to name it. If we add another Folder-based association within the same class, we have to specify association names to help XPO resolve associations as shown below.

    
    public class Folder : XPObject {
        // ...
        Folder fParent;
        [Association("Parent-Children")]
        public Folder Parent {
            get { return fParent; }
            set { SetPropertyValue(nameof(Parent), ref fParent, value); }
        }
    
        [Association("Parent-Children")]
        public XPCollection<Folder> Children {
            get { return GetCollection<Folder>(nameof(Children)); }
        }
    
        Folder fLinkedTo;
        [Association("LinkedTo-Links")]
        public Folder LinkedTo {
            get { return fLinkedTo; }
            set { SetPropertyValue(nameof(LinkedTo), ref fLinkedTo, value); }
        }
    
        [Association("LinkedTo-Links")]
        public XPCollection<Folder> Links {
            get { return GetCollection<Folder>(nameof(Links)); }
        }
    }
    

    Here, we specified association names (Parent-Children and LinkedTo-Links) based on properties at the association ends. You can provide any unique names to identify associations.

Create and Persist Association Objects in Code

TIP

A complete sample project is available in the DevExpress Code Examples database at http://www.devexpress.com/example=E4557.

Define a One-to-Many Association

Create the Customer object that has the Name and Age properties, and the Order object that has the ProductName and OrderDate properties (for details, see How to: Create a Business Model in the XPO Data Model Designer).

Relations: Create Objects

The Customer and Order objects must be connected with a one-to-many relationship since each customer can have multiple orders. Add the Customer property of the Customer type to the Order persistent class. This property will represent the "one" part of the association.

Relations: Column Type

Next, focus the AggregationRelationship toolbox item and draw a line from Customer to Order.

Relations: AggregationRelationship

Alternatively, you can define an association in code. To do this, the AssociationAttribute is used on both ends of an association. This attribute specifies the name of the relationship and points to the corresponding properties. The association name specified via the AssociationAttribute.Name parameter should match the name used at the other end of the association. The Orders property, which represents the 'many' side of a relationship, should be of the XPCollection type. It's value should be calculated via the XPBaseObject.GetCollection method that returns all persistent objects related to the property.

public class Order : XPObject {
   // ...
   private Customer _customer;
   [Association("Customer-Orders")]
   public Customer Customer {
      get { return _customer; }
      set { SetPropertyValue(nameof(Customer), ref _customer, value); }
   }
}
public class Customer : XPObject {
   // ...
   [Association("Customer-Orders")]
   public XPCollection<Order> Orders { get { return GetCollection<Order>(nameof(Orders)); } }
}

Add Persistent Objects Explicitly

Create Customer objects and populate the Customer.Orders collections with Orders.

if (xpCollection1.Count == 0) {
    Customer customer1 = new Customer(session1);
    customer1.Name = "John";
    customer1.Age = 21;
    customer1.Orders.Add(new Order(session1) { 
        ProductName = "Chai", 
        OrderDate = new DateTime(2013, 3, 11) 
    });
    customer1.Orders.Add(new Order(session1) {
        ProductName = "Konbu",
        OrderDate = new DateTime(2013, 1, 23)
    });
    customer1.Save();
    xpCollection1.Add(customer1);

    Customer customer2 = new Customer(session1);
    customer2.Name = "Bob";
    customer2.Age = 37;
    customer2.Orders.Add(new Order(session1) {
        ProductName = "Queso Cabrales",
        OrderDate = new DateTime(2013, 2, 9)
    });
    customer2.Save();
    xpCollection1.Add(customer2);
}
NOTE

The Save method checks whether the database contains the Customer and Order tables (these names match the names of the XP Objects being saved). If no such tables are found, they are created and relationships are established based on the declarations of the XP Objects.

Use the Back Master Reference Property

You can assign the Customer object to the Order object's Customer property. The new Order objects automatically appear in the Customer object's Orders collection.

if(xpCollection1.Count == 0) { 
    Customer customer1 = new Customer(session1); 
    customer1.Name = "John"; 
    customer1.Age = 21; 
    new Order(session1) { 
        ProductName = "Chai", 
        OrderDate = new DateTime(2013, 3, 11), 
        Customer = customer1 
    }; 
    new Order(session1) { 
        ProductName = "Konbu", 
        OrderDate = new DateTime(2013, 1, 23), 
        Customer = customer1 
    }; 
    customer1.Save(); 
    xpCollection1.Add(customer1); 

    Customer customer2 = new Customer(session1); 
    customer2.Name = "Bob"; 
    customer2.Age = 37; 
    new Order(session1) { 
        ProductName = "Queso Cabrales", 
        OrderDate = new DateTime(2013, 2, 9), 
        Customer = customer2 
    }; 
    customer2.Save(); 
    xpCollection1.Add(customer2); 
}

If you can access only the object's ID, get the object's instance from the current Session.

//... 
int customer1Id = 15; 
Customer customer1 = session1.GetObjectByKey<Customer>(customer1Id); 
new Order(session1) { 
    ProductName = "Chai", 
    OrderDate = new DateTime(2013, 3, 11), 
    Customer = customer1 
}; 
//... 

Aggregate Relationships (Cascade Delete)

To indicate an aggregate one-to-many relationship, add the Aggregated attribute (AggregatedAttribute) to the collection that specifies the association's "many" end. This indicates that the collection's objects are considered part of the associated object.

public class Customer : XPObject {
    // ...
    [Association, Aggregated]
    public XPCollection<Order> Orders {
        get { return GetCollection<Order>(nameof(Orders)); }
    }
}

Since it makes no sense to keep orders when the associated Customer object is deleted, the Aggregated attribute is applied to the Customer.Orders property. When a Customer object is deleted, the associated Order objects are deleted as well.

NOTE

If you have DevExpress WinForms components installed, you can try the functionality described here in the Object Relational Mapping | One to Many Relations section of the XPO Tutorials demo (C:\Users\Public\Documents\DevExpress Demos 20.1\Components\WinForms\Bin\XpoTutorials.exe).

Member Table

Online Knowledge Base

See Also