91 votes

Quand devrais-je appeler SaveChanges () lors de la création de 1 000 objets Entity Framework? (comme lors d'une importation)

Je suis en cours d'exécution d'une importation qui ont plus de 1000 dossiers sur chaque course. Il suffit de regarder pour certains la confirmation de mes hypothèses:

Laquelle de ces a fait le plus de sens:

  1. Exécutez SaveChanges() chaque AddToClassName() appel.
  2. Exécutez SaveChanges() chaque n nombre de AddToClassName() des appels.
  3. Exécutez SaveChanges() après tous les de la AddToClassName() des appels.

La première option est sans doute ralentir le droit? Depuis, il devra analyser l'EF objets en mémoire, de générer le SQL, etc.

Je suppose que la deuxième option est le meilleur des deux mondes, puisque l'on peut enrouler un try catch autour de SaveChanges() appel et ne perdez n nombre d'enregistrements à la fois, si l'un d'entre eux tombe en panne. Peut-être stocker chaque lot dans une Liste<>. Si l' SaveChanges() appel réussit, débarrassez-vous de la liste. Si elle échoue, le journal les articles.

La dernière option serait probablement finir par être très lent, puisque chaque EF objet devra être en mémoire jusqu'à ce qu' SaveChanges() est appelé. Et si l'enregistrement a échoué, rien ne pourrait être engagée, à droite?

71voto

LukLed Points 18010

Je voudrais d'abord tester pour en être sûr. La Performance ne doit pas être si mauvais que ça.

Si vous avez besoin de saisir toutes les lignes en une seule transaction, de l'appeler après tout de AddToClassName classe. Si les lignes peuvent être saisies de façon indépendante, d'enregistrer les modifications après chaque ligne. La consistance de la base de données est important.

Deuxième option, je n'aime pas. Il serait source de confusion pour moi (à partir de finale de vue de l'utilisateur) si j'ai fait l'importation de système et il y aurait une diminution de 10 lignes de 1000, juste parce que 1 est mauvais. Vous pouvez essayer d'importer 10 et si elle échoue, essayer un par un et puis connectez-vous.

Tester si ça prend beaucoup de temps. Ne pas écrire 'probablement'. Vous ne le connaissez pas encore. Seulement quand il est en fait un problème, de penser à autre solution (marc_s).

MODIFIER

J'ai fait quelques tests (temps en millisecondes):

10000 lignes:

SaveChanges() après 1 ligne:18510,534
SaveChanges() après 100 lignes:4350,3075
SaveChanges() après 10000 lignes:5233,0635

50000 lignes:

SaveChanges() après 1 ligne:78496,929
SaveChanges() après 500 lignes:22302,2835
SaveChanges() après 50000 lignes:24022,8765

Donc, il est effectivement plus rapide de s'engager après la n de lignes que, après tout.

Ma recommandation est de:

  • SaveChanges() après n lignes.
  • Si un commit échoue, essayer un par un pour trouver défectueux ligne.

Classes de Test:

TABLEAU:

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]

Classe:

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);
    }
}

20voto

Eric J. Points 73338

J'ai juste optimisé une très similaires problème dans mon code et tiens à souligner une optimisation qui a fonctionné pour moi.

J'ai trouvé que la plupart du temps dans le traitement des SaveChanges, si le traitement de 100 ou de 1000 dossiers à la fois, est liée à l'UC. Ainsi, par le traitement de l'contextes avec un producteur/consommateur motif (mis en œuvre avec BlockingCollection), j'ai été en mesure de faire beaucoup mieux l'utilisation de cœurs de PROCESSEUR et obtenu à partir d'un total de 4000 changements/seconde (comme indiqué par la valeur de retour de SaveChanges) à plus de 14 000 changements/seconde. L'utilisation de l'UC passé d'environ 13 % (j'ai 8 cœurs) à environ 60%. Même l'utilisation de plusieurs threads consommateurs, j'ai à peine imposée les (très rapide) des e / s disque système et l'utilisation du PROCESSEUR SQL Server n'a pas augmenté de 15%.

En transférant de l'économie pour plusieurs threads, vous avez la possibilité de régler à la fois le nombre de dossiers avant de s'engager et le nombre de threads d'effectuer les opérations de validation.

J'ai trouvé que la création de 1 producteur de fil et le nombre de Cœurs du PROCESSEUR)-1 consommateur fils m'a permis de régler le nombre de dossiers engagés par lot, tels que le nombre d'éléments dans la BlockingCollection varie entre 0 et 1 (d'après un thread consommateur a pris d'un seul élément). De cette façon, il y avait juste assez de travail pour la consommer des threads pour fonctionner de façon optimale.

Ce scénario de cours nécessite la création d'un nouveau contexte pour chaque lot, que je trouve plus rapide, même dans un seul thread scénario pour mon cas d'utilisation.

13voto

marc_s Points 321990

Si vous avez besoin d'importer des milliers de dossiers, je voudrais utiliser quelque chose comme SqlBulkCopy, et non l'Entité Cadre pour cela.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X