using System; using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.Logging; using NPoco; using Umbraco.Core.Cache; using Umbraco.Core.Models; using Umbraco.Core.Models.Entities; using Umbraco.Core.Persistence.Dtos; using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Scoping; using Umbraco.Core.Services; using static Umbraco.Core.Persistence.SqlExtensionsStatics; namespace Umbraco.Core.Persistence.Repositories.Implement { /// /// Represents a repository for doing CRUD operations for /// internal class RelationRepository : NPocoRepositoryBase, IRelationRepository { private readonly IRelationTypeRepository _relationTypeRepository; private readonly IEntityRepository _entityRepository; public RelationRepository(IScopeAccessor scopeAccessor, ILogger logger, IRelationTypeRepository relationTypeRepository, IEntityRepository entityRepository) : base(scopeAccessor, AppCaches.NoCache, logger) { _relationTypeRepository = relationTypeRepository; _entityRepository = entityRepository; } #region Overrides of RepositoryBase protected override IRelation PerformGet(int id) { var sql = GetBaseQuery(false); sql.Where(GetBaseWhereClause(), new { id }); var dto = Database.Fetch(SqlSyntax.SelectTop(sql, 1)).FirstOrDefault(); if (dto == null) return null; var relationType = _relationTypeRepository.Get(dto.RelationType); if (relationType == null) throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", dto.RelationType)); return DtoToEntity(dto, relationType); } protected override IEnumerable PerformGetAll(params int[] ids) { var sql = GetBaseQuery(false); if (ids.Length > 0) sql.WhereIn(x => x.Id, ids); sql.OrderBy(x => x.RelationType); var dtos = Database.Fetch(sql); return DtosToEntities(dtos); } protected override IEnumerable PerformGetByQuery(IQuery query) { var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate(); sql.OrderBy(x => x.RelationType); var dtos = Database.Fetch(sql); return DtosToEntities(dtos); } private IEnumerable DtosToEntities(IEnumerable dtos) { //NOTE: This is N+1, BUT ALL relation types are cached so shouldn't matter return dtos.Select(x => DtoToEntity(x, _relationTypeRepository.Get(x.RelationType))).ToList(); } private static IRelation DtoToEntity(RelationDto dto, IRelationType relationType) { var entity = RelationFactory.BuildEntity(dto, relationType); // reset dirty initial properties (U4-1946) entity.ResetDirtyProperties(false); return entity; } #endregion #region Overrides of NPocoRepositoryBase protected override Sql GetBaseQuery(bool isCount) { if (isCount) { return Sql().SelectCount().From(); } var sql = Sql().Select() .AndSelect("uchild", x => Alias(x.NodeObjectType, "childObjectType")) .AndSelect("uparent", x => Alias(x.NodeObjectType, "parentObjectType")) .From() .InnerJoin("uchild").On((rel, node) => rel.ChildId == node.NodeId, aliasRight: "uchild") .InnerJoin("uparent").On((rel, node) => rel.ParentId == node.NodeId, aliasRight: "uparent"); return sql; } protected override string GetBaseWhereClause() { return "umbracoRelation.id = @id"; } protected override IEnumerable GetDeleteClauses() { var list = new List { "DELETE FROM umbracoRelation WHERE id = @id" }; return list; } protected override Guid NodeObjectTypeId { get { throw new NotImplementedException(); } } #endregion #region Unit of Work Implementation protected override void PersistNewItem(IRelation entity) { entity.AddingEntity(); var dto = RelationFactory.BuildDto(entity); var id = Convert.ToInt32(Database.Insert(dto)); entity.Id = id; PopulateObjectTypes(entity); entity.ResetDirtyProperties(); } protected override void PersistUpdatedItem(IRelation entity) { entity.UpdatingEntity(); var dto = RelationFactory.BuildDto(entity); Database.Update(dto); PopulateObjectTypes(entity); entity.ResetDirtyProperties(); } #endregion /// /// Used for joining the entity query with relations for the paging methods /// /// private void SqlJoinRelations(Sql sql) { // add left joins for relation tables (this joins on both child or parent, so beware that this will normally return entities for // both sides of the relation type unless the IUmbracoEntity query passed in filters one side out). sql.LeftJoin().On((left, right) => left.NodeId == right.ChildId || left.NodeId == right.ParentId); sql.LeftJoin().On((left, right) => left.RelationType == right.Id); } public IEnumerable GetPagedParentEntitiesByChildId(int childId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); sql.Where(rel => rel.ChildId == childId); sql.Where((rel, node) => rel.ParentId == childId || node.NodeId != childId); }); } public IEnumerable GetPagedChildEntitiesByParentId(int parentId, long pageIndex, int pageSize, out long totalRecords, params Guid[] entityTypes) { // var contentObjectTypes = new[] { Constants.ObjectTypes.Document, Constants.ObjectTypes.Media, Constants.ObjectTypes.Member } // we could pass in the contentObjectTypes so that the entity repository sql is configured to do full entity lookups so that we get the full data // required to populate content, media or members, else we get the bare minimum data needed to populate an entity. BUT if we do this it // means that the SQL is less efficient and returns data that is probably not needed for what we need this lookup for. For the time being we // will just return the bare minimum entity data. return _entityRepository.GetPagedResultsByQuery(Query(), entityTypes, pageIndex, pageSize, out totalRecords, null, null, sql => { SqlJoinRelations(sql); sql.Where(rel => rel.ParentId == parentId); sql.Where((rel, node) => rel.ChildId == parentId || node.NodeId != parentId); }); } public void Save(IEnumerable relations) { foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { if (hasIdentityGroup.Key) { // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation // however we can bulk populate the object types. It might be possible to bulk update // with SQL but would be pretty ugly and we're not really too worried about that for perf, // it's the bulk inserts we care about. var asArray = hasIdentityGroup.ToArray(); foreach (var relation in hasIdentityGroup) { relation.UpdatingEntity(); var dto = RelationFactory.BuildDto(relation); Database.Update(dto); } PopulateObjectTypes(asArray); } else { // Do bulk inserts var entitiesAndDtos = hasIdentityGroup.ToDictionary( r => // key = entity { r.AddingEntity(); return r; }, RelationFactory.BuildDto); // value = DTO foreach (var dto in entitiesAndDtos.Values) { Database.Insert(dto); } // All dtos now have IDs assigned foreach (var de in entitiesAndDtos) { // re-assign ID to the entity de.Key.Id = de.Value.Id; } PopulateObjectTypes(entitiesAndDtos.Keys.ToArray()); } } } public void SaveBulk(IEnumerable relations) { foreach (var hasIdentityGroup in relations.GroupBy(r => r.HasIdentity)) { if (hasIdentityGroup.Key) { // Do updates, we can't really do a bulk update so this is still a 1 by 1 operation // however we can bulk populate the object types. It might be possible to bulk update // with SQL but would be pretty ugly and we're not really too worried about that for perf, // it's the bulk inserts we care about. foreach (var relation in hasIdentityGroup) { var dto = RelationFactory.BuildDto(relation); Database.Update(dto); } } else { // Do bulk inserts var dtos = hasIdentityGroup.Select(RelationFactory.BuildDto); Database.InsertBulk(dtos); } } } public IEnumerable GetPagedRelationsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Ordering ordering) { var sql = GetBaseQuery(false); if (ordering == null || ordering.IsEmpty) ordering = Ordering.By(SqlSyntax.GetQuotedColumn(Constants.DatabaseSchema.Tables.Relation, "id")); var translator = new SqlTranslator(sql, query); sql = translator.Translate(); // apply ordering ApplyOrdering(ref sql, ordering); var pageIndexToFetch = pageIndex + 1; var page = Database.Page(pageIndexToFetch, pageSize, sql); var dtos = page.Items; totalRecords = page.TotalItems; var relTypes = _relationTypeRepository.GetMany(dtos.Select(x => x.RelationType).Distinct().ToArray()) .ToDictionary(x => x.Id, x => x); var result = dtos.Select(r => { if (!relTypes.TryGetValue(r.RelationType, out var relType)) throw new InvalidOperationException(string.Format("RelationType with Id: {0} doesn't exist", r.RelationType)); return DtoToEntity(r, relType); }).ToList(); return result; } public void DeleteByParent(int parentId, params string[] relationTypeAliases) { var subQuery = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.RelationType, x => x.Id) .Where(x => x.ParentId == parentId); if (relationTypeAliases.Length > 0) { subQuery.WhereIn(x => x.Alias, relationTypeAliases); } Database.Execute(Sql().Delete().WhereIn(x => x.Id, subQuery)); } /// /// Used to populate the object types after insert/update /// /// private void PopulateObjectTypes(params IRelation[] entities) { var entityIds = entities.Select(x => x.ParentId).Concat(entities.Select(y => y.ChildId)).Distinct(); var nodes = Database.Fetch(Sql().Select().From() .WhereIn(x => x.NodeId, entityIds)) .ToDictionary(x => x.NodeId, x => x.NodeObjectType); foreach (var e in entities) { if (nodes.TryGetValue(e.ParentId, out var parentObjectType)) { e.ParentObjectType = parentObjectType.GetValueOrDefault(); } if (nodes.TryGetValue(e.ChildId, out var childObjectType)) { e.ChildObjectType = childObjectType.GetValueOrDefault(); } } } private void ApplyOrdering(ref Sql sql, Ordering ordering) { if (sql == null) throw new ArgumentNullException(nameof(sql)); if (ordering == null) throw new ArgumentNullException(nameof(ordering)); // TODO: although this works for name, it probably doesn't work for others without an alias of some sort var orderBy = ordering.OrderBy; if (ordering.Direction == Direction.Ascending) sql.OrderBy(orderBy); else sql.OrderByDescending(orderBy); } } }