Understanding Relationship Types in Dynamics 365: N:N vs. 1:N & N:1

 

Understanding Relationship Types in Dynamics 365: N:N vs. 1:N & N:1

When modeling data relationships in Dynamics 365, understanding the distinction between N:N (Many-to-Many) and 1:N & N:1 (One-to-Many & Many-to-One) relationships is crucial. These relationship types serve different purposes and have unique characteristics.

N:N (Many-to-Many) Relationships

  • Invisible Bridge Entity: An N:N relationship in Dynamics 365 automatically creates a hidden "intersect" entity that stores the associations between the two main entities. This entity is managed by the system and is not directly visible or accessible to users​​.
  • Non-Hierarchical: N:N relationships do not impose a hierarchical structure between the related entities. Any record from one entity can be related to any number of records from the other entity without any parent-child implications.
  • Bidirectional Visibility: In the user interface, such as on forms, N:N relationships are typically represented through subgrids. If you link Entity A to Entity B, you can view this relationship from either entity's form through separate subgrids configured to display related records.
  • Flexibility: This relationship type is ideal for scenarios where records need to be freely associated with multiple records from another entity, without any hierarchical constraints.

1:N & N:1 (One-to-Many & Many-to-One) Relationships

  • Lookup Fields: 1:N and N:1 relationships involve lookup fields that establish a direct link between records. In a 1:N relationship, a lookup field on the 'N' side points to a single record on the '1' side, establishing a parent-child relationship.
  • Hierarchical Structure: These relationships inherently create a hierarchy. If Case A is the parent (1) of Case B (N), then Case B cannot also be the parent of Case A. This structure is crucial for scenarios where records need to have a clear parent-child relationship, such as categorizing products under a single category.
  • Directionality: The relationship is directional; the child record (N side) explicitly references its parent (1 side), and this relationship is visible on the parent record's form as a related list or subgrid of child records.

Additional Considerations

  • Customization and Maintenance: N:N relationships might be simpler to set up initially, as the system manages the intersect entity, but they can become complex to manage, especially in large datasets where many records are interconnected. Conversely, 1:N & N:1 relationships, with their explicit lookup fields, can be more straightforward to maintain but require more setup and consideration of the hierarchical structure.
  • Performance: N:N relationships can impact system performance more significantly than 1:N or N:1 relationships, especially with a high volume of records and associations. This is due to the additional overhead of managing the hidden intersect entities and the potentially large number of connections.
  • Use Case Fit: Choosing between N:N and 1:N & N:1 relationships should be guided by the specific requirements of your data model and business processes. N:N relationships are best suited for flexible, non-hierarchical associations, while 1:N & N:1 relationships are ideal for structured, hierarchical data organization.

Special Requirement Scenario for N:N Relationship Within the Same Entity

For a N:N relationship within the same entity, a unique requirement might arise where a reciprocal relationship needs to be explicitly created to ensure bidirectional visibility in subgrids. For example, if you have a Case entity and you want to link cases to each other to indicate a relationship (e.g., related incidents), you might encounter a scenario where linking Case A to Case B doesn't automatically make Case A appear in Case B's related cases subgrid.



To address this, you can implement a custom plugin that triggers upon the creation of a new record in the bridge entity (e.g., cr20d_incident_incident). The plugin would check if a reciprocal record exists (where Case B links back to Case A). If not, the plugin creates this reciprocal record, ensuring that the relationship is visible from both sides in the Dynamics 365 UI.

You can use FetchXML Builder on XrmToolBox to find this invisible bridge entity's fields' names.



There are plugins, one is for Association, the other is for Disassociation.

Association:



using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;

namespace DEMO.Plugins
{
    public class IncidentAssociationPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            // Obtain the execution context from the service provider.
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            // Ensure that the plugin is triggered by an Associate message
            if (context.MessageName.ToLower() != "associate")
                return;

            // Ensure that we're dealing with the correct relationship
            if (context.InputParameters.Contains("Relationship"))
            {
                Relationship relationship = (Relationship)context.InputParameters["Relationship"];
                if (relationship.SchemaName != "cr20d_Incident_Incident_Incident") // Replace with the correct schema name of your N:N relationship
                    return;
            }

            // Obtain the related entities involved in the association
            EntityReference targetEntity = (EntityReference)context.InputParameters["Target"];
            EntityReferenceCollection relatedEntities = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];

            // Obtain the organization service reference.
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            try
            {
                // Assuming relatedEntities collection will contain only one entity for simplicity. Adjust logic if it can contain more.
                if (relatedEntities != null && relatedEntities.Count > 0)
                {
                    EntityReference relatedEntity = relatedEntities[0]; // Considering single association for simplicity

                    // Check if a reciprocal relationship already exists
                    QueryExpression query = new QueryExpression("cr20d_incident_incident")
                    {
                        ColumnSet = new ColumnSet(false), // Retrieving only the entity IDs
                        Criteria = new FilterExpression
                        {
                            Conditions =
                        {
                            new ConditionExpression("incidentidone", ConditionOperator.Equal, relatedEntity.Id),
                            new ConditionExpression("incidentidtwo", ConditionOperator.Equal, targetEntity.Id)
                        }
                        }
                    };

                    EntityCollection results = service.RetrieveMultiple(query);

                    // If no reciprocal relationship exists, create one
                    if (results.Entities.Count == 0)
                    {
                        // Prepare the AssociateRequest to create the reciprocal link
                        AssociateRequest associateRequest = new AssociateRequest
                        {
                            Target = new EntityReference("incident", relatedEntity.Id), // Set the target as the related entity
                            RelatedEntities = new EntityReferenceCollection
                            {
                                new EntityReference("incident", targetEntity.Id) // Add the original target entity as the related entity
                            },
                            Relationship = new Relationship("cr20d_Incident_Incident_Incident")
                            {
                                PrimaryEntityRole = EntityRole.Referencing, // Adjust as needed for your relationship's configuration
                            }
                        };

                        // Execute the AssociateRequest to create the reciprocal relationship
                        service.Execute(associateRequest);
                    }


                }
            }
            catch (Exception ex)
            {
                throw new InvalidPluginExecutionException("An error occurred in the IncidentAssociationPlugin.", ex);
            }
        }
    }
}


Disassociation:



using System;
using Microsoft.Xrm.Sdk;
using Microsoft.Xrm.Sdk.Messages;
using Microsoft.Xrm.Sdk.Query;

namespace DEMO.Plugins
{
    public class IncidentDisassociationPlugin : IPlugin
    {
        public void Execute(IServiceProvider serviceProvider)
        {
            // Obtain the execution context from the service provider.
            IPluginExecutionContext context = (IPluginExecutionContext)serviceProvider.GetService(typeof(IPluginExecutionContext));

            // Ensure that the plugin is triggered by a Disassociate message
            if (context.MessageName.ToLower() != "disassociate")
                return;

            // Ensure that we're dealing with the correct relationship
            if (context.InputParameters.Contains("Relationship"))
            {
                Relationship relationship = (Relationship)context.InputParameters["Relationship"];
                if (relationship.SchemaName != "cr20d_Incident_Incident_Incident")
                    return;
            }

            // Obtain the target entity and related entities involved in the disassociation
            EntityReference targetEntity = (EntityReference)context.InputParameters["Target"];
            EntityReferenceCollection relatedEntities = (EntityReferenceCollection)context.InputParameters["RelatedEntities"];

            // Obtain the organization service reference.
            IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
            IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

            try
            {
                if (relatedEntities != null && relatedEntities.Count > 0)
                {
                    EntityReference relatedEntity = relatedEntities[0];

                    // Search for the reciprocal relationship to delete
                    QueryExpression query = new QueryExpression("cr20d_incident_incident")
                    {
                        ColumnSet = new ColumnSet(false), // Only need the entity IDs for deletion
                        Criteria = new FilterExpression
                        {
                            Conditions =
                            {
                                new ConditionExpression("incidentidone", ConditionOperator.Equal, relatedEntity.Id),
                                new ConditionExpression("incidentidtwo", ConditionOperator.Equal, targetEntity.Id)
                            }
                        }
                    };

                    EntityCollection results = service.RetrieveMultiple(query);

                    // If the reciprocal relationship exists, disassociate it
                    foreach (var result in results.Entities)
                    {
                        // Prepare the DisassociateRequest
                        DisassociateRequest disassociateRequest = new DisassociateRequest
                        {
                            Target = new EntityReference("incident", relatedEntity.Id), // Set the target as one end of the relationship
                            RelatedEntities = new EntityReferenceCollection
                            {
                                new EntityReference("incident", targetEntity.Id) // Specify the other end of the relationship to disassociate
                            },
                            Relationship = new Relationship("cr20d_Incident_Incident_Incident")
                            {
                                PrimaryEntityRole = EntityRole.Referencing,
                            }
                        };

                        // Execute the DisassociateRequest to remove the reciprocal relationship
                        service.Execute(disassociateRequest);
                    }

                }
            }
            catch (Exception ex)
            {
                throw new InvalidPluginExecutionException("An error occurred in the IncidentDisassociationPlugin.", ex);
            }
        }
    }
}


When designing your Dynamics 365 system, carefully consider the nature of the relationships between your entities and choose the most appropriate type to support your business processes and data management needs effectively.

No comments:

Post a Comment