asp.net mvc - Entity Framework agrega registro dos veces

CorePress2024-01-24  14

Esta es mi primera publicación en Stack, así que lamento cualquier inconveniente o mi mal inglés. Tengo un problema con Entity Framework SaveChanges() al agregar un nuevo registro de usuario dos veces a la tabla de la base de datos.

He creado un UserFormViewModel para publicar datos basados ​​en las entradas del usuario desde el formulario de ventana modal.

Este es mi UserFormViewModel:

    public int Id { get; set; }

    [Required]
    [StringLength(50)]
    public string Username { get; set; }

    [Required]
    [StringLength(20)]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [Required]
    [StringLength(50)]
    public string Name { get; set; }

    [Required]
    [StringLength(50)]
    public string Surname { get; set; }

    [Required]
    [StringLength(255)]
    [EmailAddress]
    public string Email { get; set; }

    public byte UserRoleId { get; set; }
    public Address Address { get; set; }

    public IEnumerable<UserRole> UserRoles { get; set; }

Todas las entradas de la vista se asignan correctamente al parámetro del método de acción AddOrEdit, por lo que el problema comienza al guardar los datos en la base de datos mediante el método SaveChanges(). Está agregando registros dos veces a la tabla.

Intenté agregar Usuario por separado y lo agrega dos veces. Luego intenté agregar la dirección por separado y agrega la dirección dos veces. Al guardar Usuario y Dirección en una instancia de dbContext o en dos dbContext separadosEn algunos casos, el Usuario y la Dirección se agregan nuevamente dos veces.

También probé el primer método de base de datos, pero los resultados son los mismos.

Supongo que tiene que ver con las relaciones entre las tablas, pero no puedo señalar la causa correcta.

En la clase de modelo Dirección hay propiedades estándar para la dirección postal:

 Id, StreetName, StreetNumber, Zip, City 

y dos claves foráneas:

    public int? UserId { get; set; }
    public int? ShopId { get; set; }

    public virtual User User { get; set; }
    public virtual Shop Shop { get; set; }

Por lo tanto, el usuario y la tienda comparten la tabla de direcciones para sus direcciones.

Otra forma de evitarlo, cuando Usuario y Tienda están configurados para hacer referencia a la tabla de direcciones, todo funciona bien.

Pero quiero entender por qué esto no funciona y, al final, hacer que la Dirección pueda conectarse en cascada al eliminar el Usuario o la Tienda en lugar de usar LINQ para eliminar la Dirección.

Aquí está el método de acción AddOrEdit para un usuario y su dirección:

private readonly FurnitureStoreDbContext _context;
    
public UsersController()
{
    _context = new FurnitureStoreDbContext();
}

protected override void Dispose(bool disposing)
{
    _context.Dispose();
}

[HttpPost]
public ActionResult AddOrEdit(UserFormViewModel userForm)
{
    if (userForm.Id == 0)
    {
        var newUser = new User
                          {
                              Username = userForm.Username,
                              Password = userForm.Password,
                              Name = userForm.Name,
                              Surname = userForm.Surname,
                              Email = userForm.Email,
                              UserRoleId = userForm.UserRoleId
                          };

        _context.tblUsers.Add(newUser);

        _context.SaveChanges();

        var newAddress = new Address
            {
                StreetName = userForm.Address.StreetName,
                StreetNumber = userForm.Address.StreetNumber,
                ZipCode = userForm.Address.ZipCode,
                City = userForm.Address.City,
                UserId = newUser.Id
            };

        _context.tblStreetAddresses.Add(newAddress);
        _context.SaveChanges();

        return Json(new { success = true, message = "Saved" }, JsonRequestBehavior.AllowGet);
    }
    else
    {
        var updatedUser = new User
            {
                Username = userForm.Username,
                Password = userForm.Password,
                Name = userForm.Name,
                Surname = userForm.Surname,
                Email = userForm.Email,
                UserRoleId = userForm.UserRoleId
            };

        var updatedAddress = userForm.Address;

        _context.Entry(updatedUser).State = EntityState.Modified;
        _context.SaveChanges();

        _context.Entry(updatedAddress).State = EntityState.Modified;
        _context.SaveChanges();

        return Json(new { success = true, message = "Updated" }, JsonRequestBehavior.AllowGet);
    }
}

