web-dev-qa-db-fra.com

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

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

Laquelle de celles-ci a le plus de sens:

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

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

Je suppose que la deuxième option est la meilleure des deux mondes, car nous pouvons encapsuler un appel autour de cet appel SaveChanges() et ne perdre que n nombre d'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. S'il échoue, connectez-vous aux éléments.

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

73
John Bubriski

Je le testerais d'abord pour être sûr. La performance ne doit pas être si mauvaise.

Si vous devez entrer toutes les lignes dans une transaction, appelez-la après toute la classe AddToClassName. Si des 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. Ce serait déroutant pour moi (du point de vue de l'utilisateur final) si je faisais de l'importation dans le système et cela diminuerait 10 lignes sur 1000, simplement parce que 1 est mauvais. Vous pouvez essayer d'importer 10 et s'il échoue, essayez un par un puis connectez-vous.

Testez si cela prend du temps. N'écrivez pas "correctement". Tu ne le sais pas encore. Ce n'est que lorsqu'il s'agit réellement d'un problème, pensez à une 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

Il est donc plus rapide de valider après n lignes qu'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);
    }
}
55
LukLed

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

J'ai constaté que la plupart du temps dans le traitement des SaveChanges, qu'il s'agisse de traiter 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 faire une bien meilleure utilisation des cœurs de processeur et suis passé d'un total de 4000 changements/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 disque (très rapide) IO l'utilisation du système et du processeur de SQL Server n'était pas supérieure à 15%.

En déchargeant l'enregistrement sur plusieurs threads, vous avez la possibilité de régler à la fois 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 de 1 thread producteur et (# de cœurs de processeur) -1 threads consommateurs m'a permis de régler le nombre d'enregistrements validés par lot de sorte que le nombre d'éléments dans BlockingCollection fluctuait entre 0 et 1 (après qu'un thread consommateur en a pris un article). De cette façon, il y avait juste assez de travail pour que les threads consommateurs fonctionnent de manière optimale.

Ce scénario nécessite bien sûr la création d'un nouveau contexte pour chaque lot, ce que je trouve plus rapide même dans un scénario monothread pour mon cas d'utilisation.

17
Eric J.

Si vous devez importer des milliers d'enregistrements, j'utiliserais quelque chose comme SqlBulkCopy, et non l'Entity Framework pour cela.

12
marc_s

Utilisez une procédure stockée.

  1. Créez un type de données défini par l'utilisateur dans Sql Server.
  2. Créez et remplissez un tableau de ce type dans votre code (très rapide).
  3. Passez le tableau à votre procédure stockée avec un seul appel (très rapide).

Je pense que ce serait le moyen le plus simple et le plus rapide de le faire.

2
David

Désolé, je sais que ce fil est ancien, mais je pense que cela pourrait aider d'autres personnes avec ce problème.

J'ai eu le même problème, mais il est possible de valider les modifications avant de les valider. Mon code ressemble à ceci et il fonctionne très bien. Avec le chUser.LastUpdated Je vérifie s'il s'agit d'une nouvelle entrée ou seulement d'une modification. Parce qu'il n'est pas possible de recharger une entrée qui n'est pas encore dans la base de données.

// 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();
1
Jan Leuenberger