The model contains two entities:

1) Entity A with paired association to entity B

2) Entity B with not nullable field and paired association to entity A

After creating entity B we can set its fields to some values before persisting it to database. But in case of paired field there is one more update for linked entity that causes persisting all changes (even uncompleted). I have faced this problem after setting field in constructor but it can be reproduced with simple property setters

Code sample to reproduce the issue:

namespace Sample {

using System;
using System.Linq;
using Xtensive.Orm;
using Xtensive.Orm.Configuration;

[HierarchyRoot]
public class EntityWithVersion : Entity
{
    public EntityWithVersion(Guid id)
    {
        Id = id;
    }

    [Field]
    [Key]
    public Guid Id { get; }

    [Field]
    public LinkEntity LinkEntity { get; set; }
}

[HierarchyRoot]
public class LinkEntity : Entity
{
    public LinkEntity(Guid id)
    {
        Id = id;
    }

    [Field]
    [Key]
    public Guid Id { get; }

    [Field(Nullable = false)]
    public string Str { get; set; }

    [Field]
    [Association(PairTo = nameof(Sample.EntityWithVersion.LinkEntity))]
    public EntityWithVersion EntityWithVersion { get; set; }
}

class Program
{
    static void Main(string[] args)
    {
        var dc = new DomainConfiguration("sqlserver", "Data Source=.; Initial Catalog=DO40-Tests;Connection Timeout=300;Integrated Security = true;Max pool size=10");

        dc.Types.Register(typeof(Program).Assembly);
        dc.UpgradeMode = DomainUpgradeMode.Recreate;

        var sessionConfiguration = new SessionConfiguration(SessionOptions.AutoActivation | SessionOptions.ServerProfile);

        using (var domain = Domain.Build(dc))
        {
            for (var i = 0; i < 1000; i++)
            {
                using (var session = domain.OpenSession(sessionConfiguration))
                using (session.Activate())
                using (session.OpenTransaction(TransactionOpenMode.New))
                {
                    Console.WriteLine($"Creating {i} entities");

                    var owners = Enumerable.Range(0, i)
                        .Select(_ => new EntityWithVersion(Guid.NewGuid()))
                    .ToArray();

                    foreach (var owner in owners)
                    {
                        new LinkEntity(Guid.NewGuid())
                        {
                            EntityWithVersion = owner,
                            Str = "test",
                        };
                    }
                }
            }

        }
    }
}

}

On some iteration (it may be 499, 251 or something else) there is exception: Xtensive.Orm.CheckConstraintViolationException: SQL error occured. Storage error details 'Entity: LinkEntity; Field: Str (FieldInfo);' SQL error details 'Type: CheckConstraintViolation; Table: LinkEntity; Column: Str;' Query 'INSERT INTO [dbo].[LinkEntity] ([Id], [Str], [EntityWithVersion.Id]) VALUES (@p1_0, @p1_1, @p1_2);

...

INSERT INTO [dbo].[LinkEntity] ([Id], [Str], [EntityWithVersion.Id]) VALUES (@p25_0, @p25_1, @p25_2); [p1_0='35ae1f7f-4e17-4f6a-90a8-0993ab98f1fb';p1_1='';p1_2='';

...

p25_0='0facc445-2760-47dc-a5cc-909bb4a13e22';p25_1='test';p25_2='ce3808f0-0d4d-45b5-b2a7-7a0b8e67c615' ]'

Original message 'Cannot insert the value NULL into column 'Str', table 'DO40-Tests.dbo.LinkEntity'; column does not allow nulls. INSERT fails. The statement has been terminated.' ---> System.Data.SqlClient.SqlException: Cannot insert the value NULL into column 'Str', table 'DO40-Tests.dbo.LinkEntity'; column does not allow nulls. INSERT fails.

...

One more issue here is displaying empty string ('') instead of NULL in SQL Query text (SQL Profiler shows NULL)

Any fast workaround is appreciated

asked Feb 08 '19 at 08:05

Platonov's gravatar image

Platonov
5778

edited Feb 08 '19 at 08:07


3 Answers:

Hello Platonov,

Some changes for your code that might really help further.

1)Do not skip setters in persistent classes. There will be problems. We use both setters and getters for doing our ORM magic ;)

2) Key fields initialization does not work like the way you did. So overall it should be like

  public EntityWithVersion(Guid id)
    :base(id)
  {
  }

[Field] [Key] public Guid Id { get; private set; }

3) DataObjects.Net is smart enough to do Guid.CreateNew() for you if an entity has Guid key field so in general you don't need constructor to initialize Id at all.

4) Speaking of actual problem, the workaround is to initialize the string field before the reference field. Setting of reference to an entity can initiate persisting of changes to database. And if it does, persisting operation will be before setting of the string field.

DataObjects.Net has a bunch of field validator including [NotNullConstraintAttribute]. Xtensive.Orm.Validation namespace contains all available validators.

Also it would be great if you prevent null values of fields which are not nullable by requiring needed values in constructor.

UPDATE: Persist is happening because of a lot of changes during session. Session has a registry which stores information betwee persist operations. By default it is 256 changes. So when the registry is full it has to persist changes to database. In general 256 items is enough because by default each select query will persist changes and clear the registry. But for changes intensive sessions such as you represented it might overflow. Default settings might be changed by setting SessionConfiguration.EntityChangeRegistrySize.