Aquí hay un registro de depuración para agregar únicamentey Usuario - sin agregar Dirección.

La violación de la restricción UNIQUE KEY 'UQ__Users__536C85E479927847' ocurre porque configuré el campo Nombre de usuario como único y el contexto está intentando agregar el registro de usuario dos veces.

Step into: Stepping over property 'FurnitureStore.Models.User.get_Addresses'. To step into properties or operators, go to Tools->Options->Debugging and uncheck 'Step over properties and operators (Managed only)'.
Step into: Stepping over property 'FurnitureStore.Models.User.get_Addresses'. To step into properties or operators, go to Tools->Options->Debugging and uncheck 'Step over properties and operators (Managed only)'.
Opened connection at 3/27/2021 8:55:09 PM +01:00
Opened connection at 3/27/2021 8:55:09 PM +01:00
Started transaction at 3/27/2021 8:55:09 PM +01:00
Started transaction at 3/27/2021 8:55:09 PM +01:00
INSERT [dbo].[Users]([Username], [Password], [Name], [Surname], [Email], [UserRoleId])
VALUES (@0, @1, @2, @3, @4, @5)
SELECT [Id]
FROM [dbo].[Users]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()INSERT [dbo].[Users]([Username], [Password], [Name], [Surname], [Email], [UserRoleId])
VALUES (@0, @1, @2, @3, @4, @5)
SELECT [Id]
FROM [dbo].[Users]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()

-- @0: 'Username 1' (Type = String, Size = 50)
-- @0: 'Username 1' (Type = String, Size = 50)
-- @1: 'q1w2e3r4' (Type = String, Size = 20)
-- @2: 'Name 1' (Type = String, Size = 50)
-- @3: 'Surname 1' (Type = String, Size = 50)
-- @4: '[email protected]' (Type = String, Size = 255)
-- @5: '2' (Type = Byte, Size = 1)
-- Executing at 3/27/2021 8:55:10 PM +01:00
-- @1: 'q1w2e3r4' (Type = String, Size = 20)
-- @2: 'Name 1' (Type = String, Size = 50)
-- @3: 'Surname 1' (Type = String, Size = 50)
-- @4: '[email protected]' (Type = String, Size = 255)
-- Completed in 7 ms with result: SqlDataReader

-- @5: '2' (Type = Byte, Size = 1)
-- Executing at 3/27/2021 8:55:10 PM +01:00
Committed transaction at 3/27/2021 8:55:10 PM +01:00
-- Failed in 26 ms with error: Violation of UNIQUE KEY constraint 'UQ__Users__536C85E479927847'. Cannot insert duplicate key in object 'dbo.Users'. The duplicate key value is (Username 1).
The statement has been terminated.

Closed connection at 3/27/2021 8:55:10 PM +01:00
Closed connection at 3/27/2021 8:55:10 PM +01:00
Exception thrown: 'System.Data.Entity.Infrastructure.DbUpdateException' in EntityFramework.dll
An exception of type 'System.Data.Entity.Infrastructure.DbUpdateException' occurred in EntityFramework.dll but was not handled in user code
An error occurred while updating the entries. See the inner exception for details.

The thread 0x42cc has exited with code 0 (0x0).

Se agradece cualquier ayuda.

Gracias :)

Ese código se ve bien. Probablemente esté ejecutando el controlador dos veces o ya tenga ese usuario en su base de datos. Y tienes un error en la opción de "actualización". rama en el sentido de que no configura el ID del usuario en la entidad antes de intentar actualizar.

-David Browne - Microsoft

27/03/2021 a las 20:56

updatedUser no tiene un valor de Id establecido (mientras que la tabla de la base de datos tiene claramente el campo). Eso debería causar problemas al actualizar, pero no es necesario volver a insertarlo tal como lo codificaste.

- Gert Arnold

27/03/2021 a las 21:25



------------------------------------

