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:
Shannon
2015-01-30 10:32:46 +11:00
parent 0f7b902bba
commit 90b3bf59e0
10 changed files with 154 additions and 73 deletions

View File

@@ -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; }
}
}
}

View File

@@ -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()

View File

@@ -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>

View File

@@ -0,0 +1,9 @@
namespace Umbraco.Core.Persistence
{
internal enum RecordPersistenceType
{
Insert,
Update,
Delete
}
}

View File

@@ -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
});
}
}
}

View File

@@ -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>

View File

@@ -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);
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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" />