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

Same reference type instance shared by multiple entities is only saved to the last entity's JSON column #32799

Closed
tanemann opened this issue Jan 12, 2024 · 4 comments
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported

Comments

@tanemann
Copy link

It seems that when using JSON columns, if you use a single shared instance of a reference type for multiple entities and then call SaveChanges, only the last row will actually include the serialized JSON in the SQL INSERT. This happens in EF7 and EF8, using either SQL Server or SQLite.

Simple data model:

public class Blog
{
    public int BlogId { get; set; }
    public string Url { get; set; }
    public Writer? Author { get; set; } 
}
 
public class Writer
{
    public string Name { get; set; }
    public int Age { get; set; }
}

Using JSON column for Author:

public class BloggingContext : DbContext
{
    // ...

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.Entity<Blog>()
            .OwnsOne(av => av.Author, builder =>
            {
                builder.ToJson();
            });
    }
}

Adding blogs with identical Author using a shared instance of Writer:

var author = new Writer { Name = "Doria", Age = 20 };

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/1",
    Author = author,
});

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/2",
    Author = author,
});

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/3",
    Author = author,
});

db.SaveChanges();

Output to console when console logging enabled:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[@p0='http://blogs.msdn.com/adonet/1' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Url")
      VALUES (@p0)
      RETURNING "BlogId";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='http://blogs.msdn.com/adonet/2' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Url")
      VALUES (@p0)
      RETURNING "BlogId";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='{"Age":20,"Name":"Doria"}' (Nullable = false) (Size = 25), @p1='http://blogs.msdn.com/adonet/3' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Author", "Url")
      VALUES (@p0, @p1)
      RETURNING "BlogId";

Note that only the last INSERT included the serialized Author JSON.

For comparison, the issue does not occur when each Blog uses their own Writer instance:

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/1",
    Author = new Writer { Name = "Doria", Age = 20 },
});

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/2",
    Author = new Writer { Name = "Doria", Age = 20 },
});

db.Add(new Blog
{
    Url = "http://blogs.msdn.com/adonet/3",
    Author = new Writer { Name = "Doria", Age = 20 },
});

db.SaveChanges();

Output:

info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (6ms) [Parameters=[@p0='{"Age":20,"Name":"Doria"}' (Nullable = false) (Size = 25), @p1='http://blogs.msdn.com/adonet/1' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Author", "Url")
      VALUES (@p0, @p1)
      RETURNING "BlogId";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='{"Age":20,"Name":"Doria"}' (Nullable = false) (Size = 25), @p1='http://blogs.msdn.com/adonet/2' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Author", "Url")
      VALUES (@p0, @p1)
      RETURNING "BlogId";
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[@p0='{"Age":20,"Name":"Doria"}' (Nullable = false) (Size = 25), @p1='http://blogs.msdn.com/adonet/3' (Nullable = false) (Size = 30)], CommandType='Text', CommandTimeout='30']
      INSERT INTO "Blogs" ("Author", "Url")
      VALUES (@p0, @p1)
      RETURNING "BlogId";
@ajcvickers
Copy link
Member

@tanemann EF doesn't support sharing the same instance of an entity type in multiple places. Complex types will be more appropriate for this case once fully implemented.

@tanemann
Copy link
Author

tanemann commented Jan 22, 2024

@ajcvickers thanks for the reply. I'm still not quite sure why EF is behaving the way that it is in the described scenario. The Blog Authors in this case are not meant to be relational, but stored individually for each Blog entity (as JSON). After saving the entities, there should no longer be any dependency between the Author columns in the DB, or the Writer instances that are created when reading/querying Blog entities later on. I admit the overly simplified example is a bit misleading, since in a real-life situation the Blog Author would obviously be a relation between the Blog entity and a Writer, which would in that case also be an entity.

In any case, even if EF is not meant to support using a shared instance for a JSON column property, I don't think the current behaviour is at all rational. EF should throw an error, or at least somekind of a warning if attempting to use a shared instance, rather than just happily saving the property only for the last entity that the instance is assigned to. The current implementation leads to data loss without giving any kind of a sign that that is happening, which is something that should be avoided at all cost.

@ajcvickers
Copy link
Member

I don't think the current behaviour is at all rational. EF should throw an error, or at least somekind of a warning if attempting to use a shared instance

This has been discussed many times in the past. Doing so would require that we do significant extra work (and this will need additional data structures for tracking) to detect a case which is programmer error, and so should never occur outside of bugs in the application code. We came down on the side of perf for correct code in this case. Also, keep in mind that, as I mentioned above, mapping entity types to JSON is, perhaps, the real issue here. Complex types are a much better semantic match.

@tanemann
Copy link
Author

Also, keep in mind that, as I mentioned above, mapping entity types to JSON is, perhaps, the real issue here. Complex types are a much better semantic match.

I think you're absolutely right, complex types do indeed seem align much better with our needs. Until #31252 is implemented, I think our options are to either keep using owned entities, while being more careful with property assignments or just use a json-serializing value converter, since we don't really need json-querying capabilities for the time being.

Thanks for your help and valuable input!

@ajcvickers ajcvickers closed this as not planned Won't fix, can't repro, duplicate, stale Jan 22, 2024
@ajcvickers ajcvickers added the closed-no-further-action The issue is closed and no further action is planned. label Jan 22, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
closed-no-further-action The issue is closed and no further action is planned. customer-reported
Projects
None yet
Development

No branches or pull requests

2 participants