Que puis-je faire pour résoudre une exception «Ligne non trouvée ou modifiée» dans LINQ to SQL sur une base de données SQL Server Compact Edition?

96

Lors de l'exécution de SubmitChanges vers DataContext après la mise à jour de quelques propriétés avec une connexion LINQ to SQL (par rapport à SQL Server Compact Edition), j'obtiens un message «Ligne introuvable ou modifiée». ChangeConflictException.

var ctx = new Data.MobileServerDataDataContext(Common.DatabasePath);
var deviceSessionRecord = ctx.Sessions.First(sess => sess.SessionRecId == args.DeviceSessionId);

deviceSessionRecord.IsActive = false;
deviceSessionRecord.Disconnected = DateTime.Now;

ctx.SubmitChanges();

La requête génère le SQL suivant:

UPDATE [Sessions]
SET [Is_Active] = @p0, [Disconnected] = @p1
WHERE 0 = 1
-- @p0: Input Boolean (Size = 0; Prec = 0; Scale = 0) [False]
-- @p1: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:12:02 PM]
-- Context: SqlProvider(SqlCE) Model: AttributedMetaModel Build: 3.5.21022.8

Le problème évident est le WHERE 0 = 1 , après le chargement de l'enregistrement, j'ai confirmé que toutes les propriétés du "deviceSessionRecord" sont correctes pour inclure la clé primaire. Aussi lors de la capture de "ChangeConflictException", il n'y a aucune information supplémentaire sur la raison pour laquelle cela a échoué. J'ai également confirmé que cette exception est levée avec exactement un enregistrement dans la base de données (l'enregistrement que je tente de mettre à jour)

Ce qui est étrange, c'est que j'ai une instruction de mise à jour très similaire dans une section de code différente et qu'elle génère le SQL suivant et met effectivement à jour ma base de données SQL Server Compact Edition.

UPDATE [Sessions]
SET [Is_Active] = @p4, [Disconnected] = @p5
WHERE ([Session_RecId] = @p0) AND ([App_RecId] = @p1) AND ([Is_Active] = 1) AND ([Established] = @p2) AND ([Disconnected] IS NULL) AND ([Member_Id] IS NULL) AND ([Company_Id] IS NULL) AND ([Site] IS NULL) AND (NOT ([Is_Device] = 1)) AND ([Machine_Name] = @p3)
-- @p0: Input Guid (Size = 0; Prec = 0; Scale = 0) [0fbbee53-cf4c-4643-9045-e0a284ad131b]
-- @p1: Input Guid (Size = 0; Prec = 0; Scale = 0) [7a174954-dd18-406e-833d-8da650207d3d]
-- @p2: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:20:50 PM]
-- @p3: Input String (Size = 0; Prec = 0; Scale = 0) [CWMOBILEDEV]
-- @p4: Input Boolean (Size = 0; Prec = 0; Scale = 0) [False]
-- @p5: Input DateTime (Size = 0; Prec = 0; Scale = 0) [9/4/2008 5:20:52 PM]
-- Context: SqlProvider(SqlCE) Model: AttributedMetaModel Build: 3.5.21022.8

J'ai confirmé que les valeurs de champs primaires appropriées ont été identifiées à la fois dans le schéma de base de données et dans le DBML qui génère les classes LINQ.

Je suppose que c'est presque une question en deux parties:

  1. Pourquoi l'exception est-elle levée?
  2. Après avoir examiné le deuxième ensemble de SQL généré, il semble que pour détecter les conflits, il serait bien de vérifier tous les champs, mais j'imagine que ce serait assez inefficace. Est-ce ainsi que cela fonctionne toujours? Existe-t-il un paramètre pour vérifier simplement la clé primaire?

Je me bats avec ça depuis deux heures, donc toute aide serait appréciée.

Kevin
la source
FWIW: J'obtenais cette erreur en appelant involontairement la méthode deux fois. Cela se produirait au deuxième appel.
Kris
Excellentes informations de fond à trouver sur c-sharpcorner.com/article/…
CAK2

Réponses:

189

C'est méchant, mais simple:

Vérifiez si les types de données de tous les champs du O / R-Designer correspondent aux types de données de votre table SQL. Vérifiez à nouveau pour nullable! Une colonne doit être Nullable à la fois dans O / R-Designer et SQL, ou non Nullable dans les deux.

Par exemple, une colonne NVARCHAR "titre" est marquée comme NULLable dans votre base de données et contient la valeur NULL. Même si la colonne est marquée comme NOT NULLable dans votre O / R-Mapping, LINQ la chargera avec succès et définira la chaîne de colonne sur null.

  • Maintenant, vous changez quelque chose et appelez SubmitChanges ().
  • LINQ générera une requête SQL contenant "WHERE [title] IS NULL", pour s'assurer que le titre n'a pas été modifié par quelqu'un d'autre.
  • LINQ recherche les propriétés de [titre] dans le mappage.
  • LINQ trouvera [title] NOT NULLable.
  • Puisque [titre] n'est PAS NULL, par logique, il ne pourrait jamais être NULL!
  • Ainsi, en optimisant la requête, LINQ la remplace par "où 0 = 1", l'équivalent SQL de "jamais".

Le même symptôme apparaît lorsque les types de données d'un champ ne correspondent pas au type de données dans SQL, ou si des champs sont manquants, car LINQ ne pourra pas s'assurer que les données SQL n'ont pas changé depuis la lecture des données.

Sam
la source
4
J'ai eu un problème similaire - quoique légèrement différent -, et votre conseil de vérifier la présence de nullable m'a sauvé la journée! J'étais déjà chauve, mais ce problème m'aurait sûrement coûté une autre chevelure si j'en avais une .. merci!
Rune Jacobsen
7
Assurez-vous de définir la propriété «Nullable» dans la fenêtre des propriétés sur True. J'étais en train de modifier la propriété 'Server Data Type', en la changeant de VARCHAR(MAX) NOT NULLen VARCHAR(MAX) NULLet en m'attendant à ce qu'elle fonctionne. Erreur très simple.
J'ai dû voter pour cela. Cela m'a fait gagner beaucoup de temps. Je regardais mes niveaux d'isolement parce que j'avais pensé que c'était un problème de concurrence
Adrian
3
J'avais une NUMERIC(12,8)colonne mappée à une Decimalpropriété. J'ai dû préciser le DbType dans l'attribut Column [Column(DbType="numeric(12,8)")] public decimal? MyProperty ...
Costo
3
Une façon d'identifier les champs / colonnes problématiques consiste à enregistrer vos classes d'entités Linq-to-SQL actuelles, situées dans le fichier .dbml, dans un fichier séparé. Ensuite, supprimez votre modèle actuel et régénérez-le à partir de la base de données (en utilisant VS), ce qui générera un nouveau fichier .dbml. Ensuite, exécutez simplement un comparateur comme WinMerge ou WinDiff sur les deux fichiers .dbml pour localiser les différences de problème.
david.barkhuizen
24

Premièrement, il est utile de savoir ce qui cause le problème. La solution Google devrait aider, vous pouvez enregistrer les détails (table, colonne, ancienne valeur, nouvelle valeur) sur le conflit pour trouver une meilleure solution pour résoudre le conflit plus tard:

public class ChangeConflictExceptionWithDetails : ChangeConflictException
{
    public ChangeConflictExceptionWithDetails(ChangeConflictException inner, DataContext context)
        : base(inner.Message + " " + GetChangeConflictExceptionDetailString(context))
    {
    }

    /// <summary>
    /// Code from following link
    /// https://ittecture.wordpress.com/2008/10/17/tip-of-the-day-3/
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    static string GetChangeConflictExceptionDetailString(DataContext context)
    {
        StringBuilder sb = new StringBuilder();

        foreach (ObjectChangeConflict changeConflict in context.ChangeConflicts)
        {
            System.Data.Linq.Mapping.MetaTable metatable = context.Mapping.GetTable(changeConflict.Object.GetType());

            sb.AppendFormat("Table name: {0}", metatable.TableName);
            sb.AppendLine();

            foreach (MemberChangeConflict col in changeConflict.MemberConflicts)
            {
                sb.AppendFormat("Column name : {0}", col.Member.Name);
                sb.AppendLine();
                sb.AppendFormat("Original value : {0}", col.OriginalValue.ToString());
                sb.AppendLine();
                sb.AppendFormat("Current value : {0}", col.CurrentValue.ToString());
                sb.AppendLine();
                sb.AppendFormat("Database value : {0}", col.DatabaseValue.ToString());
                sb.AppendLine();
                sb.AppendLine();
            }
        }

        return sb.ToString();
    }
}