Recomendaría dividir la adición y actualización en acciones separadas, ya que es fácil configurar el enlace apropiado en la interfaz de usuario a través de Razor, etc.

En cualquier caso, su problema está en la lógica de actualización. Cuando trabaje con EF DbContexts, debe buscar recuperar la entidad al actualizar. Por ejemplo:

var existingUser = _context.Users
    .Include(x => x.Address)
    .Single(x => x.UserId == userForm.Id);

Desde aquí, copie los campos de su formulario de usuario que permita editar. Por ejemplo si pueden cambiar su nombre, apellido y correo electrónico:

existingUser.Name = userForm.Name;
existingUser.Surname = userForm.Surname;
existingUser.Email = userForm.Email;

El beneficio de este enfoque es que EF solo generará una instrucción UPDATE para los campos que realmente cambian, y solo si al menos uno realmente cambia. Usar el método Update() o establecer el estado de una entidad en Modificado dará como resultado una declaración ACTUALIZAR para todas las columnas, incluso si nola cosa realmente cambió.

El próximo número será con referencias. Cuando se utilizan modelos de vista, las entidades nunca deben pasarse a la vista. Un código como este suele ser problemático:

var updatedAddress = userForm.Address;
_context.Entry(updatedUser).State = EntityState.Modified;

Esto hace muchas suposiciones peligrosas de que userForm.Address apunta a un registro válido y que DbContext aún no está rastreando una entidad con ese ID.

En la declaración de ejemplo anterior que buscó al usuario de DbContext, cargué ansiosamente la dirección. De esta manera, en lugar de pasar una entidad de Dirección en su modelo de vista UserForm, pase un modelo de vista POCO para los detalles de la dirección y podrá usarlo para editar o insertar una dirección según sea necesario:

if (existingUser.Address != null)
{
    existingUser.Address.AddressLine1 = userForm.Addres.AddressLine1;
    // ...
}
else
{
    var address = new Address
    {
        AddressLine1 = userForm.Addres.AddressLine1;
        // ...
    };
    existingUser.Address = address;
}

Es muy importante obtener referencias cuando se trata de relaciones de muchos a muchos o de muchos a uno, como cuando se asocian usuarios aTiendas. Desde la perspectiva del usuario, al asociarse a una tienda, la entidad Shop ya debería existir, por lo que recuperarla de DbContext por ID sirve como validación de que ShopId es válido y también evita situaciones en las que termines insertando filas duplicadas sin querer.

Por último, llame a SaveChanges() solo una vez. Esto garantiza que los cambios realizados en las distintas entidades relacionadas sólo se produzcan todos juntos o no se produzcan en absoluto.

Es posible actualizar entidades mediante el uso de métodos como Actualizar y Adjuntar /w EntityState; sin embargo, este método es bastante propenso a errores y puede generar errores de tiempo de ejecución intermitentes/situacionales en casos en los que DbContext podría estar rastreando una entidad cuando usa algo. como Adjuntar o Actualizar en una nueva instancia con el mismo ID. Como se mencionó, también conduce a menosdeclaraciones UPDATE SQL eficientes y pueden dejar su sistema vulnerable a manipulaciones no intencionadas.

1

Fue un error estúpido, lo siento. Olvidé escribir return false en la función onSubmit(form), por lo que el formulario se envió dos veces. Todo el tiempo me centré en el código C#. De todos modos, sus sugerencias mejoraron mi código y evitaron errores futuros, así que muchas gracias a todos por su tiempo y esfuerzo :)

– Saša V

28/03/2021 a las 17:15



------------------------------------

Pensé en agregar mi experiencia personal y solucionarlo. Tenía un problema porque todas mis llamadas se publicaban dos veces, pero solo en Firefox. El problema resultó ser la forma en que Firefox e IIS se comunican. La forma en que lo resolví fue deshabilitando HTTP/2 en la configuración de enlace https dentro de IIS. Solo descubrí la solución después de un día buscando en el código un href o src en blanco en alguna parte.

Respondido

12 de julio de 2021 a las 14:41

Tony Cobb

Tony Cobb

326

4

4 insignias de plata

6

6 insignias de bronce

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare