Cosa posso fare per risolvere un'eccezione "Riga non trovata o modificata" in LINQ to SQL su un database SQL Server Compact Edition?

StackOverflow https://stackoverflow.com/questions/45045

  •  09-06-2019
  •  | 
  •  

Domanda

Durante l'esecuzione di sottomissione in DataContext dopo aver aggiornato un paio di proprietà con una connessione Linq a SQL (contro SQL Server Compact Edition) ottengo una "riga non trovata o modificata". 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 query genera il seguente SQL:

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

Il problema ovvio è il DOVE 0=1, Dopo che il record è stato caricato, ho confermato che tutte le proprietà in "deviceSessionRecord" sono corrette per includere la chiave primaria.Inoltre, quando si rileva "ChangeConflittiException" non sono presenti informazioni aggiuntive sul motivo per cui l'operazione non è riuscita.Ho anche confermato che questa eccezione viene generata esattamente con un record nel database (il record che sto tentando di aggiornare)

La cosa strana è che ho un'istruzione di aggiornamento molto simile in una diversa sezione di codice e genera il seguente SQL e aggiorna effettivamente il mio database 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

Ho confermato che i valori corretti dei campi primari sono stati identificati sia nello schema del database che nel DBML che genera le classi LINQ.

Immagino che questa sia quasi una domanda in due parti:

  1. Perché viene lanciata l'eccezione?
  2. Dopo aver esaminato il secondo set di SQL generato, sembra che per rilevare i conflitti sarebbe carino controllare tutti i campi, ma immagino che sarebbe abbastanza inefficiente.Funziona sempre così?Esiste un'impostazione per controllare solo la chiave primaria?

Ho lottato con questo problema nelle ultime due ore, quindi qualsiasi aiuto sarebbe apprezzato.

È stato utile?

Soluzione

È brutto, ma semplice:

Controlla se i tipi di dati per tutti i campi in O/R-Designer corrispondono ai tipi di dati nella tua tabella SQL.Doppio controllo per nullable! Una colonna deve essere nullable sia in O/R-Designer che in SQL oppure non nullable in entrambi.

Ad esempio, una colonna "titolo" NVARCHAR è contrassegnata come NULLable nel database e contiene il valore NULL.Anche se la colonna è contrassegnata come NOT NULLable nell'O/R-Mapping, LINQ la caricherà correttamente e imposterà la stringa di colonna su null.

  • Ora cambi qualcosa e chiama i sottomissione ().
  • LINQ genererà una query SQL contenente "dove [il titolo] è null", per assicurarsi che il titolo non sia stato cambiato da qualcun altro.
  • Linq cerca le proprietà di [titolo] nella mappatura.
  • LINQ troverà [titolo] NOT NULLable.
  • Poiché [il titolo] non è nullabile, per logica non potrebbe mai essere nullo!
  • Quindi, ottimizzando la query, Linq la sostituisce con "dove 0 = 1", l'equivalente SQL di "Never".

Lo stesso sintomo verrà visualizzato quando i tipi di dati di un campo non corrispondono al tipo di dati in SQL o se mancano i campi, poiché LINQ non sarà in grado di garantire che i dati SQL non siano cambiati dopo la lettura dei dati.

Altri suggerimenti

Innanzitutto è utile sapere qual è la causa del problema.La soluzione di ricerca su Google dovrebbe aiutare, puoi registrare i dettagli (tabella, colonna, vecchio valore, nuovo valore) sul conflitto per trovare una soluzione migliore per risolvere il conflitto in un secondo momento:

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

Crea un helper per avvolgere i tuoi sumbitModifiche:

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

E poi chiama il codice di invio delle modifiche:

Datamodel.SubmitChangesWithDetailException();

Infine, registra l'eccezione nel tuo gestore eccezioni globale:

protected void Application_Error(object sender, EventArgs e)
{         
    Exception ex = Server.GetLastError();
    //TODO
}

Esiste un metodo su DataContext chiamato ricaricare che può aiutare qui.Consente di ricaricare il record del database prima che vengano inviate le modifiche e offre diverse modalità per determinare quali valori mantenere."KeepChanges" sembra il più intelligente per i miei scopi, ha lo scopo di unire le mie modifiche con qualsiasi modifica non in conflitto avvenuta nel frattempo nel database.

Se ho capito bene.:)