Créez un assistant pour envelopper vos sumbitChanges:

public static class DataContextExtensions
{
    public static void SubmitChangesWithDetailException(this DataContext dataContext)
    {   
        try
        {         
            dataContext.SubmitChanges();
        }
        catch (ChangeConflictException ex)
        {
            throw new ChangeConflictExceptionWithDetails(ex, dataContext);
        }           
    }
}

Et puis appelez le code de soumission des modifications:

Datamodel.SubmitChangesWithDetailException();

Enfin, enregistrez l'exception dans votre gestionnaire d'exceptions global:

protected void Application_Error(object sender, EventArgs e)
{         
    Exception ex = Server.GetLastError();
    //TODO
}
Tomas Kubes
la source
3
Superbe solution! J'ai une table qui contient environ 80 champs, et il y a de nombreux déclencheurs sur la table qui mettent à jour divers champs lors des insertions et des mises à jour. J'obtenais cette erreur lors de la mise à jour du datacontext à l'aide de L2S, mais j'étais à peu près sûr qu'elle était causée par l'un des déclencheurs mettant à jour un champ, ce qui rendait le contexte des données différent des données de la table. Votre code m'a aidé à voir exactement quel champ provoquait la désynchronisation du contexte de données avec la table. Merci beaucoup!!
Jagd
1
C'est une excellente solution pour les grandes tables. Pour gérer les valeurs nulles, remplacez «col.XValue.ToString ()» par «col.XValue == null? "null": col.XValue.ToString () 'pour chacun des trois champs de valeur.
humbads
Idem sur la protection contre les références nulles lors de la stringification OriginalValue, CurrentValue et DatabaseValue.
Floyd Kosch
16

Il existe une méthode sur DataContext appelée Refresh qui peut aider ici. Il vous permet de recharger l'enregistrement de la base de données avant que les modifications ne soient soumises et propose différents modes pour déterminer les valeurs à conserver. "KeepChanges" semble le plus intelligent pour mes besoins, il est destiné à fusionner mes modifications avec tout changement non conflictuel qui s'est produit dans la base de données entre-temps.

Si je comprends bien. :)

