¿Cuándo debo llamar a SaveChanges () al crear miles de objetos de Entity Framework? (como durante una importación)

80

Estoy ejecutando una importación que tendrá miles de registros en cada ejecución. Solo busco una confirmación de mis suposiciones:

Cuál de estos tiene más sentido:

  1. Ejecute SaveChanges()todas las AddToClassName()llamadas.
  2. Ejecute SaveChanges()cada n número de AddToClassName()llamadas.
  3. Corre SaveChanges()después de todas las AddToClassName()llamadas.

La primera opción probablemente sea lenta, ¿verdad? Dado que necesitará analizar los objetos EF en la memoria, generar SQL, etc.

Supongo que la segunda opción es la mejor de ambos mundos, ya que podemos envolver una captura de prueba alrededor de esa SaveChanges()llamada y solo perder un número n de registros a la vez, si uno de ellos falla. Quizás almacene cada lote en una Lista <>. Si la SaveChanges()llamada tiene éxito, elimine la lista. Si falla, registre los elementos.

La última opción probablemente también terminaría siendo muy lenta, ya que cada objeto EF tendría que estar en la memoria hasta que SaveChanges()se llame. Y si la salvación falla, no se comete nada, ¿verdad?

John Bubriski
fuente

Respuestas:

62

Primero lo probaría para estar seguro. El rendimiento no tiene por qué ser tan malo.

Si necesita ingresar todas las filas en una transacción, llámelo después de toda la clase AddToClassName. Si las filas se pueden ingresar de forma independiente, guarde los cambios después de cada fila. La consistencia de la base de datos es importante.

Segunda opción que no me gusta. Sería confuso para mí (desde la perspectiva del usuario final) si hiciera una importación al sistema y disminuiría 10 filas de 1000, solo porque 1 es malo. Puede intentar importar 10 y, si falla, intente uno por uno y luego inicie sesión.

Prueba si lleva mucho tiempo. No escriba "propagable". Aún no lo sabes. Solo cuando sea realmente un problema, piense en otra solución (marc_s).

EDITAR

He hecho algunas pruebas (tiempo en milisegundos):

10000 filas:

SaveChanges () después de 1 fila: 18510,534
SaveChanges () después de 100 filas: 4350,3075
SaveChanges () después de 10000 filas: 5233,0635

50000 filas:

SaveChanges () después de 1 fila: 78496,929
SaveChanges () después de 500 filas: 22302,2835
SaveChanges () después de 50000 filas: 24022,8765

Entonces, en realidad, es más rápido comprometerse después de n filas que después de todo.

Mi recomendación es:

  • SaveChanges () después de n filas.
  • Si falla una confirmación, inténtelo uno por uno para encontrar la fila defectuosa.

Clases de prueba:

MESA:

CREATE TABLE [dbo].[TestTable](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [SomeInt] [int] NOT NULL,
    [SomeVarchar] [varchar](100) NOT NULL,
    [SomeOtherVarchar] [varchar](50) NOT NULL,
    [SomeOtherInt] [int] NULL,
 CONSTRAINT [PkTestTable] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

Clase:

public class TestController : Controller
{
    //
    // GET: /Test/
    private readonly Random _rng = new Random();
    private const string _chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";

    private string RandomString(int size)
    {
        var randomSize = _rng.Next(size);

        char[] buffer = new char[randomSize];

        for (int i = 0; i < randomSize; i++)
        {
            buffer[i] = _chars[_rng.Next(_chars.Length)];
        }
        return new string(buffer);
    }


    public ActionResult EFPerformance()
    {
        string result = "";

        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(10000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 100 rows:" + EFPerformanceTest(10000, 100).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 10000 rows:" + EFPerformanceTest(10000, 10000).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 1 row:" + EFPerformanceTest(50000, 1).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 500 rows:" + EFPerformanceTest(50000, 500).TotalMilliseconds + "<br/>";
        TruncateTable();
        result = result + "SaveChanges() after 50000 rows:" + EFPerformanceTest(50000, 50000).TotalMilliseconds + "<br/>";
        TruncateTable();

        return Content(result);
    }

    private void TruncateTable()
    {
        using (var context = new CamelTrapEntities())
        {
            var connection = ((EntityConnection)context.Connection).StoreConnection;
            connection.Open();
            var command = connection.CreateCommand();
            command.CommandText = @"TRUNCATE TABLE TestTable";
            command.ExecuteNonQuery();
        }
    }

    private TimeSpan EFPerformanceTest(int noOfRows, int commitAfterRows)
    {
        var startDate = DateTime.Now;

        using (var context = new CamelTrapEntities())
        {
            for (int i = 1; i <= noOfRows; ++i)
            {
                var testItem = new TestTable();
                testItem.SomeVarchar = RandomString(100);
                testItem.SomeOtherVarchar = RandomString(50);
                testItem.SomeInt = _rng.Next(10000);
                testItem.SomeOtherInt = _rng.Next(200000);
                context.AddToTestTable(testItem);

                if (i % commitAfterRows == 0) context.SaveChanges();
            }
        }

        var endDate = DateTime.Now;

        return endDate.Subtract(startDate);
    }
}
LukLed
fuente
La razón por la que escribí "probablemente" es que hice una conjetura. Para dejar más claro que "no estoy seguro", lo hice en una pregunta. Además, creo que tiene mucho sentido pensar en los problemas potenciales ANTES de encontrarme con ellos. Esa es la razón por la que hice esta pregunta. Tenía la esperanza de que alguien supiera qué método sería más eficiente, y podría ir con eso, de inmediato.
John Bubriski
Tipo impresionante. Exactamente lo que estaba buscando. ¡Gracias por tomarse el tiempo para probar esto! Supongo que puedo almacenar cada lote en la memoria, probar la confirmación y luego, si falla, revisar cada uno individualmente como dijiste. Luego, una vez que termine ese lote, libere las referencias a esos 100 elementos para que se puedan borrar de la memoria. ¡Gracias de nuevo!
John Bubriski
3
La memoria no se liberará, porque todos los objetos serán retenidos por ObjectContext, pero tener 50000 o 100000 en contexto no ocupa mucho espacio en estos días.
LukLed
6
De hecho, descubrí que el rendimiento se degrada entre cada llamada a SaveChanges (). La solución a esto es eliminar el contexto después de cada llamada a SaveChanges () y volver a crear una nueva instancia para el siguiente lote de datos que se agregará.
Shawn de Wet
1
@LukLed no del todo ... está llamando a SaveChanges dentro de su bucle For ... por lo que el código podría continuar agregando más elementos para guardar dentro del bucle for en la misma instancia de ctx y llamar a SaveChanges más tarde en esa misma instancia .
Shawn de Wet
18

Acabo de optimizar un problema muy similar en mi propio código y me gustaría señalar una optimización que funcionó para mí.

Descubrí que gran parte del tiempo en el procesamiento de SaveChanges, ya sea que procese 100 o 1000 registros a la vez, depende de la CPU. Entonces, al procesar los contextos con un patrón productor / consumidor (implementado con BlockingCollection), pude hacer un uso mucho mejor de los núcleos de la CPU y obtuve un total de 4000 cambios / segundo (según lo informado por el valor de retorno de SaveChanges) a más de 14.000 cambios / segundo. La utilización de la CPU pasó de aproximadamente el 13% (tengo 8 núcleos) a aproximadamente el 60%. Incluso usando múltiples hilos de consumo, apenas gravé el sistema de E / S de disco (muy rápido) y la utilización de la CPU de SQL Server no fue superior al 15%.

Al descargar el guardado en varios subprocesos, tiene la capacidad de ajustar tanto el número de registros antes de la confirmación como la cantidad de subprocesos que realizan las operaciones de confirmación.

Descubrí que la creación de 1 subproceso de productor y (# de núcleos de CPU) -1 subprocesos de consumidor me permitió ajustar la cantidad de registros comprometidos por lote de modo que el recuento de elementos en BlockingCollection fluctuara entre 0 y 1 (después de que un subproceso de consumidor tomó uno articulo). De esa manera, había suficiente trabajo para que los subprocesos consumidores funcionaran de manera óptima.

Este escenario, por supuesto, requiere la creación de un nuevo contexto para cada lote, que encuentro que es más rápido incluso en un escenario de un solo subproceso para mi caso de uso.

Eric J.
fuente
Hola, @ eric-j, ¿podría elaborar un poco esta línea "procesando los contextos con un patrón de productor / consumidor (implementado con BlockingCollection)" para que pueda probar con mi código?
Foyzul Karim
14

Si necesita importar miles de registros, usaría algo como SqlBulkCopy, y no Entity Framework para eso.

marc_s
fuente
15
Odio cuando la gente no responde a mi pregunta :) Bueno, digamos que "necesito" usar EF. ¿Entonces que?
John Bubriski
3
Bueno, si realmente DEBE usar EF, entonces trataría de comprometerme después de un lote de, digamos, 500 o 1000 registros. De lo contrario, terminará usando demasiados recursos y una falla potencialmente revertiría todas las 99999 filas que ha actualizado cuando la 100000a falla.
marc_s
Con el mismo problema, terminé usando SqlBulkCopy, que es mucho más eficiente que EF en ese caso. Aunque no me gusta usar varias formas de acceder a la base de datos.
Julien N
2
También estoy investigando esta solución porque tengo el mismo problema ... La copia masiva sería una excelente solución, pero mi servicio de alojamiento no permite su uso (y supongo que otros también lo harían), por lo que esta no es una solución viable. opción para algunas personas.
Dennis Ward
3
@marc_s: ¿Cómo maneja la necesidad de hacer cumplir las reglas comerciales inherentes a los objetos comerciales cuando usa SqlBulkCopy? No veo cómo no usar EF sin implementar las reglas de manera redundante.
Eric J.
2

Utilice un procedimiento almacenado.

  1. Cree un tipo de datos definido por el usuario en Sql Server.
  2. Cree y complete una matriz de este tipo en su código (muy rápido).
  3. Pase la matriz a su procedimiento almacenado con una llamada (muy rápido).

Creo que esta sería la forma más fácil y rápida de hacerlo.

David
fuente
7
Normalmente, en SO, las afirmaciones de "esto es más rápido" deben fundamentarse con código de prueba y resultados.
Michael Blackburn
2

Lo siento, sé que este hilo es antiguo, pero creo que esto podría ayudar a otras personas con este problema.

Tuve el mismo problema, pero existe la posibilidad de validar los cambios antes de confirmarlos. Mi código se ve así y está funcionando bien. Con el chUser.LastUpdatedcompruebo si es una entrada nueva o solo un cambio. Porque no es posible recargar una Entrada que aún no está en la base de datos.

// Validate Changes
var invalidChanges = _userDatabase.GetValidationErrors();
foreach (var ch in invalidChanges)
{
    // Delete invalid User or Change
    var chUser  =  (db_User) ch.Entry.Entity;
    if (chUser.LastUpdated == null)
    {
        // Invalid, new User
        _userDatabase.db_User.Remove(chUser);
        Console.WriteLine("!Failed to create User: " + chUser.ContactUniqKey);
    }
    else
    {
        // Invalid Change of an Entry
        _userDatabase.Entry(chUser).Reload();
        Console.WriteLine("!Failed to update User: " + chUser.ContactUniqKey);
    }                    
}

_userDatabase.SaveChanges();
Jan Leuenberger
fuente
Sí, se trata del mismo problema, ¿verdad? Con esto, puede agregar los 1000 registros y antes de ejecutar saveChanges()puede eliminar los que causarían un Error.
Jan Leuenberger
1
Pero el énfasis de la pregunta está en cuántas inserciones / actualizaciones realizar de manera eficiente en una SaveChangesllamada. No aborda ese problema. Tenga en cuenta que existen más razones potenciales para que SaveChanges falle que los errores de validación. Por cierto, también puede marcar entidades como en Unchangedlugar de recargarlas / eliminarlas.
Gert Arnold
1
Tiene razón, no aborda directamente la pregunta, pero creo que la mayoría de las personas que tropiezan con este hilo están teniendo problemas con la validación, aunque hay otras razones por las que SaveChangesfalla. Y esto resuelve el problema. Si esta publicación realmente te molesta en este hilo, puedo eliminar esto, mi problema está resuelto, solo intento ayudar a otros
Jan Leuenberger
Tengo una pregunta sobre este. Cuando llamas, GetValidationErrors()¿"falsifica" una llamada a la base de datos y recupera errores o qué? Gracias por responder :)
Jeancarlo Fontalvo