Ho risolto questo errore trascinando nuovamente una tabella da Esplora server al progettista e ricostruendo.

Ciò può essere causato anche dall'utilizzo di più di un DbContext.

Quindi ad esempio:

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

Questo codice fallirà di tanto in tanto, in modi che sembrano imprevedibili, perché l'utente viene utilizzato in entrambi i contesti, modificato e salvato in uno, quindi salvato nell'altro.La rappresentazione in memoria dell'utente che possiede "Something" non corrisponde a ciò che è presente nel database e quindi si ottiene questo bug in agguato.

Un modo per evitare ciò è scrivere qualsiasi codice che potrebbe mai essere chiamato come metodo di libreria in modo tale da accettare un DbContext opzionale:

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

Quindi ora il tuo metodo prende un database opzionale e, se non ce n'è uno, ne crea uno da solo.Se c'è, riutilizza semplicemente ciò che è stato passato.Il metodo helper semplifica il riutilizzo di questo modello nella tua app.

Non so se hai trovato risposte soddisfacenti alla tua domanda, ma ho postato una domanda simile e alla fine ho risposto io stesso.Si è scoperto che l'opzione di connessione predefinita NOCOUNT era attivata per il database, il che ha causato una ChangeConflictException per ogni aggiornamento effettuato con Linq to Sql.Puoi fare riferimento al mio post su Qui.

Ho risolto il problema aggiungendo (UpdateCheck = UpdateCheck.Never) a tutti [Column] definizioni.

Non sembra, però, una soluzione adeguata.Nel mio caso sembra essere correlato al fatto che questa tabella ha un'associazione con un'altra tabella da cui viene eliminata una riga.

Questo è su Windows Phone 7.5.

Questo è ciò di cui hai bisogno per sovrascrivere questo errore sul codice C#:

            try
            {
                _db.SubmitChanges(ConflictMode.ContinueOnConflict);
            }
            catch (ChangeConflictException e)
            {
                foreach (ObjectChangeConflict occ in _db.ChangeConflicts)
                {
                    occ.Resolve(RefreshMode.KeepChanges);
                }
            }

So che a questa domanda è stata data risposta da tempo, ma ho passato le ultime ore a sbattere la testa contro il muro e volevo solo condividere la mia soluzione che si è rivelata non correlata a nessuno degli elementi di questo thread:

Memorizzazione nella cache!

La parte select() del mio oggetto dati utilizzava la memorizzazione nella cache.Quando si trattava di aggiornare l'oggetto, si verificava un errore di riga non trovata o modificata.

Molte delle risposte menzionavano l'utilizzo di diversi DataContext e in retrospettiva questo è probabilmente ciò che stava accadendo, ma non mi ha portato immediatamente a pensare alla memorizzazione nella cache, quindi spero che questo possa aiutare qualcuno!

Recentemente ho riscontrato questo errore e ho scoperto che il problema non riguardava il contesto dei dati, ma un'istruzione di aggiornamento attivata all'interno di un trigger dopo che Commit veniva chiamato sul contesto.Il trigger stava tentando di aggiornare un campo non nullable con un valore null e causava un errore nel contesto con il messaggio menzionato sopra.

Sto aggiungendo questa risposta esclusivamente per aiutare gli altri a gestire questo errore e non trovare una soluzione nelle risposte sopra.

Ho anche ricevuto questo errore a causa dell'utilizzo di due contesti diversi.Ho risolto questo problema utilizzando il contesto dati singolo.

Nel mio caso il problema riguardava le opzioni utente a livello di server.Seguente:

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

Ho abilitato l'opzione NOCOUNT nella speranza di ottenere alcuni vantaggi in termini di prestazioni:

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

e questo risulta rompere i controlli di Linq per le righe interessate (per quanto riesco a capirlo dalle fonti .NET), portando a ChangeConflittException

Il ripristino delle opzioni per escludere 512 bit ha risolto il problema.

Dopo aver utilizzato la risposta di qub1n, ho scoperto che il problema per me era che avevo inavvertitamente dichiarato una colonna del database come decimale (18,0).Stavo assegnando un valore decimale, ma il database lo stava modificando, eliminando la parte decimale.Ciò ha comportato il problema della modifica della riga.

Lo aggiungo solo se qualcun altro riscontra un problema simile.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top