answered Feb 08 '19 at 09:46

Alexey%20Kulakov's gravatar image

Alexey Kulakov
77225

edited Feb 12 '19 at 06:10

I have updated my answer. Please, check it out.

(Feb 12 '19 at 06:05) Alexey Kulakov Alexey%20Kulakov's gravatar image

Thanks, EntityChangeRegistrySize can really help. Why the default value is set to 256, not 1000000 or int.MaxValue? What the problems will I face if I change it to some big value like 1000000?

(Feb 13 '19 at 01:40) Platonov Platonov's gravatar image

Because it should have reasonable size to have persist operation distributed in time even if you have only creations. The bigger registry size the longer persist operation will be because they will have more changes registered. So with huge sized you may have one persist on transaction committing containing all changes.

(Feb 14 '19 at 07:35) Alexey Kulakov Alexey%20Kulakov's gravatar image

Imagine you created 500 000 entities with registry size of 1000000. You would have one persist on transaction committing. By default it would be at least 500 000/32 = 15625 batches. Before actual INSERTs persister does some work and organizes all changes according to dependency graph - it arranges inserts in certain way to not break foreign key constraints. All this work would have to be done for 500 000 entities at ones. It may cause performance decrease.

(Feb 14 '19 at 07:46) Alexey Kulakov Alexey%20Kulakov's gravatar image

To prevent this you would have to split these changes into portions by manual saving of change by executing Session.SaveChanges();

(Feb 14 '19 at 07:52) Alexey Kulakov Alexey%20Kulakov's gravatar image

Thanks for your answer Alexey. Unfortunately, this workaround is not applicable because reference field is initialized inside the constructor of base class for huge hierarchy, so it is always set first, before all other fields. Speaking generally, it looks like a bug of ORM that setting field can cause persisting of partially filled entity. Having two or more paired non nullable fields makes it impossible to safely initialize instance even with parametrized constructor.

Using Main function similar to previous example with the following model shows that it is not possible to safely create instanse of LinkEntity:

[HierarchyRoot]
public class EntityA : Entity
{
    public EntityA(Guid id) : base(id) { }

    [Field]
    [Key]
    public Guid Id { get; set; }

    [Field]
    public EntitySet<LinkEntity> LinkedEntities { get; set; }

    [Field]
    [Version]
    public DateTime Version { get; set; }
}

[HierarchyRoot]
public class EntityB : Entity
{
    public EntityB(Guid id) : base(id) { }

    [Field]
    [Key]
    public Guid Id { get; set; }

    [Field]
    public EntitySet<LinkEntity> LinkedEntities { get; set; }

    [Field]
    [Version]
    public DateTime Version { get; set; }
}

[HierarchyRoot]
public class LinkEntity : Entity
{
    public LinkEntity(Guid id, EntityA a, EntityB b) : base(id)
    {
        EntityA = a;
        EntityB = b;
    }

    [Field]
    [Key]
    public Guid Id { get; set; }

    [Field(Nullable = false)]
    [Association(PairTo = nameof(Sample.EntityA.LinkedEntities))]
    public EntityA EntityA { get; set; }

    [Field(Nullable = false)]
    [Association(PairTo = nameof(Sample.EntityB.LinkedEntities))]
    public EntityB EntityB { get; set; }
}

Using NotNullConstraintAttribute really solves the problem, but it is much more suitable not weaken database consistency checks

answered Feb 08 '19 at 17:24

Platonov's gravatar image

Platonov
5778

edited Feb 08 '19 at 17:37

It does queries but when it needs to. If I knew that such field initialization performed within a constructor I would recommend you to wrap constructor body with using (session.DisableSaveChanges()) {...}. Such operation delays persist but does not forbid select queries.

(Feb 11 '19 at 04:23) Alexey Kulakov Alexey%20Kulakov's gravatar image

So every time I change some field from nullable to notnullable or add paired association, I have to look through all my codebase with reflection and referencing assemblies and reorder or wrap field initialization? If I understand you correctly, almost every entity initialization now must be performed inside session.DisableSaveChanges() block. If it's true, you should mention it in your docs and think about backward compatibility.

Why can't you persist changes just before first query or when it's clear that entity state is valid?

answered Feb 11 '19 at 05:01

Platonov's gravatar image

Platonov
5778

You asked for a workaround and I'm trying to give it to you to prevent you from getting stuck while we are working. At least we need time to investigate the issue and if it is then fixed it. It will take time anyway. Please answer by comment to my comments - we are notified about answers and comments both

(Feb 11 '19 at 09:39) Alexey Kulakov Alexey%20Kulakov's gravatar image

Okey, thanks for your answers. Unfortunately, these workarounds are not applicable for my codebase. Looking forward for your fixes

(Feb 12 '19 at 00:29) Platonov Platonov's gravatar image

I have updated my first answer. Please check it out

(Feb 12 '19 at 06:06) Alexey Kulakov Alexey%20Kulakov's gravatar image
Your answer
Please start posting your answer anonymously - your answer will be saved within the current session and published after you log in or create a new account. Please try to give a substantial answer, for discussions, please use comments and please do remember to vote (after you log in)!
toggle preview

powered by OSQA