Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IsAssigned on child entities no longer get correct IsTransient value #1978

Closed
emwoutersen opened this issue Jan 15, 2019 · 3 comments
Closed

Comments

@emwoutersen
Copy link

emwoutersen commented Jan 15, 2019

We got a project where we want to use two different persisting strategies. In one case, we want the ID of an entity to be set by our database. In the second case, we want nhibernate to set the ID given by our software. In the past, we accomplished this second case by setting the IdentifierGeneratorStrategy property to assigned. However, in the NHibernate update 5.2.0, the check of this property in the IsTransient method has been removed. As a result, whenever we try to add a child entity with a given ID to a new or existing parent, and then commit the transaction, the sql NHibernate creates for the child entity is an update instead of an insert.

Below we created a simplified test project in which nhibernate tries to save (update) the new entity incorrectly:

namespace ConsoleApp1
{
	class Program
	{
		private const string connectionString = "connectionstring";

		static void Main(string[] args)
		{
			var cfg = CreateConfiguration();
			SetAssignedIdentifierGeneratorStrategy(cfg);

			var sessionFactory = cfg.BuildSessionFactory();
			var session = sessionFactory.OpenSession();
			session.BeginTransaction(IsolationLevel.ReadCommitted);

			var parent = new Parent(2, Guid.NewGuid().ToString());
			parent.AddChild(new Child(25, Guid.NewGuid().ToString()));
			session.Save(parent);

			session.Transaction.Commit();
		}

		private static Configuration CreateConfiguration()
		{
			var cfg = new Configuration();

			cfg.DataBaseIntegration(
				config => {
					config.ConnectionString = connectionString;
					config.LogSqlInConsole = false;
					config.Driver<OracleManagedDataClientDriver>();
					config.Dialect<Oracle10gDialect>();
					config.IsolationLevel = IsolationLevel.RepeatableRead;
					config.Timeout = 10;
					config.BatchSize = 10;
				});

			var mapping = CreateMapping();
			cfg.AddMapping(mapping);

			cfg.SetProperty("hibernate.current_session_context_class", "thread");
			cfg.SetProperty("current_session_context_class", "thread");
			cfg.SetProperty("default_flush_mode", "Commit");

			return cfg;
		}

		private static HbmMapping CreateMapping()
		{
			var mapper = new ModelMapper();
			mapper.AddMappings(new Type[]
			{
				typeof(ParentMapping),
				typeof(ChildMapping)
			});

			return mapper.CompileMappingForAllExplicitlyAddedEntities();
		}

		private static void SetAssignedIdentifierGeneratorStrategy(Configuration configuration)
		{
			foreach (var classMapping in configuration.ClassMappings)
			{
				var simpleValue = classMapping.Key as SimpleValue;
				if (simpleValue is null)
					continue;

				simpleValue.IdentifierGeneratorStrategy = "assigned";
			}
		}
	}
}

The exception this code gives is as follows:

System.Transactions Critical: 0 : <TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Critical">
  <TraceIdentifier>http://msdn.microsoft.com/TraceCodes/System/ActivityTracing/2004/07/Reliability/Exception/Unhandled</TraceIdentifier>
  <Description>Unhandled exception</Description>
  <AppDomain>ConsoleApp1.exe</AppDomain>
  <Exception>
    <ExceptionType>NHibernate.StaleStateException, NHibernate, Version=5.2.0.0, Culture=neutral, PublicKeyToken=aa95f207798dfdb4</ExceptionType>
    <Message>Batch update returned unexpected row count from update; actual row count: 0; expected: 1
[ UPDATE CHILDENTITY SET NAME = :p0 WHERE ID = :p1 ]
</Message>
    <StackTrace>   at NHibernate.AdoNet.Expectations.VerifyOutcomeBatched(Int32 expectedRowCount, Int32 rowCount, DbCommand statement)
   at NHibernate.AdoNet.OracleDataClientBatchingBatcher.DoExecuteBatch(DbCommand ps)
   at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchWithTiming(DbCommand ps)
   at NHibernate.AdoNet.AbstractBatcher.ExecuteBatch()
   at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
   at NHibernate.Engine.ActionQueue.ExecuteActions()
   at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
   at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
   at NHibernate.Impl.SessionImpl.Flush()
   at NHibernate.Impl.SessionImpl.FlushBeforeTransactionCompletion()
   at NHibernate.Impl.SessionImpl.BeforeTransactionCompletion(ITransaction tx)
   at NHibernate.Transaction.AdoTransaction.Commit()
   at ConsoleApp1.Program.Main(String[] args) in D:\Nhibernate\nhibernate-core\src\ConsoleApp1\ConsoleApp1\Program.cs:line 31</StackTrace>
    <ExceptionString>NHibernate.StaleStateException: Batch update returned unexpected row count from update; actual row count: 0; expected: 1
