/* Copyright (C) 2008 - 2011 Jordan Marr This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 3 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library. If not, see . */ using System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Data.Common; using Marr.Data.Mapping; using System.Reflection; namespace Marr.Data { /// /// Holds metadata about an object graph that is being queried and eagerly loaded. /// Contains all metadata needed to instantiate the object and fill it with data from a DataReader. /// Does not iterate through lazy loaded child relationships. /// internal class EntityGraph : IEnumerable { private MapRepository _repos; private EntityGraph _parent; private Type _entityType; private Relationship _relationship; private ColumnMapCollection _columns; private RelationshipCollection _relationships; private List _children; private object _entity; private GroupingKeyCollection _groupingKeyColumns; private Dictionary _entityReferences; internal IList RootList { get; private set; } internal bool IsParentReference { get; private set; } /// /// Recursively builds an entity graph of the given parent type. /// /// public EntityGraph(Type entityType, IList rootList) : this(entityType, null, null) // Recursively constructs hierarchy { RootList = rootList; } /// /// Recursively builds entity graph hierarchy. /// /// /// /// private EntityGraph(Type entityType, EntityGraph parent, Relationship relationship) { _repos = MapRepository.Instance; _entityType = entityType; _parent = parent; _relationship = relationship; IsParentReference = !IsRoot && AnyParentsAreOfType(entityType); if (!IsParentReference) { _columns = _repos.GetColumns(entityType); } _relationships = _repos.GetRelationships(entityType); _children = new List(); Member = relationship != null ? relationship.Member : null; _entityReferences = new Dictionary(); if (IsParentReference) { return; } // Create a new EntityGraph for each child relationship that is not lazy loaded foreach (Relationship childRelationship in Relationships) { if (!childRelationship.IsLazyLoaded) { _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType, this, childRelationship)); } } } public MemberInfo Member { get; private set; } /// /// Gets the parent of this EntityGraph. /// public EntityGraph Parent { get { return _parent; } } /// /// Gets the Type of this EntityGraph. /// public Type EntityType { get { return _entityType; } } /// /// Gets a boolean than indicates whether this entity is the root node in the graph. /// public bool IsRoot { get { return _parent == null; } } /// /// Gets a boolean that indicates whether this entity is a child. /// public bool IsChild { get { return _parent != null; } } /// /// Gets the columns mapped to this entity. /// public ColumnMapCollection Columns { get { return _columns; } } /// /// Gets the relationships mapped to this entity. /// public RelationshipCollection Relationships { get { return _relationships; } } /// /// A list of EntityGraph objects that hold metadata about the child entities that will be loaded. /// public List Children { get { return _children; } } /// /// Adds an Child in the graph for LazyLoaded property. /// public void AddLazyRelationship(Relationship childRelationship) { _children.Add(new EntityGraph(childRelationship.RelationshipInfo.EntityType.GetGenericArguments()[0], this, childRelationship)); } /// /// Adds an entity to the appropriate place in the object graph. /// /// public void AddEntity(object entityInstance) { _entity = entityInstance; // Add newly created entityInstance to list (Many) or set it to field (One) if (IsRoot) { RootList.Add(entityInstance); } else if (_relationship.RelationshipInfo.RelationType == RelationshipTypes.Many) { var list = _parent._entityReferences[_parent.GroupingKeyColumns.GroupingKey] .ChildLists[_relationship.Member.Name]; list.Add(entityInstance); } else // RelationTypes.One { if (_relationship.IsLazyLoaded) _relationship.Setter(_parent._entity, Activator.CreateInstance(_relationship.MemberType, entityInstance)); else _relationship.Setter(_parent._entity, entityInstance); } EntityReference entityRef = new EntityReference(entityInstance); _entityReferences.Add(GroupingKeyColumns.GroupingKey, entityRef); InitOneToManyChildLists(entityRef); } /// /// Searches for a previously loaded parent entity and then sets that reference to the mapped Relationship property. /// public void AddParentReference() { var parentReference = FindParentReference(); _relationship.Setter(_parent._entity, parentReference); } /// /// Concatenates the values of the GroupingKeys property and compares them /// against the LastKeyGroup property. Returns true if the values are different, /// or false if the values are the same. /// The currently concatenated keys are saved in the LastKeyGroup property. /// /// /// public bool IsNewGroup(DbDataReader reader) { bool isNewGroup = false; // Get primary keys from parent entity and any one-to-one child entites GroupingKeyCollection groupingKeyColumns = GroupingKeyColumns; // Concatenate column values KeyGroupInfo keyGroupInfo = groupingKeyColumns.CreateGroupingKey(reader); if (!keyGroupInfo.HasNullKey && !_entityReferences.ContainsKey(keyGroupInfo.GroupingKey)) { isNewGroup = true; } return isNewGroup; } /// /// Gets the GroupingKeys for this entity. /// GroupingKeys determine when to create and add a new entity to the graph. /// /// /// A simple entity with no relationships will return only its PrimaryKey columns. /// A parent entity with one-to-one child relationships will include its own PrimaryKeys, /// and it will recursively traverse all Children with one-to-one relationships and add their PrimaryKeys. /// A child entity that has a one-to-one relationship with its parent will use the same /// GroupingKeys already defined by its parent. /// public GroupingKeyCollection GroupingKeyColumns { get { if (_groupingKeyColumns == null) _groupingKeyColumns = GetGroupingKeyColumns(); return _groupingKeyColumns; } } private bool AnyParentsAreOfType(Type type) { EntityGraph parent = _parent; while (parent != null) { if (parent._entityType == type) { return true; } parent = parent._parent; } return false; } private object FindParentReference() { var parent = Parent.Parent; while (parent != null) { if (parent._entityType == _relationship.MemberType) { return parent._entity; } parent = parent.Parent; } return null; } /// /// Initializes the owning lists on many-to-many Children. /// /// private void InitOneToManyChildLists(EntityReference entityRef) { // Get a reference to the parent's the childrens' OwningLists to the parent entity for (int i = 0; i < Relationships.Count; i++) { Relationship relationship = Relationships[i]; if (relationship.RelationshipInfo.RelationType == RelationshipTypes.Many) { try { IList list = (IList)_repos.ReflectionStrategy.CreateInstance(relationship.MemberType); relationship.Setter(entityRef.Entity, list); // Save a reference to each 1-M list entityRef.AddChildList(relationship.Member.Name, list); } catch (Exception ex) { throw new DataMappingException( string.Format("{0}.{1} is a \"Many\" relationship type so it must derive from IList.", entityRef.Entity.GetType().Name, relationship.Member.Name), ex); } } } } /// /// Gets a list of keys to group by. /// /// /// When converting an unnormalized set of data from a database view, /// a new entity is only created when the grouping keys have changed. /// NOTE: This behavior works on the assumption that the view result set /// has been sorted by the root entity primary key(s), followed by the /// child entity primary keys. /// /// private GroupingKeyCollection GetGroupingKeyColumns() { // Get primary keys for this parent entity GroupingKeyCollection groupingKeyColumns = new GroupingKeyCollection(); groupingKeyColumns.PrimaryKeys.AddRange(Columns.PrimaryKeys); // The following conditions should fail with an exception: // 1) Any parent entity (entity with children) must have at least one PK specified or an exception will be thrown // 2) All 1-M relationship entities must have at least one PK specified // * Only 1-1 entities with no children are allowed to have 0 PKs specified. if ((groupingKeyColumns.PrimaryKeys.Count == 0 && _children.Count > 0) || (groupingKeyColumns.PrimaryKeys.Count == 0 && !IsRoot && _relationship.RelationshipInfo.RelationType == RelationshipTypes.Many)) throw new MissingPrimaryKeyException(string.Format("There are no primary key mappings defined for the following entity: '{0}'.", EntityType.Name)); // Add parent's keys if (IsChild) groupingKeyColumns.ParentPrimaryKeys.AddRange(Parent.GroupingKeyColumns); return groupingKeyColumns; } #region IEnumerable Members public IEnumerator GetEnumerator() { return TraverseGraph(this); } /// /// Recursively traverses through every entity in the EntityGraph. /// /// /// private static IEnumerator TraverseGraph(EntityGraph entityGraph) { Stack stack = new Stack(); stack.Push(entityGraph); while (stack.Count > 0) { EntityGraph node = stack.Pop(); yield return node; foreach (EntityGraph childGraph in node.Children) { stack.Push(childGraph); } } } #endregion #region IEnumerable Members IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } #endregion } } public struct KeyGroupInfo { private string _groupingKey; private bool _hasNullKey; public KeyGroupInfo(string groupingKey, bool hasNullKey) { _groupingKey = groupingKey; _hasNullKey = hasNullKey; } public string GroupingKey { get { return _groupingKey; } } public bool HasNullKey { get { return _hasNullKey; } } }