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.Linq.Expressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
using Kyoo.Models.Exceptions;
|
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>
|
/// <summary>
|
||||||
/// Check if the exception is a duplicated exception.
|
/// Check if the exception is a duplicated exception.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -515,14 +533,12 @@ namespace Kyoo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private void DiscardChanges()
|
private void DiscardChanges()
|
||||||
{
|
{
|
||||||
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Unchanged
|
foreach (EntityEntry entry in ChangeTracker.Entries().Where(x => x.State != EntityState.Detached))
|
||||||
&& x.State != EntityState.Detached))
|
|
||||||
{
|
{
|
||||||
entry.State = EntityState.Detached;
|
entry.State = EntityState.Detached;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Perform a case insensitive like operation.
|
/// Perform a case insensitive like operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -71,9 +71,9 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
foreach (MetadataID id in resource.ExternalIDs)
|
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;
|
id.ProviderID = id.Provider.ID;
|
||||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
|
||||||
}
|
}
|
||||||
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
|
_database.MetadataIds<Collection>().AttachRange(resource.ExternalIDs);
|
||||||
}
|
}
|
||||||
|
@ -170,9 +170,9 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
foreach (MetadataID id in resource.ExternalIDs)
|
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;
|
id.ProviderID = id.Provider.ID;
|
||||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
|
||||||
}
|
}
|
||||||
_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
|
_database.MetadataIds<Episode>().AttachRange(resource.ExternalIDs);
|
||||||
}
|
}
|
||||||
|
@ -62,7 +62,6 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
await base.Create(obj);
|
await base.Create(obj);
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
_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).");
|
await _database.SaveChangesAsync($"Trying to insert a duplicated people (slug {obj.Slug} already exists).");
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@ -71,23 +70,35 @@ namespace Kyoo.Controllers
|
|||||||
protected override async Task Validate(People resource)
|
protected override async Task Validate(People resource)
|
||||||
{
|
{
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
await resource.ExternalIDs.ForEachAsync(async id =>
|
|
||||||
|
if (resource.ExternalIDs != null)
|
||||||
{
|
{
|
||||||
id.Provider = await _providers.CreateIfNotExists(id.Provider);
|
foreach (MetadataID id in resource.ExternalIDs)
|
||||||
id.ProviderID = id.Provider.ID;
|
{
|
||||||
_database.Entry(id.Provider).State = EntityState.Detached;
|
id.Provider = _database.LocalEntity<Provider>(id.Provider.Slug)
|
||||||
});
|
?? await _providers.CreateIfNotExists(id.Provider);
|
||||||
await resource.Roles.ForEachAsync(async role =>
|
id.ProviderID = id.Provider.ID;
|
||||||
|
}
|
||||||
|
_database.MetadataIds<People>().AttachRange(resource.ExternalIDs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resource.Roles != null)
|
||||||
{
|
{
|
||||||
role.Show = await _shows.Value.CreateIfNotExists(role.Show);
|
foreach (PeopleRole role in resource.Roles)
|
||||||
role.ShowID = role.Show.ID;
|
{
|
||||||
_database.Entry(role.Show).State = EntityState.Detached;
|
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 />
|
/// <inheritdoc />
|
||||||
protected override async Task EditRelations(People resource, People changed, bool resetOld)
|
protected override async Task EditRelations(People resource, People changed, bool resetOld)
|
||||||
{
|
{
|
||||||
|
await Validate(changed);
|
||||||
|
|
||||||
if (changed.Roles != null || resetOld)
|
if (changed.Roles != null || resetOld)
|
||||||
{
|
{
|
||||||
await Database.Entry(resource).Collection(x => x.Roles).LoadAsync();
|
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();
|
await Database.Entry(resource).Collection(x => x.ExternalIDs).LoadAsync();
|
||||||
resource.ExternalIDs = changed.ExternalIDs;
|
resource.ExternalIDs = changed.ExternalIDs;
|
||||||
|
|
||||||
}
|
}
|
||||||
await base.EditRelations(resource, changed, resetOld);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
@ -107,9 +107,9 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
foreach (MetadataID id in resource.ExternalIDs)
|
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;
|
id.ProviderID = id.Provider.ID;
|
||||||
_database.Entry(id.Provider).State = EntityState.Unchanged;
|
|
||||||
}
|
}
|
||||||
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
|
_database.MetadataIds<Season>().AttachRange(resource.ExternalIDs);
|
||||||
}
|
}
|
||||||
|
@ -102,9 +102,9 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
foreach (MetadataID id in resource.ExternalIDs)
|
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;
|
id.ProviderID = id.Provider.ID;
|
||||||
_database.Entry(id.Provider).State = EntityState.Detached;
|
|
||||||
}
|
}
|
||||||
_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
|
_database.MetadataIds<Show>().AttachRange(resource.ExternalIDs);
|
||||||
}
|
}
|
||||||
@ -113,9 +113,9 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
foreach (PeopleRole role in resource.People)
|
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;
|
role.PeopleID = role.People.ID;
|
||||||
_database.Entry(role.People).State = EntityState.Detached;
|
|
||||||
_database.Entry(role).State = EntityState.Added;
|
_database.Entry(role).State = EntityState.Added;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,6 @@ namespace Kyoo.Controllers
|
|||||||
{
|
{
|
||||||
await base.Create(obj);
|
await base.Create(obj);
|
||||||
_database.Entry(obj).State = EntityState.Added;
|
_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).");
|
await _database.SaveChangesAsync($"Trying to insert a duplicated studio (slug {obj.Slug} already exists).");
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@ -63,12 +62,16 @@ namespace Kyoo.Controllers
|
|||||||
protected override async Task Validate(Studio resource)
|
protected override async Task Validate(Studio resource)
|
||||||
{
|
{
|
||||||
await base.Validate(resource);
|
await base.Validate(resource);
|
||||||
await resource.ExternalIDs.ForEachAsync(async x =>
|
if (resource.ExternalIDs != null)
|
||||||
{
|
{
|
||||||
x.Provider = await _providers.CreateIfNotExists(x.Provider);
|
foreach (MetadataID id in resource.ExternalIDs)
|
||||||
x.ProviderID = x.Provider.ID;
|
{
|
||||||
_database.Entry(x.Provider).State = EntityState.Detached;
|
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 />
|
/// <inheritdoc />
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Kyoo.Controllers;
|
using Kyoo.Controllers;
|
||||||
using Kyoo.Models;
|
using Kyoo.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
|
|
||||||
@ -33,5 +37,134 @@ namespace Kyoo.Tests.Database
|
|||||||
{
|
{
|
||||||
_repository = Repositories.LibraryManager.PeopleRepository;
|
_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"
|
[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