[ UPDATE CHILDENTITY SET NAME = :p0 WHERE ID = :p1 ]
   at NHibernate.AdoNet.Expectations.VerifyOutcomeBatched(Int32 expectedRowCount, Int32 rowCount, DbCommand statement)
   at NHibernate.AdoNet.OracleDataClientBatchingBatcher.DoExecuteBatch(DbCommand ps)
   at NHibernate.AdoNet.AbstractBatcher.ExecuteBatchWithTiming(DbCommand ps)
   at NHibernate.AdoNet.AbstractBatcher.ExecuteBatch()
   at NHibernate.Engine.ActionQueue.ExecuteActions(IList list)
   at NHibernate.Engine.ActionQueue.ExecuteActions()
   at NHibernate.Event.Default.AbstractFlushingEventListener.PerformExecutions(IEventSource session)
   at NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(FlushEvent event)
   at NHibernate.Impl.SessionImpl.Flush()
   at NHibernate.Impl.SessionImpl.FlushBeforeTransactionCompletion()
   at NHibernate.Impl.SessionImpl.BeforeTransactionCompletion(ITransaction tx)
   at NHibernate.Transaction.AdoTransaction.Commit()
   at ConsoleApp1.Program.Main(String[] args) in D:\Nhibernate\nhibernate-core\src\ConsoleApp1\ConsoleApp1\Program.cs:line 31</ExceptionString>
  </Exception>
</TraceRecord>
An unhandled exception of type 'NHibernate.StaleStateException' occurred in NHibernate.dll
Batch update returned unexpected row count from update; actual row count: 0; expected: 1
[ UPDATE CHILDENTITY SET NAME = :p0 WHERE ID = :p1 ]

This exception is caused by a the following change in the code:
From NHibernate.Persister.Entity.AbstractEntityPersister.IsTransient():

if (result2.HasValue)
{
	if (IdentifierGenerator is Assigned)
	{
		// if using assigned identifier, we can only make assumptions
		// if the value is a known unsaved-value
		if (result2.Value)
			return true;
	}
	else
	{
		return result2;
	}
}

To:

if (result2.HasValue)
{
	return result2;
}

(commit 52e3798)

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Jan 15, 2019

See #1756. It is likely you need to adjust the unsaved-value on your id mapping.

@fredericDelaporte
Copy link
Member

fredericDelaporte commented Jan 20, 2019

In other words, it is likely that your mapping, lacking in your report, is defining an unsuitable unsaved-value for your ids.
unsaved-value must have the special value undefined for having NHibernate query the database for checking if an entity exists or not, in order to perform the adequate operation (insert or update).
If your need is instead to have it always perform an insert, unsaved-value has to be set to any. This avoids the additional query for determining the existence of the entity. The interactions between the assigned identifier generator strategy and unsaved-value setting are detailed here.

The change you are blaming is the fix for a bug causing unsaved-value to not be taken into account correctly for assigned identifier. This is why I believe your mapping have indeed an inadequate unsaved-value setting, but which was ignored priori to v5.2 due the bug. Now you have to remove that inadequate unsaved-value setting.

@emwoutersen
Copy link
Author

emwoutersen commented Jan 20, 2019

In the mapping, the unsaved-value wasn't set. However, as we changed the identifiergeneratorstrategy in code (as shown in the example), the unsaved-value was implicitly left with the
(in this case) incorrect default value. We fixed the issue by changing this code part to the following:

private static void SetAssignedIdentifierGeneratorStrategy(Configuration configuration)
{
	foreach (var classMapping in configuration.ClassMappings)
	{
		var simpleValue = classMapping.Key as SimpleValue;
		if (simpleValue is null)
			continue;

		simpleValue.IdentifierGeneratorStrategy = "assigned";
		simpleValue.NullValue = "undefined";
	}
}

Thanks for your help.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants