none
EF e gestione della Concorrenza

    Domanda

  • Buongiorno, nella mia applicazione Winform ( un paio di textBox ed un bindingsource), che lavora in modalità connessa, sto implementando un controllo sulla concorrenza.
    Ho abilitato un procedure che verifica, innanzitutto, se sto effettuando l'inserimento di una nuova riga, oppure se sto procedendo all'aggiornamento dell'esistente.
    Quindi se sto inserendo una nuova riga chiamo la procedura Save(). Da notare la colonna LastMod, di tipo DateTime, su cui, nel modello, ho impostato il ConcurrencyMode a Fixed.
    Se invece sto effettuando l'aggiornamentodi una riga già esistente, quindi il mio stato non è NewInsert chiamo la procedura sUpdate().
    Premetto che il codice, per quanto concerne l'inserimento e l'aggiornamento funziona correttamente (...forse accede un pò troppe volte al DB)
    Tuttavia non mi gestisce la concorrenza. Come risolvo?

    private void btnSave_Click(object sender, EventArgs e)
            {

                if (stato == NewInsert) // nuovo inserimento
                {
                    try
                    {
                        if (ho scritto qualcosa nei textBox)
                        {
                            Save(); //procedura per un nuovo inserimento
                        }
                        else MessageBox.Show("Nessun dato inserito!", "Attenzione!",
                          MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                    }
                    catch (Exception ex)
                    {
                        if (ex.InnerException != null)
                            MessageBox.Show(ex.InnerException.Message.ToString());
                        else
                            MessageBox.Show(ex.Message.ToString());
                    }
                }
                else //sono in variazione
                {
                    sUpdate(); //procedura per l'aggiornamento
                }
            }


    private void Save()
            {
                using (EFPDEntities ctx = new EFPDEntities())
                {
                    City com = new City();
                    firstBindingSource.EndEdit();
                    com.CityName = cityNameTxt.Text;
                    com.ZipCode = zipCodeTxt.Text;
                    com.LastMod = DateTime.Now;
                    ctx.AddToCities(com);
                    ctx.SaveChanges();
                }
            }

    private void sUpdate()
            {
                using (EFPDEntities ctx = new EFPDEntities())
                {
                    City com = ctx.Cities.First(i => i.ID_City == ID); //ID è un int in cui memorizzo l'ID ottenuto dalla Ricerca
                    com.CityName = cityNameTxt.Text;
                    com.ZipCode = zipCodeTxt.Text;
                    com.LastMod = DateTime.Now;
                    try
                    {
                        ctx.SaveChanges();
                    }
                    catch (OptimisticConcurrencyException)
                    {
                        DialogResult result = (MessageBox.Show("Si vuole comunque sovrascriverli?",
                        "Attenzione! I dati sono stati modificati da un altro utente", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning));

                        switch (result)
                        {
                            case DialogResult.OK:
                                // yes
                                ctx.Refresh(RefreshMode.ClientWins, com);
                                ctx.SaveChanges();
                                break;

                            case DialogResult.Cancel:
                                // cancel
                                ctx.Refresh(RefreshMode.ClientWins, com);
                                break;
                        }
                    }
                }
            }

    Altra domanda, trattandondosi di modalità connessa è corretto scrivere ogni volta:

    using (EFPDEntities ctx = new EFPDEntities())
                {
      //

    oppure è sufficiente scrivere:

    public partial class Form1 : Form
        {
            EFDemoEntities ctx
            City com;

    Grazie

    mercoledì 21 marzo 2012 11:14

Risposte

  • Io farei qualcosa tipo:

    City lastSearchedCity = null;
    void frm_FormClosed(object sender, FormClosedEventArgs e)
    {
         SearchFrm frmSrc = sender as SearchFrm;

         ID = frmSrc.SelectedRow.ID_City; //qui potresti restituire direttamente la città volendo

         using (EFPDEntities ctx = new EFPDEntities())
         {
             var query = from c in ctx.Cities where c.ID_City == ID select c;
             lastSearchedCity = query.FirstOrDefault();
             firstBindingSource.DataSource = lastSearchedCity; //puoi mettere anche un solo oggetto
         }
    }

    Se le TextBox sono in Binding al firstBindignSource, l'oggetto lastSearchedCity viene modificato da solo quando modifichi i textbox.

    private void sUpdate()
    {
        using (EFPDEntities ctx = new EFPDEntities())
        {
             ctx.Cities.Attach(lastSearchedCity);
             ctx.ObjectStateManager.GetObjectStateEntry(lastSearchedCity).SetModified();
             try
             {
                   ctx.SaveChanges();
             }
             catch (OptimisticConcurrencyException)
             { //...

    • Contrassegnato come risposta piedatt80 venerdì 23 marzo 2012 11:19
    giovedì 22 marzo 2012 11:31

Tutte le risposte

  • Domande: In che senso non gestisce la concorrenza? quale valore hai impostato come StoreGeneratedPattern?

    Se imposti ConcurrencyMode=Fixed sulla proprietà LastMod, in fase di update EF controlla che il campo non è cambiato. Tu in sUpdate lo stai modificando sempre ad ogni update, quindi dovrebbe lanciarti *sempre* un OptimisticConcurrencyException.

    Attenzione che se voui salvare manualmente LastMod, allora devi utilizzare un altro campo per verificare la concorrenza.

    E' corretto usare il contesto in uno using in quanto fai la dispose dello stesso e quindi la chiususa delle connessioni aperte. Non è buona cosa avere connessioni al database lunghe.

    mercoledì 21 marzo 2012 12:51
  • Mi spiego meglio: ho impostato come StoreGeneratedPattern (in realtà lo ha fatto il modello in automatico, quando l'ho generato) ID_City.

    Effettivamente in sUpdate, LastMod viene modificato ad ogni aggiornamento, ed ho verificato che effettivamente, nel modello generato, la proprietà ConcurrencyMode di LastMod sia impostato a Fixed. Tuttavia in debug, non genera mai un OptimisticConcurrencyException, sebbene LastMod viene sovrascritto con il nuovo datetime. Dove sbaglio?

    Esprimo il mio concetto di concorrenza: Utente1 apre l'applicazione, fa una ricerca, seleziona il comune Milano (CityName) con CAP 20121 (zipCode) e si ferma per bere il suo amato caffè. Anche Utente2 apre l'applicazione, seleziona anche lui il comune di Milano (stesso ID) con il medesimo CAP e decide di sostituire il CAP da 20121 a 20162, salva ed esce dall'applicazione. Nel frattempo Utente1, che ha appena finito di bere il proprio caffè, ma aveva il form ancora aperto, decide di modificare il CAP da lui visualizzato da 20121 a 20130 e fa salva. Tuttavia la riga erà già stata oggetto di modifica da parte di Utente2 perciò scatta un OptimisticConcurrencyException. Questo è quello che vorrei facesse la mia applicazione.

    • Modificato piedatt80 mercoledì 21 marzo 2012 15:47
    mercoledì 21 marzo 2012 15:18
  • Ciao.

    La mia domanda riguardava la proprietà StoreGeneratedPattern di LastMod: http://msdn.microsoft.com/en-us/library/system.data.metadata.edm.storegeneratedpattern.aspx

    Nel database il record viene correttemente aggiornato (in particolare il valore di LastMod)?

    Ti chiedo tutto questo perchè, se ricordo bene, se usi Fixed con Computed, allora LastMod=DateTime.Now non dovrebbe essere preso in considerazione da EF. Però su DB non dovresti avere la data aggiornata.

    Prova ad usare un Sql Profiler, oppure loggare il sql con questo provider di EF (http://code.msdn.microsoft.com/EFProviderWrappers) per verificare che tutto funziona correttamente.

    Non mi viene in mente altro.

    mercoledì 21 marzo 2012 15:39
  • La proprietà StoreGeneratedPattern di LastMod è impostata a Computed. Ho provato anche a impostarla a Identity ma non cambia nulla.

    Il problema potrebbe essere imputabile al fatto che in sUpdate con:

    City com = ctx.Cities.First(i => i.ID_City == ID);

    vado a leggere dal DB anche le modifiche già apportate da altri utenti (ignorando/perdendo ciò che visualizzo attualmente sul form, ma che in realtà sul db risulta già variato) e successivamente con:

    com.LastMod = DateTime.Now;

    salvo il tutto. Ho anche provato a sostituire nel Db LastMod da datetime a rowversion. Ma l'OptimisticConcurrencyException non viene mai intercettata.

    mercoledì 21 marzo 2012 16:33
  • Ok.

    Leggi qui http://www.ladislavmrnka.com/2011/03/the-bug-in-storegeneratedpattern-fixed-in-vs-2010-sp1/
    Spiegazione StoreGeneratedPattern:
     - None: la imposti nell'applicazione, la salvi sul db.
     - Identity: viene letta dal db all'inserimento dell'entità.
     - Computed: viene letta dal db all'inserimento ed ad ogni update.

    Impostando quindi a Computed, è come se non scrivessi niente in LastMod. Probabilmetne permette valori null oppure ha un valore di default sul database, altrimenti non riusciresti nemmeno a salvare.

    Detto questo, se ti serve il valore LastMod per le regole di business, tieni la proprietà come ogni altra (ConcurrencyMode.None e StoreGenPattern.None).

    Per avere concorrenza, usa una proprietà nell'entità di tipo Binary, ConcurrencyMode.Fixed, StoreGenPattern.Computed. Sul database imposta la proprietà a timestamp.
    Quando aggiorni non devi leggere da db, ma fare l'attach dell'entità e impostarla come modificata:
     - ObjectContext.CollezioneOggetti.Attach(oggettoModificato);
     - ObjectCotnext.ObjectStateManager.GetObjectStateEntry(oggettoModificato).SetModified();

    Così ti solleva l'eccezione.

    mercoledì 21 marzo 2012 17:04
  • Ciao, il valore LastMod mi serve solo ed esclusivamente per la gestione della concorrenza e nient'altro. Immagino che sia proprio questo che devo impostare come da te suggerito: di tipo Binary, ConcurrencyMode.Fixed, StoreGenPattern.Computed e timestamp dul DB.

    Aggiungo un altro passaggio. Per eseguire la ricerca utitlizzo il seguente codice, il cui contesto è all'interno di un priorio using:

    using (EFPDEntities ctx = new EFPDEntities())
                    {
                        var query = from c in ctx.Cities
                                    where c.ID_City == txtID.text
                                    select c;
                        firstBindingSource.DataSource = query.ToList();
                    }

    quindi, una volta che sul form appare il risultato, l'utente apporta le modifiche (modifica un CAP), procede ad aggiornare invocando sUpdate. Tutto ok, se non fosse per il fatto che in sUpdate utilizzo un nuovo blocco using, quindi un ulteriore riferimento al contesto.

     using (EFPDEntities ctx = new EFPDEntities())
                {
                    City com = ctx.Cities.First(i => i.ID_City == ID); //ID è un int in cui memorizzo l'ID ottenuto dalla Ricerca
                    com.CityName = cityNameTxt.Text;
                    com.ZipCode = zipCodeTxt.Text;
                    com.LastMod = DateTime.Now;
                    try
                    {
                        ctx.SaveChanges();
                    }

    Quindi prima di:

    ObjectContext.CollezioneOggetti.Attach(oggettoModificato);
    ObjectCotnext.ObjectStateManager.GetObjectStateEntry(oggettoModificato).SetModified();

    devo, comunque, indicare al contesto qual'è la riga che sto modificando. Ed ecco perchè utilizzo:

    City com = ctx.Cities.First(i => i.ID_City == ID);

    Tuttavia, così facendo - consapevole che non è questa la soluzione - si verifica quanto descritto qualche post sopra. E' perfettamente corretto quanto da te scritto ovvero che dovrei fare un attach.

    Ma come modifico correttamente il mio codice ( sUpdate )?


    • Modificato piedatt80 giovedì 22 marzo 2012 08:59
    giovedì 22 marzo 2012 08:58
  • Ciao.

    In update non dovresti rileggere City dal database. Così facendo potresti leggere una Città modificata da un altra persona (con LastMod più recende del tuo), aggiornare i campi, salvare senza avere eccezioni di concorrenza.

    Per aggiornare dovresti solo scrivere

    using (EFPDEntities ctx = new EFPDEntities())
    {
            ctx.Cities.Attach(cittàModificata);
            ctx.Cities.GetObjectStateEntry(cittàModificata).SetModified();         
             try
             {
              ctx.SaveChanges();
             }
              catch {/*concurrency exception*/}
    }

    Quello che non vedo nel tuo codice è l'oggetto "cittàModificata", ovvero l'oggetto che ricavi con la precedente query.ToList() [forse puoi prenderlo da (City)firstBindingSource.Current];

    Altrimenti potresti fare tutto manualmente, rileggere la città dal database, verificare che LastMod è uguale al valore che hai su client, aggiornare e salvare.
    Se non è uguale allora hai un problema di concorrenza.
    Attenzoinen che anche così puoi avere una eccezione di concorrenza al savechanges.
    In ogni caso LastMod non devi modificarlo manualmente, è il DB che ci pensa.

    giovedì 22 marzo 2012 09:19
  • Ho modificato il codice come segue:

    private void sUpdate()
            {
                using (EFPDEntities ctx = new EFPDEntities())
                {
                    City com = new City();
                    ctx.Cities.Attach(com);
                    ctx.ObjectStateManager.GetObjectStateEntry(com).SetModified();
                    try
                    {
                        ctx.SaveChanges();
                    }
                    catch (OptimisticConcurrencyException)
                    {
                        DialogResult result = (MessageBox.Show("Si vuole comunque sovrascriverli?",
                        "Attenzione! I dati sono stati modificati da un altro utente", MessageBoxButtons.OKCancel, MessageBoxIcon.Warning));

                        switch (result)
                        {
                            case DialogResult.OK:
                                // yes
                                ctx.Refresh(RefreshMode.ClientWins, com);
                                ctx.SaveChanges();
                                break;

                            case DialogResult.Cancel:
                                // cancel
                                ctx.Refresh(RefreshMode.ClientWins, com);
                                break;
                        }
                    }
                }
            }

    Ma già quando vado ad effettuare una qualsiasi modifica (non in ambito concorrenziale) mi genera un eccezione di concorrenza.

    il compilatore si ferma generandomi il seguente errore:

    System.InvalidOperationException was unhandled
      Message=I seguenti oggetti non sono stati aggiornati perché non sono stati trovati nell'archivio:
    'EntitySet=Cities;ID_City=0'

    questo, immagino sia dovuto al fatto che il contesto non sa quale riga sto aggiornando. Secondo me, perchè l'attach lo sto facendo fuori dal blocco Using che ho utilizzato per effettuare la ricerca.

    Sto impazzendo....

    giovedì 22 marzo 2012 10:07
  • Wait :)

    E' sbagliato il City com = new City();
    Dovresti fare qualcosa tipo City com = (City)firstBindingSource.Current;

    Insomma, devi attacchare l'entità che stai modificando, non una nuova. Non so ora se Current fa al caso tuo, non so quale sia il tuo flusso...
    Supponi qualcosa tipo:

     - seleziono una city da una lista
     - clicco su modifica -> mostro una popup con delle textbox a cui passo un istanza di City.
     - clicco su salva -> aggiorno i campi di City ed esegue l'sUpdate.

    Se è più o meno così, allora quando clicchi su modifica memorizza City in una variabile. Quando clicci su salva, aggiorna la City che hai memorizzato poi attaccala al contesto ecc ecc...

    giovedì 22 marzo 2012 10:18
  • Ok, allora questo è il mio codice per quanto riguarda la ricerca. Non faccio altro che aprire un secondo form di ricerca. Alla chiusura del form ricerca, passo al form principale l'ID della riga selezionata:

    private void Search()
            {
                SearchFrm frmSrc = new SearchFrm();
                frmSrc.FormClosed += new FormClosedEventHandler(frm_FormClosed);
                frmSrc.ShowDialog();
            }

            void frm_FormClosed(object sender, FormClosedEventArgs e)
            {
                SearchFrm frmSrc = sender as SearchFrm;

                if (frmSrc.SelectedRow != null)
                {
                    ID = frmSrc.SelectedRow.ID_City;

                    using (EFPDEntities ctx = new EFPDEntities())
                    {
                        var query = from c in ctx.Cities
                                    where c.ID_City == ID
                                    select c;
                        firstBindingSource.DataSource = query.ToList();
                    }
                    
                }
                else
                {
                    infoID.Text = null;
                }
            }

    In riferimento a questo, e al precedente post, cosa mi consigli di modificare?

    giovedì 22 marzo 2012 10:53
  • Io farei qualcosa tipo:

    City lastSearchedCity = null;
    void frm_FormClosed(object sender, FormClosedEventArgs e)
    {
         SearchFrm frmSrc = sender as SearchFrm;

         ID = frmSrc.SelectedRow.ID_City; //qui potresti restituire direttamente la città volendo

         using (EFPDEntities ctx = new EFPDEntities())
         {
             var query = from c in ctx.Cities where c.ID_City == ID select c;
             lastSearchedCity = query.FirstOrDefault();
             firstBindingSource.DataSource = lastSearchedCity; //puoi mettere anche un solo oggetto
         }
    }

    Se le TextBox sono in Binding al firstBindignSource, l'oggetto lastSearchedCity viene modificato da solo quando modifichi i textbox.

    private void sUpdate()
    {
        using (EFPDEntities ctx = new EFPDEntities())
        {
             ctx.Cities.Attach(lastSearchedCity);
             ctx.ObjectStateManager.GetObjectStateEntry(lastSearchedCity).SetModified();
             try
             {
                   ctx.SaveChanges();
             }
             catch (OptimisticConcurrencyException)
             { //...

    • Contrassegnato come risposta piedatt80 venerdì 23 marzo 2012 11:19
    giovedì 22 marzo 2012 11:31
  • Ho modificato il codice come da te suggerito, ma accade questo:

    se salvo, mi aggiorna la colonna LastMod nel db (l'ho modificata in rowversion) ma non mi salva gli aggirnamenti;

    se salvo in maniera concorrenziale (apro due volte l'applicazione) mi intercetta l'eccezione di concorrenza e se gli dico di salvare, questa volta mi salva tutto correttamente.

    Ho impostato un breakpoint su

    ctx.ObjectStateManager.GetObjectStateEntry(lastSearchedCity).SetModified();

    e, tuttavia, in lastSearchCity mi fa vedere il dato aggiornato. Ma proseguendo non mi salva gli aggiornamenti. Comportamento strano.

    giovedì 22 marzo 2012 12:53
  • Già strano.

    Prova a cambiare in

    ctx.ObjectStateManager.GetObjectStateEntry(lastSearchedCity).ChangeState(EntityState.Modified);

    Ho provato ora così (EF4) e mi funziona.

    se no in rete ci sono procedure che chiamano SetModifiedProperty su tutte le proprietà (http://msdn.microsoft.com/en-us/magazine/cc700340.aspx)

    giovedì 22 marzo 2012 13:29
  • Buongiorno!!!

    Con grande gioia posso confermare che sostituendo con quanto da te suggerito, ovvero:

    ctx.ObjectStateManager.GetObjectStateEntry(lastSearchedCity).ChangeState(EntityState.Modified);

    il tutto funziona correttamente. Mi salva correttamente gli aggiornamenti ed in maniera concorrenziale mi avverte che i dati sono stati aggiornati da un altro utente. Proprio come doveva essere.

    Un ultima domanda, prima di chiudere questa discussione:

    quando - in maniera concorrenziale - decido di non persistere alcuna modifica quindi sono nel caso:

     case DialogResult.Cancel:
                                // cancel
                                ctx.Refresh(RefreshMode.ClientWins, com);
                                break;

    i dati visualizzati sul form spariscono. Diversamente, mi sarei aspettato un refresh con la visualizzazione dei dati aggiornati.

    venerdì 23 marzo 2012 08:54
  • Ciao.

    Riprovando sul test di ieri a me non spariscono.

    Comunque per aggiornare la entity con i dati presenti nello storage (e quindi annullare le modifiche dell'utente) devi specificare RefreshMode.StoreWins.

    (com cos'è? devi mettere lastSearchedCity).

    Ciao.

    venerdì 23 marzo 2012 09:48
  • Tutto risolto. Per la gioia mi sono dimenticato di prestare attenzione a ciò che fa il codice dopo che salvo le modifiche. Effettivamente dopo aver persistito i dati, gli dico di modificare lo stato. Quindi pulisco i textbox.

    Nel caso in cui l'utente, dopo che è stato avvisato che i dati da aggiornare sono obsoleti, decide di non persistere le modifiche penso (corereggimi se sbaglio) sia preferibile fargli vedere a video quali sono i dati aggiornati da altro utente.

    si, com è sbagliato, in realtà avevo già modificato con lastSearchedCity.

    Un consiglio che mi sento di dare a chi ha seguito  "quest'avventura" è quella di utilizzare sul Db rowversion, piuttosto che timestamp per la gestione dell OptimisticConcurrencyException.

    Un grazie a Sarati Roberto per avermi seguito!

    venerdì 23 marzo 2012 11:19