mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
EF: Fixing multiple same-entity references
This commit is contained in:
parent
6566b717f6
commit
928a8d2147
@ -4,6 +4,7 @@ using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Kyoo.Models.Exceptions;
|
||||
@ -503,6 +504,23 @@ namespace Kyoo
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return the first resource with the given slug that is currently tracked by this context.
|
||||
/// This allow one to limit redundant calls to <see cref="IRepository{T}.CreateIfNotExists"/> during the
|
||||
/// same transaction and prevent fails from EF when two same entities are being tracked.
|
||||
/// </summary>
|
||||
/// <param name="slug">The slug of the resource to check</param>
|
||||
/// <typeparam name="T">The type of entity to check</typeparam>
|
||||
/// <returns>The local entity representing the resource with the given slug if it exists or null.</returns>
|
||||
[CanBeNull]
|
||||
public T LocalEntity<T>(string slug)
|
||||
where T : class, IResource
|
||||
{
|
||||
return ChangeTracker.Entries<T>()
|
||||
.FirstOrDefault(x => x.Entity.Slug == slug)
|
||||
?.Entity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the exception is a duplicated exception.
|
||||
/// </summary>
|
||||
@ -515,14 +533,12 @@ namespace Kyoo
|
||||
/// </summary>
|
||||
private void DiscardChanges()
|
||||
{
|
||||
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
|
||||
&& x.State != EntityState.Detached))
|
||||
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Detached))
|
||||
{
|
||||
entry.State = EntityState.Detached;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Perform a case insensitive like operation.
|
||||
/// </summary>
|
||||
|
@ -71,9 +71,9 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
||||
}
|
||||
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
|
@ -170,9 +170,9 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
||||
}
|
||||
_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
|
@ -62,7 +62,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
obj.ExternalIDs.ForEach(x => _database.MetadataIds<People>().Attach(x));
|
||||
await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists).");
|
||||
return obj;
|
||||
}
|
||||
@ -71,23 +70,35 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(People resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
||||
|
||||
if (resource.ExternalIDs != null)
|
||||
{
|
||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
_database.Entry(id.Provider).State = EntityState.Detached;
|
||||
});
|
||||
await resource.Roles.ForEachAsync(async role =>
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
}
|
||||
_database.MetadataIds<People>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
|
||||
if (resource.Roles != null)
|
||||
{
|
||||
role.Show = await _shows.Value.CreateIfNotExists(role.Show);
|
||||
role.ShowID = role.Show.ID;
|
||||
_database.Entry(role.Show).State = EntityState.Detached;
|
||||
});
|
||||
foreach (PeopleRole role in resource.Roles)
|
||||
{
|
||||
role.Show = _database.LocalEntity<Show>(role.Show.Slug)
|
||||
?? await _shows.Value.CreateIfNotExists(role.Show);
|
||||
role.ShowID = role.Show.ID;
|
||||
_database.Entry(role).State = EntityState.Added;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override async Task EditRelations(People resource, People changed, bool resetOld)
|
||||
{
|
||||
await Validate(changed);
|
||||
|
||||
if (changed.Roles != null || resetOld)
|
||||
{
|
||||
await Database.Entry(resource).Collection(x => x.Roles).LoadAsync();
|
||||
@ -98,9 +109,7 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
|
||||
resource.ExternalIDs = changed.ExternalIDs;
|
||||
|
||||
}
|
||||
await base.EditRelations(resource, changed, resetOld);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -107,9 +107,9 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
||||
}
|
||||
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
|
@ -102,9 +102,9 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
_database.Entry(id.Provider).State = EntityState.Detached;
|
||||
}
|
||||
_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
@ -113,9 +113,9 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
foreach (PeopleRole role in resource.People)
|
||||
{
|
||||
role.People = await _people.CreateIfNotExists(role.People);
|
||||
role.People = _database.LocalEntity<People>(role.People.Slug)
|
||||
?? await _people.CreateIfNotExists(role.People);
|
||||
role.PeopleID = role.People.ID;
|
||||
_database.Entry(role.People).State = EntityState.Detached;
|
||||
_database.Entry(role).State = EntityState.Added;
|
||||
}
|
||||
}
|
||||
|
@ -54,7 +54,6 @@ namespace Kyoo.Controllers
|
||||
{
|
||||
await base.Create(obj);
|
||||
_database.Entry(obj).State = EntityState.Added;
|
||||
obj.ExternalIDs.ForEach(x => _database.MetadataIds<Studio>().Attach(x));
|
||||
await _database.SaveChangesAsync($"Trying to insert a duplicated studio (slug {obj.Slug} already exists).");
|
||||
return obj;
|
||||
}
|
||||
@ -63,12 +62,16 @@ namespace Kyoo.Controllers
|
||||
protected override async Task Validate(Studio resource)
|
||||
{
|
||||
await base.Validate(resource);
|
||||
await resource.ExternalIDs.ForEachAsync(async x =>
|
||||
{
|
||||
x.Provider = await _providers.CreateIfNotExists(x.Provider);
|
||||
x.ProviderID = x.Provider.ID;
|
||||
_database.Entry(x.Provider).State = EntityState.Detached;
|
||||
});
|
||||
if (resource.ExternalIDs != null)
|
||||
{
|
||||
foreach (MetadataID id in resource.ExternalIDs)
|
||||
{
|
||||
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||
?? await _providers.CreateIfNotExists(id.Provider);
|
||||
id.ProviderID = id.Provider.ID;
|
||||
}
|
||||
_database.MetadataIds<Studio>().AttachRange(resource.ExternalIDs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
@ -1,5 +1,9 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Kyoo.Controllers;
|
||||
using Kyoo.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
@ -33,5 +37,134 @@ namespace Kyoo.Tests.Database
|
||||
{
|
||||
_repository = Repositories.LibraryManager.PeopleRepository;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CreateWithExternalIdTest()
|
||||
{
|
||||
People value = TestSample.GetNew<People>();
|
||||
value.ExternalIDs = new[]
|
||||
{
|
||||
new MetadataID
|
||||
{
|
||||
Provider = TestSample.Get<Provider>(),
|
||||
Link = "link",
|
||||
DataID = "id"
|
||||
},
|
||||
new MetadataID
|
||||
{
|
||||
Provider = TestSample.GetNew<Provider>(),
|
||||
Link = "new-provider-link",
|
||||
DataID = "new-id"
|
||||
}
|
||||
};
|
||||
await _repository.Create(value);
|
||||
|
||||
People retrieved = await _repository.Get(2);
|
||||
await Repositories.LibraryManager.Load(retrieved, x => x.ExternalIDs);
|
||||
Assert.Equal(2, retrieved.ExternalIDs.Count);
|
||||
KAssert.DeepEqual(value.ExternalIDs.First(), retrieved.ExternalIDs.First());
|
||||
KAssert.DeepEqual(value.ExternalIDs.Last(), retrieved.ExternalIDs.Last());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditTest()
|
||||
{
|
||||
People value = await _repository.Get(TestSample.Get<People>().Slug);
|
||||
value.Name = "New Name";
|
||||
value.Images = new Dictionary<int, string>
|
||||
{
|
||||
[Images.Poster] = "new-poster"
|
||||
};
|
||||
await _repository.Edit(value, false);
|
||||
|
||||
await using DatabaseContext database = Repositories.Context.New();
|
||||
People retrieved = await database.People.FirstAsync();
|
||||
|
||||
KAssert.DeepEqual(value, retrieved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EditMetadataTest()
|
||||
{
|
||||
People value = await _repository.Get(TestSample.Get<People>().Slug);
|
||||
value.ExternalIDs = new[]
|
||||
{
|
||||
new MetadataID
|
||||
{
|
||||
Provider = TestSample.Get<Provider>(),
|
||||
Link = "link",
|
||||
DataID = "id"
|
||||
},
|
||||
};
|
||||
await _repository.Edit(value, false);
|
||||
|
||||
await using DatabaseContext database = Repositories.Context.New();
|
||||
People retrieved = await database.People
|
||||
.Include(x => x.ExternalIDs)
|
||||
.ThenInclude(x => x.Provider)
|
||||
.FirstAsync();
|
||||
|
||||
KAssert.DeepEqual(value, retrieved);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task AddMetadataTest()
|
||||
{
|
||||
People value = await _repository.Get(TestSample.Get<People>().Slug);
|
||||
value.ExternalIDs = new List<MetadataID>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Provider = TestSample.Get<Provider>(),
|
||||
Link = "link",
|
||||
DataID = "id"
|
||||
},
|
||||
};
|
||||
await _repository.Edit(value, false);
|
||||
|
||||
{
|
||||
await using DatabaseContext database = Repositories.Context.New();
|
||||
People retrieved = await database.People
|
||||
.Include(x => x.ExternalIDs)
|
||||
.ThenInclude(x => x.Provider)
|
||||
.FirstAsync();
|
||||
|
||||
KAssert.DeepEqual(value, retrieved);
|
||||
}
|
||||
|
||||
value.ExternalIDs.Add(new MetadataID
|
||||
{
|
||||
Provider = TestSample.GetNew<Provider>(),
|
||||
Link = "link",
|
||||
DataID = "id"
|
||||
});
|
||||
await _repository.Edit(value, false);
|
||||
|
||||
{
|
||||
await using DatabaseContext database = Repositories.Context.New();
|
||||
People retrieved = await database.People
|
||||
.Include(x => x.ExternalIDs)
|
||||
.ThenInclude(x => x.Provider)
|
||||
.FirstAsync();
|
||||
|
||||
KAssert.DeepEqual(value, retrieved);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Me")]
|
||||
[InlineData("me")]
|
||||
[InlineData("na")]
|
||||
public async Task SearchTest(string query)
|
||||
{
|
||||
People value = new()
|
||||
{
|
||||
Slug = "slug",
|
||||
Name = "name",
|
||||
};
|
||||
await _repository.Create(value);
|
||||
ICollection<People> ret = await _repository.Search(query);
|
||||
KAssert.DeepEqual(value, ret.First());
|
||||
}
|
||||
}
|
||||
}
|
@ -104,6 +104,20 @@ namespace Kyoo.Tests
|
||||
[Images.Logo] = "logo"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
typeof(People),
|
||||
() => new People
|
||||
{
|
||||
ID = 2,
|
||||
Slug = "new-person-name",
|
||||
Name = "New person name",
|
||||
Images = new Dictionary<int, string>
|
||||
{
|
||||
[Images.Logo] = "Old Logo",
|
||||
[Images.Poster] = "Old poster"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user