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

entity-framework import loops performance savechanges

Question

J'effectue une importation qui comportera des milliers d'enregistrements à chaque exécution. Je cherche juste une confirmation de mes hypothèses:

Laquelle de ces choses a le plus de sens:

  1. Exécutez SaveChanges() chaque appel AddToClassName() .
  2. Exécutez SaveChanges() chaque nombre n d' AddToClassName() .
  3. Exécutez SaveChanges() après tous les AddToClassName() .

La première option est probablement lente non? Puisqu'il faudra analyser les objets EF en mémoire, générer du SQL, etc.

Je suppose que la deuxième option est le meilleur des deux mondes, car nous pouvons encapsuler une tentative d’essais autour de cet appel SaveChanges() et ne perdre que n enregistrements à la fois, si l’un d’eux échoue. Peut-être stocker chaque lot dans une liste <>. Si l'appel SaveChanges() réussit, supprimez la liste. Si cela échoue, enregistrez les éléments.

La dernière option finirait probablement aussi par être très lente, car chaque objet EF devrait être en mémoire jusqu'à SaveChanges() que SaveChanges() soit appelé. Et si la sauvegarde échouait, rien ne serait commis, non?

Réponse acceptée

Je le testerais d'abord pour en être sûr. Les performances ne doivent pas être si mauvaises.

Si vous devez entrer toutes les lignes dans une transaction, appelez-la après toute la classe AddToClassName. Si les lignes peuvent être entrées indépendamment, enregistrez les modifications après chaque ligne. La cohérence de la base de données est importante.

Deuxième option que je n'aime pas. Il serait déroutant pour moi (du point de vue de l'utilisateur final) si j'importais dans le système et que cela déclinerait de 10 lignes sur 1 000 simplement parce que 1 est mauvais. Vous pouvez essayer d’importer 10 et, en cas d’échec, essayez un par un, puis connectez-vous.

Testez si cela prend beaucoup de temps. N'écris pas "propablement". Vous ne le savez pas encore. Pensez à une autre solution (marc_s) uniquement en cas de problème.

MODIFIER

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

10000 lignes:

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

50000 lignes:

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

Il est donc plus rapide de s’engager après n lignes que après tout.

Ma recommandation est de:

  • SaveChanges () après n lignes.
  • Si un commit échoue, essayez-le un par un pour trouver la ligne défectueuse.

Classes de test:

TABLE:

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

Réponse populaire

Je viens d’optimiser un problème très similaire dans mon propre code et je voudrais signaler une optimisation qui a fonctionné pour moi.

J'ai constaté qu'une grande partie du temps de traitement de SaveChanges, qu'il s'agisse du traitement de 100 ou 1000 enregistrements à la fois, est lié au processeur. Ainsi, en traitant les contextes avec un modèle producteur / consommateur (implémenté avec BlockingCollection), j'ai pu mieux utiliser les cœurs de processeur et obtenu un total de 4 000 modifications / seconde (comme indiqué par la valeur de retour de SaveChanges). plus de 14 000 changements / seconde. L'utilisation du processeur est passée d'environ 13% (j'ai 8 cœurs) à environ 60%. Même en utilisant plusieurs threads grand public, j'ai à peine taxé le système d'E / S de disque (très rapide) et l'utilisation du processeur de SQL Server ne dépassait pas 15%.

En déchargeant l'enregistrement sur plusieurs threads, vous avez la possibilité d'ajuster le nombre d'enregistrements avant la validation et le nombre de threads effectuant les opérations de validation.

J'ai constaté que la création d'un thread producteur et (nombre de cœurs de processeur) -1 threads grand public me permettait d'ajuster le nombre d'enregistrements validés par lot, de sorte que le nombre d'éléments de BlockingCollection fluctuait entre 0 et 1 article). De cette façon, il restait juste assez de travail pour que les threads consommateurs fonctionnent de manière optimale.

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



Related

Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow
Sous licence: CC-BY-SA with attribution
Non affilié à Stack Overflow