EF Core - obsługa współbieżności

0

Mam projekt, w którym w modelu dodałem:

        [Timestamp]
        public byte[] RowVersion { get; set; }

W bazie danych utworzona jest kolumna RowVersion, która przy każdej aktualizacji rekordu ulega zmianie.
Jeżeli teraz próbuję w dwóch odrębnych sesjach modyfikować ten sam rekord to zawsze zachowywana jest jego ostatnia wartość. Nigdy nie jest rzucany DBConcurrencyException bez względu nawet na to czy ten model zawiera RowVersion w zwracanym modelu.

Czy w EF Core obsługę współbieżności trzeba w jakikolwiek sposób konfigurować, żeby działała?

0

Pokaż jak zapisujesz

0

Zachowuje się prawidłowo. Relacyjne Bazy Danych nie obsługują asychronicznego zapisu jak np. MongoDb. ORM powinien rzucić wyjątek ze względu na nie atomowość operacji. Czyli np. jeśli przy dużym latency dwa wątki z tą samą sesją będą chciały wykonać na raz tranzakcję zostanie rzucony wyjątek. Dlatego stosuje się TPL przy zapisie.

0
baroo napisał(a):

Czy w EF Core obsługę współbieżności trzeba w jakikolwiek sposób konfigurować, żeby działała?

Oczywiście. Musisz ORMowi powiedzieć, że dane property jest wersją rekordu.

https://docs.microsoft.com/en-us/ef/core/modeling/concurrency#how-concurrency-tokens-work-in-ef

0

Jedynie adnotacja timestamp i propertis jest potrzebne do optymistycznego rozwiązywania konfliktów współbieżnych w EF core. Więc albo problem jest w Twoim kodzie, albo rozumowaniu jak to ma działać, tutaj przykład rzucający wyjątkiem:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;

namespace ConsoleApp10
{

    public class BloggingContext :DbContext
    {
        public DbSet<Blog> Blogs { get; set; }
       

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;");
        }
    }

    public class Blog
    {
        public int BlogId { get; set; }
        public string Url { get; set; }

        [Timestamp]
        public byte[] Timestamp { get; set; }
    }

   






    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");


            using (var db = new BloggingContext())
            {
                db.Blogs.Add(new Blog { Url = "http://blogs.msdn.com/adonet" });
                db.SaveChanges();               
            }

            Blog blog = null;

            // zaczynamy edycję bloga
            using (var db = new BloggingContext())
            {
                blog = await db.Blogs.FirstOrDefaultAsync();                
            }

            /// O nie ktoś nam za plecami majstruje z naszą encją! 
            using (var db = new BloggingContext())
            {
                var blog2 = await db.Blogs.FirstOrDefaultAsync();
                blog2.Url = DateTime.UtcNow.ToString();
                db.SaveChanges();
            }

            // kończymy edycję bloga
            using (var db = new BloggingContext())
            {
                db.Blogs.Attach(blog);
                blog.Url += "+";

                try
                {
                    db.SaveChanges();
                } catch (Microsoft.EntityFrameworkCore.DbUpdateConcurrencyException ex)
                {
                    Console.WriteLine("DBConcurrencyException");
                }
            }
        }
    }
}


0

Ok, dzięki. Chyba już wiem gdzie robię błąd w swoim kodzie.

0

Czy taki kod operacji update zapewni poprawne śledzenie danej encji?

        // PUT api/OneTimeServiceItems/5
        [HttpPut("{id}")]
        public async Task<IActionResult> Put(int id, [FromBody]OneTimeServiceItemDto updatedOneTimeServiceItemDto)
        {
            if (!ModelState.IsValid || updatedOneTimeServiceItemDto.Id != id)
                return BadRequest();

            var oneTimeServiceItems = await _context.ServiceItems.OfType<OneTimeServiceItem>().Where(g => g.Id == id).ToListAsync();

            if (!oneTimeServiceItems.Any())
                return NotFound();

            var updatedOneTimeServiceItem = oneTimeServiceItems.First();
            Mapper.Map(updatedOneTimeServiceItemDto, updatedOneTimeServiceItem);

            try
            {
                _context.ServiceItems.Update(updatedOneTimeServiceItem);
                await _context.SaveChangesAsync();
                _cache.Remove(IMemoryCacheKeys.customersCacheKey);
            }
            catch (Exception exception)
            {
                BadRequest(exception);
            }

            return new NoContentResult();
        }
0

O czym Ty w ogóle piszesz? Co ma asynchroniczny zapis przez bazę do optymistic concurrency, które istnieje w ORMach od lat?

Wydawało mi się, że autor bije do tego jak się zachowuje ORM podczas współbieżności a nie jak działa optimistic lock.

Poza tym EF nie powinno automatycznie robić Dirty Cheking?
Typu :

Set Name WHERE Id=1 AND Name="name"

Ja tam po prostu robie Flush przed Commitem i umnie to po prostu działa. ;)

0

Jak sobie debuguję tą akcje kontrolera to teoretycznie RowVersion jest brane pod uwagę:

UPDATE [ServiceItems] SET [CreatedAt] = @p0, [CreatedById] = @p1, [GrossValueAdded] = @p2, [IsArchived] = @p3, [IsBlocked] = @p4, [IsManual] = @p5, [IsSubNamePrinted] = @p6, [IsSuspended] = @p7, [IsValueVariable] = @p8, [Name] = @p9, [NetValue] = @p10, [Notes] = @p11, [Quantity] = @p12, [RemoteSystemServiceCode] = @p13, [ServiceCategoryType] = @p14, [ServiceItemCustomerSpecificTag] = @p15, [SpecificLocation] = @p16, [SubName] = @p17, [UpdatedAt] = @p18, [UpdatedById] = @p19, [VATRate] = @p20, [InstallationDate] = @p21, [IsInvoiced] = @p22, [ServiceItemsSetId] = @p23
WHERE [Id] = @p24 AND [RowVersion] = @p25;
SELECT [RowVersion]
FROM [ServiceItems]
WHERE @@ROWCOUNT = 1 AND [Id] = @p24;

ale nijak mi to wyjątku nie rzuca.

1 użytkowników online, w tym zalogowanych: 0, gości: 1