Fixes: U4-2038 Violation of PRIMARY KEY constraint 'PK_cmsContentPreviewXml' YSOD occurs when publishing content
Conflicts: src/Umbraco.Core/Umbraco.Core.csproj
This commit is contained in:
@@ -10,12 +10,14 @@ namespace Umbraco.Core.Models
|
||||
internal class ContentPreviewEntity<TContent> : ContentXmlEntity<TContent>
|
||||
where TContent : IContentBase
|
||||
{
|
||||
public ContentPreviewEntity(bool previewExists, TContent content, Func<TContent, XElement> xml)
|
||||
: base(previewExists, content, xml)
|
||||
public ContentPreviewEntity(TContent content, Func<TContent, XElement> xml)
|
||||
: base(content, xml)
|
||||
{
|
||||
Version = content.Version;
|
||||
}
|
||||
|
||||
public Guid Version { get; private set; }
|
||||
public Guid Version
|
||||
{
|
||||
get { return Content.Version; }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,11 @@ namespace Umbraco.Core.Models
|
||||
internal class ContentXmlEntity<TContent> : IAggregateRoot
|
||||
where TContent : IContentBase
|
||||
{
|
||||
private readonly bool _entityExists;
|
||||
private readonly Func<TContent, XElement> _xml;
|
||||
|
||||
public ContentXmlEntity(bool entityExists, TContent content, Func<TContent, XElement> xml)
|
||||
{
|
||||
public ContentXmlEntity(TContent content, Func<TContent, XElement> xml)
|
||||
{
|
||||
if (content == null) throw new ArgumentNullException("content");
|
||||
_entityExists = entityExists;
|
||||
_xml = xml;
|
||||
Content = content;
|
||||
}
|
||||
@@ -32,6 +30,7 @@ namespace Umbraco.Core.Models
|
||||
{
|
||||
get { return _xml(Content); }
|
||||
}
|
||||
|
||||
public TContent Content { get; private set; }
|
||||
|
||||
public int Id
|
||||
@@ -44,9 +43,14 @@ namespace Umbraco.Core.Models
|
||||
public DateTime CreateDate { get; set; }
|
||||
public DateTime UpdateDate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Special case, always return false, this will cause the repositories managing
|
||||
/// this object to always do an 'insert' but these are special repositories that
|
||||
/// do an InsertOrUpdate on insert since the data for this needs to be managed this way
|
||||
/// </summary>
|
||||
public bool HasIdentity
|
||||
{
|
||||
get { return _entityExists; }
|
||||
get { return false; }
|
||||
}
|
||||
|
||||
public object DeepClone()
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Data.SqlClient;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Umbraco.Core.Logging;
|
||||
@@ -18,6 +20,95 @@ namespace Umbraco.Core.Persistence
|
||||
|
||||
internal static event CreateTableEventHandler NewTable;
|
||||
|
||||
/// <summary>
|
||||
/// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which
|
||||
/// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint.
|
||||
/// </summary>
|
||||
/// <param name="db"></param>
|
||||
/// <param name="poco"></param>
|
||||
/// <returns>
|
||||
/// Returns the action that executed, either an insert or an update
|
||||
///
|
||||
/// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are
|
||||
/// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to
|
||||
/// try to be as proficient as possible when we know this can occur and manually handle the issue.
|
||||
///
|
||||
/// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then
|
||||
/// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception,
|
||||
/// that's due to a race condition, in which case we need to retry and update.
|
||||
/// </remarks>
|
||||
internal static RecordPersistenceType InsertOrUpdate<T>(this Database db, T poco)
|
||||
where T : class
|
||||
{
|
||||
return db.InsertOrUpdate(poco, null, null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will handle the issue of inserting data into a table when there can be a violation of a primary key or unique constraint which
|
||||
/// can occur when two threads are trying to insert data at the exact same time when the data violates this constraint.
|
||||
/// </summary>
|
||||
/// <param name="db"></param>
|
||||
/// <param name="poco"></param>
|
||||
/// <param name="updateArgs"></param>
|
||||
/// <param name="updateCommand">If the entity has a composite key they you need to specify the update command explicitly</param>
|
||||
/// <returns>
|
||||
/// Returns the action that executed, either an insert or an update
|
||||
///
|
||||
/// NOTE: If an insert occurred and a PK value got generated, the poco object passed in will contain the updated value.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// In different databases, there are a few raw SQL options like MySql's ON DUPLICATE KEY UPDATE or MSSQL's MERGE WHEN MATCHED, but since we are
|
||||
/// also supporting SQLCE for which this doesn't exist we cannot simply rely on the underlying database to help us here. So we'll actually need to
|
||||
/// try to be as proficient as possible when we know this can occur and manually handle the issue.
|
||||
///
|
||||
/// We do this by first trying to Update the record, this will return the number of rows affected. If it is zero then we insert, if it is one, then
|
||||
/// we know the update was successful and the row was already inserted by another thread. If the rowcount is zero and we insert and get an exception,
|
||||
/// that's due to a race condition, in which case we need to retry and update.
|
||||
/// </remarks>
|
||||
internal static RecordPersistenceType InsertOrUpdate<T>(this Database db,
|
||||
T poco,
|
||||
string updateCommand,
|
||||
object updateArgs)
|
||||
where T : class
|
||||
{
|
||||
if (poco == null) throw new ArgumentNullException("poco");
|
||||
|
||||
var rowCount = updateCommand.IsNullOrWhiteSpace()
|
||||
? db.Update(poco)
|
||||
: db.Update<T>(updateCommand, updateArgs);
|
||||
|
||||
if (rowCount > 0) return RecordPersistenceType.Update;
|
||||
|
||||
try
|
||||
{
|
||||
db.Insert(poco);
|
||||
return RecordPersistenceType.Insert;
|
||||
}
|
||||
//TODO: Need to find out if this is the same exception that will occur for all databases... pretty sure it will be
|
||||
catch (SqlException ex)
|
||||
{
|
||||
//This will occur if the constraint was violated and this record was already inserted by another thread,
|
||||
//at this exact same time, in this case we need to do an update
|
||||
|
||||
rowCount = updateCommand.IsNullOrWhiteSpace()
|
||||
? db.Update(poco)
|
||||
: db.Update<T>(updateCommand, updateArgs);
|
||||
|
||||
if (rowCount == 0)
|
||||
{
|
||||
//this would be strange! in this case the only circumstance would be that at the exact same time, 3 threads executed, one
|
||||
// did the insert and the other somehow managed to do a delete precisely before this update was executed... now that would
|
||||
// be real crazy. In that case we need to throw an exception.
|
||||
throw new DataException("Record could not be inserted or updated");
|
||||
}
|
||||
|
||||
return RecordPersistenceType.Update;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will escape single @ symbols for peta poco values so it doesn't think it's a parameter
|
||||
/// </summary>
|
||||
|
||||
9
src/Umbraco.Core/Persistence/RecordPersistenceType.cs
Normal file
9
src/Umbraco.Core/Persistence/RecordPersistenceType.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
namespace Umbraco.Core.Persistence
|
||||
{
|
||||
internal enum RecordPersistenceType
|
||||
{
|
||||
Insert,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
}
|
||||
@@ -60,13 +60,20 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
//NOTE: Not implemented because all ContentPreviewEntity will always return false for having an Identity
|
||||
protected override void PersistUpdatedItem(ContentPreviewEntity<TContent> entity)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
protected override void PersistNewItem(ContentPreviewEntity<TContent> entity)
|
||||
{
|
||||
if (entity.Content.HasIdentity == false)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot insert a preview for a content item that has no identity");
|
||||
throw new InvalidOperationException("Cannot insert or update a preview for a content item that has no identity");
|
||||
}
|
||||
|
||||
var previewPoco = new PreviewXmlDto
|
||||
@@ -77,33 +84,13 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
Xml = entity.Xml.ToString(SaveOptions.None)
|
||||
};
|
||||
|
||||
Database.Insert(previewPoco);
|
||||
//We need to do a special InsertOrUpdate here because we know that the PreviewXmlDto table has a composite key and thus
|
||||
// a unique constraint which can be violated if 2+ threads try to execute the same insert sql at the same time.
|
||||
Database.InsertOrUpdate(previewPoco,
|
||||
//Since the table has a composite key, we need to specify an explit update statement
|
||||
"SET xml = @Xml, timestamp = @Timestamp WHERE nodeId=@NodeId AND versionId=@VersionId",
|
||||
new {NodeId = previewPoco.NodeId, VersionId = previewPoco.VersionId, Xml = previewPoco.Xml, Timestamp = previewPoco.Timestamp});
|
||||
}
|
||||
|
||||
protected override void PersistUpdatedItem(ContentPreviewEntity<TContent> entity)
|
||||
{
|
||||
if (entity.Content.HasIdentity == false)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot update a preview for a content item that has no identity");
|
||||
}
|
||||
|
||||
var previewPoco = new PreviewXmlDto
|
||||
{
|
||||
NodeId = entity.Id,
|
||||
Timestamp = DateTime.Now,
|
||||
VersionId = entity.Version,
|
||||
Xml = entity.Xml.ToString(SaveOptions.None)
|
||||
};
|
||||
|
||||
Database.Update<PreviewXmlDto>(
|
||||
"SET xml = @Xml, timestamp = @Timestamp WHERE nodeId = @Id AND versionId = @Version",
|
||||
new
|
||||
{
|
||||
Xml = previewPoco.Xml,
|
||||
Timestamp = previewPoco.Timestamp,
|
||||
Id = previewPoco.NodeId,
|
||||
Version = previewPoco.VersionId
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,10 +681,8 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// <param name="content"></param>
|
||||
/// <param name="xml"></param>
|
||||
public void AddOrUpdateContentXml(IContent content, Func<IContent, XElement> xml)
|
||||
{
|
||||
var contentExists = Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0;
|
||||
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IContent>(contentExists, content, xml));
|
||||
{
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IContent>(content, xml));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -703,11 +701,7 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
/// <param name="xml"></param>
|
||||
public void AddOrUpdatePreviewXml(IContent content, Func<IContent, XElement> xml)
|
||||
{
|
||||
var previewExists =
|
||||
Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version",
|
||||
new { Id = content.Id, Version = content.Version }) != 0;
|
||||
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IContent>(previewExists, content, xml));
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IContent>(content, xml));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -55,6 +55,12 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
{
|
||||
get { throw new NotImplementedException(); }
|
||||
}
|
||||
|
||||
//NOTE: Not implemented because all ContentXmlEntity will always return false for having an Identity
|
||||
protected override void PersistUpdatedItem(ContentXmlEntity<TContent> entity)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -68,22 +74,21 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
{
|
||||
if (entity.Content.HasIdentity == false)
|
||||
{
|
||||
throw new InvalidOperationException("Cannot insert an xml entry for a content item that has no identity");
|
||||
throw new InvalidOperationException("Cannot insert or update an xml entry for a content item that has no identity");
|
||||
}
|
||||
|
||||
var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) };
|
||||
Database.Insert(poco);
|
||||
}
|
||||
|
||||
protected override void PersistUpdatedItem(ContentXmlEntity<TContent> entity)
|
||||
{
|
||||
if (entity.Content.HasIdentity == false)
|
||||
var poco = new ContentXmlDto
|
||||
{
|
||||
throw new InvalidOperationException("Cannot update an xml entry for a content item that has no identity");
|
||||
}
|
||||
NodeId = entity.Id,
|
||||
Xml = entity.Xml.ToString(SaveOptions.None)
|
||||
};
|
||||
|
||||
var poco = new ContentXmlDto { NodeId = entity.Id, Xml = entity.Xml.ToString(SaveOptions.None) };
|
||||
Database.Update(poco);
|
||||
//We need to do a special InsertOrUpdate here because we know that the ContentXmlDto table has a 1:1 relation
|
||||
// with the content table and a record may or may not exist so the
|
||||
// unique constraint which can be violated if 2+ threads try to execute the same insert sql at the same time.
|
||||
Database.InsertOrUpdate(poco);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -259,18 +259,12 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
|
||||
public void AddOrUpdateContentXml(IMedia content, Func<IMedia, XElement> xml)
|
||||
{
|
||||
var contentExists = Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0;
|
||||
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IMedia>(contentExists, content, xml));
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IMedia>(content, xml));
|
||||
}
|
||||
|
||||
public void AddOrUpdatePreviewXml(IMedia content, Func<IMedia, XElement> xml)
|
||||
{
|
||||
var previewExists =
|
||||
Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version",
|
||||
new { Id = content.Id, Version = content.Version }) != 0;
|
||||
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IMedia>(previewExists, content, xml));
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IMedia>(content, xml));
|
||||
}
|
||||
|
||||
protected override void PerformDeleteVersion(int id, Guid versionId)
|
||||
|
||||
@@ -675,18 +675,12 @@ namespace Umbraco.Core.Persistence.Repositories
|
||||
|
||||
public void AddOrUpdateContentXml(IMember content, Func<IMember, XElement> xml)
|
||||
{
|
||||
var contentExists = Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsContentXml WHERE nodeId = @Id", new { Id = content.Id }) != 0;
|
||||
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IMember>(contentExists, content, xml));
|
||||
_contentXmlRepository.AddOrUpdate(new ContentXmlEntity<IMember>(content, xml));
|
||||
}
|
||||
|
||||
public void AddOrUpdatePreviewXml(IMember content, Func<IMember, XElement> xml)
|
||||
{
|
||||
var previewExists =
|
||||
Database.ExecuteScalar<int>("SELECT COUNT(nodeId) FROM cmsPreviewXml WHERE nodeId = @Id AND versionId = @Version",
|
||||
new { Id = content.Id, Version = content.Version }) != 0;
|
||||
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IMember>(previewExists, content, xml));
|
||||
_contentPreviewRepository.AddOrUpdate(new ContentPreviewEntity<IMember>(content, xml));
|
||||
}
|
||||
|
||||
protected override string GetDatabaseFieldNameForOrderBy(string orderBy)
|
||||
|
||||
@@ -314,6 +314,7 @@
|
||||
<Compile Include="HideFromTypeFinderAttribute.cs" />
|
||||
<Compile Include="IApplicationEventHandler.cs" />
|
||||
<Compile Include="IDisposeOnRequestEnd.cs" />
|
||||
<Compile Include="Persistence\RecordPersistenceType.cs" />
|
||||
<Compile Include="IO\ResizedImage.cs" />
|
||||
<Compile Include="IO\UmbracoMediaFile.cs" />
|
||||
<Compile Include="Models\IPartialView.cs" />
|
||||
|
||||
Reference in New Issue
Block a user