Matt Sherman
la source
5
Cette réponse a résolu le problème dans mon cas: dc.Refresh(RefreshMode.KeepChanges,changedObject);avant dc.SubmitChanges
HugoRune
J'ai eu ce problème lors de l'application de ReadOnlyAttribute aux propriétés d'un site Web Dynamic Data. Les mises à jour ont cessé de fonctionner et j'obtenais l'erreur "Ligne non trouvée ou modifiée" (les insertions étaient bien cependant). Le correctif ci-dessus a économisé beaucoup d'efforts et de temps!
Chris Cannon
Pourriez-vous s'il vous plaît expliquer les valeurs RefreshMode, par exemple que signifie KeepCurrentValues? Qu'est ce que ça fait? Merci beaucoup. Je pourrais créer une question ...
Chris Cannon
J'ai eu des problèmes avec des transactions simultanées qui ne se terminaient pas à temps pour qu'une autre transaction commence sur les mêmes lignes. KeepChanges m'a aidé ici, alors peut-être qu'il abandonne simplement la transaction en cours (tout en conservant les valeurs sauvegardées) et en démarre la nouvelle (honnêtement, je n'en ai aucune idée)
Erik Bergstedt
11

Cela peut également être dû à l'utilisation de plusieurs DbContext.

Donc par exemple:

protected async Task loginUser(string username)
{
    using(var db = new Db())
    {
        var user = await db.Users
            .SingleAsync(u => u.Username == username);
        user.LastLogin = DateTime.UtcNow;
        await db.SaveChangesAsync();
    }
}

protected async Task doSomething(object obj)
{
    string username = "joe";
    using(var db = new Db())
    {
        var user = await db.Users
            .SingleAsync(u => u.Username == username);

        if (DateTime.UtcNow - user.LastLogin >
            new TimeSpan(0, 30, 0)
        )
            loginUser(username);

        user.Something = obj;
        await db.SaveChangesAsync();
    }
}

Ce code échouera de temps en temps, d'une manière qui semble imprévisible, car l'utilisateur est utilisé dans les deux contextes, modifié et enregistré dans l'un, puis enregistré dans l'autre. La représentation en mémoire de l'utilisateur qui possède "Something" ne correspond pas à ce qui est dans la base de données, et vous obtenez donc ce bogue caché.

Une façon d'éviter cela est d'écrire tout code qui pourrait être appelé en tant que méthode de bibliothèque de telle manière qu'il prenne un DbContext facultatif:

protected async Task loginUser(string username, Db _db = null)
{
    await EFHelper.Using(_db, async db =>
    {
        var user = await db.Users...
        ... // Rest of loginUser code goes here
    });
}

public class EFHelper
{
    public static async Task Using<T>(T db, Func<T, Task> action)
        where T : DbContext, new()
    {
        if (db == null)
        {
            using (db = new T())
            {
                await action(db);
            }
        }
        else
        {
            await action(db);
        }
    }
}

Alors maintenant, votre méthode prend une base de données optionnelle, et s'il n'y en a pas, elle en crée une elle-même. Si tel est le cas, il réutilise simplement ce qui a été transmis. La méthode d'assistance facilite la réutilisation de ce modèle dans votre application.

Chris Moschini
la source
10

J'ai résolu cette erreur en déplaçant à nouveau sur une table de l'explorateur de serveur au concepteur et en reconstruisant.


la source
Le redragging de la table incriminée de l'Explorateur de serveur au concepteur et la reconstruction ont également résolu ce problème pour moi.
rstackhouse
4

Voici ce dont vous avez besoin pour remplacer cette erreur sur le code C #:

            try
            {
                _db.SubmitChanges(ConflictMode.ContinueOnConflict);
            }
            catch (ChangeConflictException e)
            {
                foreach (ObjectChangeConflict occ in _db.ChangeConflicts)
                {
                    occ.Resolve(RefreshMode.KeepChanges);
                }
            }
MarceloBarbosa
la source
J'ai programmé des éléments soumis par une application frontale à la base de données. Ceux-ci déclenchent l'exécution dans un service, chacun sur des threads différents. L'utilisateur peut appuyer sur un bouton «annuler» qui modifie tout l'état de la commande en suspens. Le service termine chacun mais constate que «En attente» a été changé en «Annulé» et ne peut pas le changer en «Terminé». Cela a résolu le problème pour moi.
pwrgreg007
2
Vérifiez également les autres énumérations de RefreshMode, comme KeepCurrentValues. Notez que vous devez à nouveau appeler SubmitChanges après avoir utilisé cette logique. Voir msdn.microsoft.com/en-us/library/… .
pwrgreg007
3

Je ne sais pas si vous avez trouvé des réponses satisfaisantes à votre question, mais j'ai posté une question similaire et y ai finalement répondu moi-même. Il s'est avéré que l'option de connexion par défaut NOCOUNT était activée pour la base de données, ce qui a provoqué une exception ChangeConflictException pour chaque mise à jour effectuée avec Linq vers Sql. Vous pouvez consulter mon message ici .

Michael Nero
la source
3

J'ai corrigé cela en ajoutant (UpdateCheck = UpdateCheck.Never)à toutes les [Column]définitions.

Ne semble pas être une solution appropriée, cependant. Dans mon cas, cela semble être lié au fait que cette table a une association avec une autre table à partir de laquelle une ligne est supprimée.

C'est sur Windows Phone 7.5.

Johan Paul
la source
1

Dans mon cas, l'erreur a été générée lorsque deux utilisateurs ayant des contextes de données LINQ-to-SQL différents ont mis à jour la même entité de la même manière. Lorsque le deuxième utilisateur a tenté la mise à jour, la copie qu'il avait dans son contexte de données était périmée même si elle avait été lue après la fin de la première mise à jour.

J'ai découvert l'explication et la solution dans cet article d'Akshay Phadke: https://www.c-sharpcorner.com/article/overview-of-concurrency-in-linq-to-sql/

Voici le code que j'ai le plus souvent levé:

try
{
    this.DC.SubmitChanges();
}
catch (ChangeConflictException)
{
     this.DC.ChangeConflicts.ResolveAll(RefreshMode.OverwriteCurrentValues);

     foreach (ObjectChangeConflict objectChangeConflict in this.DC.ChangeConflicts)
     {
         foreach (MemberChangeConflict memberChangeConflict in objectChangeConflict.MemberConflicts)
         {
             Debug.WriteLine("Property Name = " + memberChangeConflict.Member.Name);
             Debug.WriteLine("Current Value = " + memberChangeConflict.CurrentValue.ToString());
             Debug.WriteLine("Original Value = " + memberChangeConflict.OriginalValue.ToString());
             Debug.WriteLine("Database Value = " + memberChangeConflict.DatabaseValue.ToString());
         }
     }
     this.DC.SubmitChanges();
     this.DC.Refresh(RefreshMode.OverwriteCurrentValues, att);
 }

Lorsque j'ai regardé ma fenêtre de sortie pendant le débogage, je pouvais voir que la valeur actuelle correspondait à la valeur de la base de données. La «valeur originale» a toujours été le coupable. C'était la valeur lue par le contexte de données avant d'appliquer la mise à jour.

Merci à MarceloBarbosa pour l'inspiration.

CAK2
la source
0

Je sais que cette question a depuis longtemps reçu une réponse, mais ici, j'ai passé les dernières heures à me cogner la tête contre un mur et je voulais juste partager ma solution qui s'est avérée ne pas être liée à l'un des éléments de ce fil:

Mise en cache!

La partie select () de mon objet de données utilisait la mise en cache. Lorsqu'il s'agissait de mettre à jour l'objet, une erreur de ligne non trouvée ou modifiée apparaissait.

Plusieurs des réponses ont mentionné l'utilisation de différents DataContext et rétrospectivement, c'est probablement ce qui se passait, mais cela ne m'a pas immédiatement amené à penser à la mise en cache, alors j'espère que cela aidera quelqu'un!

rtpHarry
la source
0

J'ai récemment rencontré cette erreur et j'ai découvert que le problème n'était pas lié à mon contexte de données, mais à une instruction de mise à jour se déclenchant dans un déclencheur après l'appel de Commit sur le contexte. Le déclencheur essayait de mettre à jour un champ non Nullable avec une valeur Null et provoquait une erreur de contexte avec le message mentionné ci-dessus.

J'ajoute cette réponse uniquement pour aider les autres à gérer cette erreur et à ne pas trouver de résolution dans les réponses ci-dessus.

jamisonLikeCode
la source
0

J'ai également eu cette erreur en raison de l'utilisation de deux contextes différents. J'ai résolu ce problème en utilisant un contexte de données unique.

srinivas vadlamudi
la source
0

Dans mon cas, le problème était lié aux options utilisateur à l'échelle du serveur. Suivant:

https://msdn.microsoft.com/en-us/library/ms190763.aspx

J'ai activé l'option NOCOUNT dans l'espoir d'obtenir des avantages en termes de performances:

EXEC sys.sp_configure 'user options', 512;
RECONFIGURE;

et cela s'avère casser les vérifications de Linq pour les lignes affectées (autant que je puisse le comprendre à partir de sources .NET), conduisant à ChangeConflictException

La réinitialisation des options pour exclure les 512 bits a résolu le problème.

Wojtek
la source
0

Après avoir utilisé la réponse de qub1n, j'ai trouvé que le problème pour moi était que j'avais déclaré par inadvertance qu'une colonne de base de données était décimale (18,0). J'attribuais une valeur décimale, mais la base de données la changeait, supprimant la partie décimale. Cela a entraîné le problème de changement de ligne.

Il suffit d'ajouter ceci si quelqu'un d'autre rencontre un problème similaire.

John Pasquet
la source
0

allez simplement avec Linq2DB, beaucoup mieux

nam vo
la source