O que posso fazer para resolver uma exceção “Linha não encontrada ou alterada” no LINQ to SQL em um banco de dados SQL Server Compact Edition?

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

  •  09-06-2019
  •  | 
  •  

Pergunta

Ao executar submitigos para o DataContext depois de atualizar algumas propriedades com uma conexão LINQ para SQL (contra o SQL Server Compact Edition), recebo uma "linha não encontrada ou alterada". 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();

A consulta gera o seguinte 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

O problema óbvio é o ONDE 0=1, Após o carregamento do registro, confirmei que todas as propriedades em "deviceSessionRecord" estão corretas para incluir a chave primária.Além disso, ao capturar "ChangeConflictException", não há informações adicionais sobre o motivo da falha.Também confirmei que esta exceção é lançada com exatamente um registro no banco de dados (o registro que estou tentando atualizar)

O que é estranho é que tenho uma instrução de atualização muito semelhante em uma seção diferente do código e ela gera o SQL a seguir e de fato atualiza meu banco de dados 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

Confirmei que os valores adequados dos campos primários foram identificados no esquema do banco de dados e no DBML que gera as classes LINQ.

Acho que esta é quase uma pergunta em duas partes:

  1. Por que a exceção está sendo lançada?
  2. Depois de revisar o segundo conjunto de SQL gerado, parece que para detectar conflitos seria bom verificar todos os campos, mas imagino que isso seria bastante ineficiente.É assim que sempre funciona?Existe uma configuração para apenas verificar a chave primária?

Tenho lutado com isso nas últimas duas horas, então qualquer ajuda seria apreciada.

Foi útil?

Solução

Isso é desagradável, mas simples:

Verifique se os tipos de dados de todos os campos do O/R-Designer correspondem aos tipos de dados da sua tabela SQL.Verifique novamente se há valor anulável! Uma coluna deve ser anulável no O/R-Designer e no SQL ou não anulável em ambos.

Por exemplo, uma coluna NVARCHAR "título" é marcada como NULLable em seu banco de dados e contém o valor NULL.Mesmo que a coluna esteja marcada como NOT NULLable em seu mapeamento O/R, o LINQ irá carregá-la com sucesso e definir a string da coluna como nula.

  • Agora você muda de algo e chama submitchanges ().
  • O LINQ gerará uma consulta SQL contendo "onde [título] é nulo", para garantir que o título não tenha sido alterado por outra pessoa.
  • Linq procura as propriedades do [título] no mapeamento.
  • O LINQ encontrará [título] NOT NULLable.
  • Como o [título] não é anulado, por lógica, nunca poderia ser nulo!
  • Portanto, otimizando a consulta, o LINQ a substitui por "Where 0 = 1", o equivalente do SQL de "Never".

O mesmo sintoma aparecerá quando os tipos de dados de um campo não corresponderem ao tipo de dados no SQL ou se os campos estiverem faltando, pois o LINQ não será capaz de garantir que os dados SQL não foram alterados desde a leitura dos dados.

Outras dicas

Primeiro, é útil saber o que está causando o problema.A solução de pesquisa no Google deve ajudar, você pode registrar os detalhes (tabela, coluna, valor antigo, novo valor) sobre o conflito para encontrar a melhor solução para resolver o conflito posteriormente:

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

Crie um auxiliar para agrupar suas sumbitChanges:

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

E então chame o código de envio de alterações:

Datamodel.SubmitChangesWithDetailException();

Por fim, registre a exceção em seu manipulador de exceções global:

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

Existe um método no DataContext chamado Atualizar o que pode ajudar aqui.Ele permite recarregar o registro do banco de dados antes do envio das alterações e oferece diferentes modos para determinar quais valores manter."KeepChanges" parece o mais inteligente para meus propósitos; seu objetivo é mesclar minhas alterações com qualquer alteração não conflitante que tenha acontecido no banco de dados nesse meio tempo.

Se bem entendi.:)

Resolvi esse erro arrastando novamente uma tabela do explorador de servidores para o designer e reconstruindo.

Isso também pode ser causado pelo uso de mais de um DbContext.

Então, por exemplo:

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

Esse código falhará de tempos em tempos, de maneiras que parecem imprevisíveis, porque o usuário é usado em ambos os contextos, alterado e salvo em um e depois salvo no outro.A representação na memória do usuário que possui "Algo" não corresponde ao que está no banco de dados e, portanto, você obtém esse bug oculto.

Uma maneira de evitar isso é escrever qualquer código que possa ser chamado como um método de biblioteca de forma que receba um DbContext opcional:

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

Então agora seu método usa um banco de dados opcional e, se não houver um, ele mesmo cria um.Se houver, apenas reutiliza o que foi passado.O método auxiliar facilita a reutilização desse padrão em seu aplicativo.

Não sei se você encontrou alguma resposta satisfatória para sua pergunta, mas postei uma pergunta semelhante e acabei respondendo eu mesmo.Descobriu-se que a opção de conexão padrão NOCOUNT estava ativada para o banco de dados, o que causava um ChangeConflictException para cada atualização feita com Linq to Sql.Você pode consultar minha postagem em aqui.

Eu consertei isso adicionando (UpdateCheck = UpdateCheck.Never) para todos [Column] definições.

No entanto, não parece uma solução apropriada.No meu caso parece estar relacionado ao fato desta tabela ter uma associação com outra tabela de onde uma linha é excluída.

Isso está no Windows Phone 7.5.

Isto é o que você precisa para substituir este erro no código C#:

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

Eu sei que esta pergunta já foi respondida há muito tempo, mas passei as últimas horas batendo minha cabeça contra a parede e só queria compartilhar minha solução, que acabou não estando relacionada a nenhum dos itens deste tópico:

Cache!

A parte select() do meu objeto de dados estava usando cache.Quando se tratou de atualizar o objeto, um erro de linha não encontrada ou alterada estava surgindo.

Várias das respostas mencionaram o uso de diferentes DataContext e, em retrospecto, isso é provavelmente o que estava acontecendo, mas não me levou instantaneamente a pensar em cache, então espero que isso ajude alguém!

Recentemente encontrei esse erro e descobri que o problema não estava no meu contexto de dados, mas em uma instrução de atualização disparada dentro de um gatilho após o Commit ser chamado no contexto.O gatilho estava tentando atualizar um campo não anulável com um valor nulo e estava causando erro no contexto com a mensagem mencionada acima.

Estou adicionando esta resposta apenas para ajudar outras pessoas a lidar com esse erro e não encontrar uma solução nas respostas acima.

Também recebi esse erro por usar dois contextos diferentes.Resolvi esse problema usando um contexto de dados único.

No meu caso, o problema estava nas opções do usuário em todo o servidor.Seguindo:

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

Ativei a opção NOCOUNT na esperança de obter alguns benefícios de desempenho:

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

e isso acaba quebrando as verificações do Linq para as linhas afetadas (tanto quanto eu consigo descobrir nas fontes .NET), levando a ChangeConflictException

Redefinir as opções para excluir o bit 512 resolveu o problema.

Depois de empregar a resposta de qub1n, descobri que o problema para mim era que eu havia declarado inadvertidamente uma coluna do banco de dados como decimal (18,0).Eu estava atribuindo um valor decimal, mas o banco de dados o alterou, eliminando a parte decimal.Isso resultou no problema de alteração de linha.

Basta adicionar isso se alguém tiver um problema semelhante.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top