234 messaggi dal 08 marzo 2012
Ciao a tutti,

sto utilizzando per la prima volta Entity Framework Code First ed avrei alcune domande relative a degli aspetti che mi sono poco chiari.
Ho due soli oggetti per ora: User e UserProfile

Le tabelle sono state correttamente create in SQL Server e tra i due oggetti c'è una property sull'oggetto User, chiamata Profile, che fa riferimento all'istanza "UserProfile". Ho inoltre creare un campo INT chiamato UserProfileId (come da convenzione richiesta).

Ecco le mie domande:

1) perchè è necessario creare il campo UserProfileId? Non è sufficiente avere la property Profile?
2) durante il salvataggio "context.SaveChanges()" devo impostare manualmente UserProfileId=user.Profile.Id, altrimenti mi solleva un'eccezione in quanto mi dice che i due valori non corrispondono. Come mai questa cosa? Mi sembra piuttosto strano.
3) in User la property "Profile" è impostata a "virtual" per fare uso del lazy loading (almeno così ho capito dalla documentazione) ma non capisco perchè poi in una query Linq devo esplicitamente dirgli di fare ".Include("Profile")". Come si combinano correttamente le due cose?
4) Al salvataggio inoltre dello User ho dovuto mettergli un controllo del tipo "se user.Id==0" imposta lo stato della entity a "Nuovo" altrimenti impostalo a "Modificato", altrimenti continuava a crearmi nuovi record nel db anche la chiave era la stessa. E' corretto questo modo di procedere?
5) Stessa cosa per la foreign key associata (ovvero la classe Profile) se non imposto la stato ad "Unchanged" continua a crearmi nuovi record nella tabella esterna collegata "UserProfile" ogni volta che salvo un nuovo utente "User". Come si può gestire più correttamente questa cosa?
6) Come posso gestire una transazione, ovvero applicare più modifiche al db e dare il commit/rollback solo alla fine?

Grazie mille in anticipo
Buona giornata
Modificato da evil80 il 11 agosto 2016 14.59 -
11.886 messaggi dal 09 febbraio 2002
Contributi
Ciao,
puoi spiegare meglio come hai mappato la relazione tra User e UserProfile? C'è questa cosa che mi confonde un po':

devo impostare manualmente UserProfileId=user.Profile.Id


Tipicamente, tra User e UserProfile c'è una relazione uno-a-uno che si realizza così:
  • L'entità principale è User (infatti non può esistere uno UserProfile senza User) ed ha una chiave primaria UserId autoincrementante (se vuoi).
  • L'entità dipendente è UserProfile e la sua chiave primaria è anch'essa chiamata UserId ma non è autoincrementante perché deve assumere lo stesso valore che ha la chiave User.UserId.
    UserProfile.UserId è quindi anche chiave esterna verso User.UserId.

La relazione è spiegata qui con un esempio. Ci trovi anche il mapping da usare.
http://www.entityframeworktutorial.net/code-first/configure-one-to-one-relationship-in-code-first.aspx


3) in User la property "Profile" è impostata a "virtual" per fare uso del lazy loading (almeno così ho capito dalla documentazione) ma non capisco perchè poi in una query Linq devo esplicitamente dirgli di fare ".Include("Profile")".

Non "devi" fare l'Include ma "puoi" farlo.
EF ti offre queste possibilità quando si tratta di caricare entità correlate.
  • Se nel contesto corrente devi solo leggere le proprietà di User, allora l'Include e la funzionalità di Lazy Loading non ti serviranno. In questo modo non coinvolgerai affatto l'entità UserProfile ed Entity Framework perciò non dovrà fare delle Join nella query SQL che invierà al database.
  • Se il tuo codice prevede *alcune* path di esecuzione (non tutte) che accendono ai dati di UserProfile, allora neanche in questo caso potrebbe servirti l'Include. Puoi lasciare che sia la funzionalità di Lazy loading a caricarti lo UserProfile al volo solo nel caso in cui l'esecuzione passi per la path che legge le sue proprietà. Attenzione perché con il Lazy Loading è facile perdere di vista la quantità di query che EF invia al database. Se sfrutti il Lazy loading per leggere lo UserProfile di 1000 utenti, allora EF invierà 1+1000 query al database (la prima per recuperare la lista di User, e le altre mille per recuperare ciascun singolo UserProfile). Questo è noto come problema del SELECT n+1 e per evitare di incapparci inavvertitamente, alcuni sviluppatori preferiscono disabilitare il lazy loading.
  • Se invece sai per certo che dovrai leggere sia le proprietà dello User che del suo UserProfile, allora ti conviene indicare ad EF che deve caricarti tutte le entità subito, con un'unica query. Questo approccio si chiama eager loading



Non è sufficiente avere la property Profile?

In certi casi ti è sufficiente conoscere l'ID dell'entità correlata e per questo non vuoi andare a caricare tutto l'oggetto. In EF6 è obbligatorio avere la chiave esterna nell'entità, mentre in EF Core no.


o "se user.Id==0" imposta lo stato della entity a "Nuovo" altrimenti impostalo a "Modificato", altrimenti continuava a crearmi nuovi record nel db anche la chiave era la stessa. E' corretto questo modo di procedere?

Non so che tipo di applicazione stai sviluppando ma, se è ASP.NET MVC, ti conviene prendere l'ID dalla route. Per nuovi Utenti, puoi usare un url tipo /Users/Edit, mentre per la modifica di User esistenti puoi usare /Users/Edit/1.

Nel caso in cui l'id sia null, aggiungi una nuova istanza al dbcontext, altrimenti fai una query per recuperare l'entità dal db.
Poi, usa il metodo TryUpdateModel per modificare automaticamente i valori delle proprietà secondo i valori arrivati dal form. E' un sistema che puoi usare anche con WebForms. Leggi qua, in particolare i commenti in fondo.
http://www.aspitalia.com/script/1196/Model-Binding-Asincrono-ASP.NET-WebForms-4.6.aspx


se non imposto la stato ad "Unchanged" continua a crearmi nuovi record nella tabella esterna collegata "UserProfile" ogni volta che salvo un nuovo utente "User"

Credo che tu non abbia mappato correttamente la relazione o creato uno schema adeguato nel db.
Mappa una relazione uno-ad-uno e poi lascia creare il db da Entity Framework Code First. Vedi che tabelle ti genera per capire come funziona la relazione a livello relazionale. In quel modo è impossibile che vengano creato duplicati, perché la foreign key che si trova su UserProfile sarà anche la primary key, e non ammetterà duplicati.


6) Come posso gestire una transazione, ovvero applicare più modifiche al db e dare il commit/rollback solo alla fine?

La transazione è implicita. Nel momento in cui invochi il SaveChanges, EF creerà una transazione e nel suo ambito invierà tutti i comandi SQL che servono a persistere le modifiche che hai apportato dalla creazione del DbContext. Se si verifica un errore nell'inserimento, l'intera transazione verrà Rollbackata.

Se vuoi modificare il livello di isolamento della transazione o se vuoi far partecipare EF ad una transazione già creata da te, allora guarda i metodi BeginTransaction e UseTransaction illustrati qui:
https://msdn.microsoft.com/en-us/data/dn456843.aspx

ciao,
Moreno

Enjoy learning and just keep making
234 messaggi dal 08 marzo 2012
Ciao,

di seguito riporto il codice di dettaglio delle classi:

  public class User : GeneralObject
    {
        public User()
        {
        }

        [Key]
        public int UserId { get; set; }

        [StringLength(50), Required]
        public string Username { get; set; }

        [StringLength(350), Required]
        public string Password { get; set; }
        [StringLength(40), Required]
        public string FirstName { get; set; }
        [StringLength(40), Required]
        public string LastName { get; set; }

        [NotMapped]
        public string FullName {
            get { return string.Format("{0} {1}", LastName, FirstName); }
        }

        [StringLength(50), Required]
        public string Email { get; set; }
        [StringLength(30)]
        public string Phone { get; set; }
        [StringLength(30)]
        public string Fax { get; set; }

        public bool IsActive { get; set; }
        public bool IsAdministrator { get; set; }

        public int UserProfileId { get; set; }
        public virtual UserProfile Profile { get; set; }
    }
  
  public class UserProfile: GeneralObject
    {
        public UserProfile()
        {
        }

        public int UserProfileId { get; set; }

        [StringLength(50), Required]
        public string Description { get; set; }
    }
  
  public abstract class GeneralObject
    {
        public GeneralObject()
        {
            CreatedOn = DateTime.Now;
            LastModifiedOn = DateTime.Now;
        }

        [StringLength(36), Required]
        public string Guid { get; set; }

        [StringLength(36)]
        public string CreatedBy { get; set; }
        public DateTime CreatedOn { get; set; }
        [NotMapped]
        public string CreatedOnAsString {
            get { return CreatedOn.ToString("dd/MM/yyyy"); }
        }

        public DateTime LastModifiedOn { get; set; }
        [NotMapped]
        public string LastModifiedOnAsString
        {
            get { return LastModifiedOn.ToString("dd/MM/yyyy"); }
        }
        [StringLength(36)]
        public string ModifiedBy { get; set; }
    }



Tipicamente, tra User e UserProfile c'è una relazione uno-a-uno che si realizza così:


La relazione è uno-a-molti, infatti sullo stesso profilo posso avere più User.
Mi sembra di aver creato correttamente la relazione, sbaglio?
Infatti anche la struttura del db generata da Code First mi torna, ovvero User ha una foreign key verso la chiave primaria della tabella UserProfile.

Come ti dicevo se non gli do il comando user.ProfileId = user.Profile.Id non mi permette di salvare in quanto va in eccezione.


Non "devi" fare l'Include ma "puoi" farlo.
EF ti offre queste possibilità quando si tratta di caricare entità correlate.


Sto sviluppando un'applicazione MVC basata su AngularJS non capisco però come possa in questo contesto funzionare il Lazy Loading.
AngularJS dovrebbe invocare il metodo del controller e quindi richiedere la selezione dei dati di UseProfile.
Se imposto "Include" ottengo che la query sul db è una sola anche se avessi 1.000 utenti?


Nel caso in cui l'id sia null, aggiungi una nuova istanza al dbcontext, altrimenti fai una query per recuperare l'entità dal db.
Poi, usa il metodo TryUpdateModel per modificare automaticamente i valori delle proprietà secondo i valori arrivati dal form. E' un sistema che puoi usare anche con WebForms. Leggi qua, in particolare i commenti in fondo.


Uso come dicevo MVC con Angular...non credo che TryUpdateModel possa venirmi incontro...o sbaglio?


Credo che tu non abbia mappato correttamente la relazione o creato uno schema adeguato nel db.
Mappa una relazione uno-ad-uno e poi lascia creare il db da Entity Framework Code First. Vedi che tabelle ti genera per capire come funziona la relazione a livello relazionale. In quel modo è impossibile che vengano creato duplicati, perché la foreign key che si trova su UserProfile sarà anche la primary key, e non ammetterà duplicati.


Lo credo anche io ma non capisco cosa ci sia di sbagliato...mi sembra piuttosto semplice il modello.

Grazie mille!
Ciao
Modificato da evil80 il 11 agosto 2016 21.45 -
Modificato da evil80 il 11 agosto 2016 22.15 -
11.886 messaggi dal 09 febbraio 2002
Contributi
Ciao, prego!


La relazione è uno-a-molti, infatti sullo stesso profilo posso avere più User.

Allora ok, la relazione è modellata correttamente. In questo caso UserProfile è l'entità principale e gli User sono le dipendenti.


Come ti dicevo se non gli do il comando user.ProfileId = user.Profile.Id non mi permette di salvare in quanto va in eccezione.

Secondo il codice che hai postato, non è user.ProfileId, ma user.UserProfileId.
Sembra una stupidaggine ma è importante notarlo perchè la proprietà di navigazione relativa a questa foreign key si chiama semplciemente Profile e non UserProfile.
Questo non rispetta la convenzione di nomi e perciò EF non sta capendo che quella è la proprietà di navigazione relativa a UserProfileId.
Dovresti o cambiargli nome o legarle esplicitamente usando l'attributo ForeignKey.
[ForeignKey(nameof(Profile))]
public int UserProfileId { get; set; }

Nota al margine: a questo punto è indifferente valorizzare la proprietà foreign key (UserProfileId) o la proprietà di navigazione (Profile). Entity Framework farà il cosiddetto "relationship fixup" al SaveChanges, e renderà entrambe le proprietà coerenti.


Sto sviluppando un'applicazione MVC basata su AngularJS non capisco però come possa in questo contesto funzionare il Lazy Loading.

Non ha importanza quale sia il linguaggio o la tecnologia usata client-side. Lazy loading e Include sono funzionalità di Entity Framework.
Se stai visualizzando un elenco di tutti i profili (e dei relativi utenti), allora dovresti usare l'Include, in modo che EF non generi molte query al database.
A prescindere dal sistema di caricamento usato con EF, il risultato si presenterà ad Angular come un unico array JSON (o altro formato di serializzazione che hai scelto di usare).


Se imposto "Include" ottengo che la query sul db è una sola anche se avessi 1.000 utenti?

Sì, EF farà una SQL Join per estrarre con un colpo solo sia le informazioni sugli User che sugli UserProfile.
Ovviamente cerca di non estrarre 1000 utenti, ma solo quelli che visualizzi effettivamente nella pagina. Quando l'utente va alla pagina successiva, allora invia una nuova richiesta ajax per recuperare altri risultati.


Uso come dicevo MVC con Angular...non credo che TryUpdateModel possa venirmi incontro...o sbaglio?

Beh, potresti pur sempre dare ai tuoi campi input dei name che seguono la convenzione di MVC, in modo che sfrutti il suo model binder. Ecco un esempio (non con Angular ma il principio è lo stesso).
http://forum.aspitalia.com/forum/post/403972/Inserire-Lista-Proprieta-Model-Interno-Form.aspx#404046
Se segui questa convenzione, puoi usare TryUpdateModel. Ha senso nelle applicazioni data-driven.

Inoltre, hai valutato ASP.NET Web API al posto di MVC? Ti avrebbe permesso di postare oggetti JSON alle action. Più utile nelle interfacce task-based.

ciao,
Moreno
Modificato da BrightSoul il 11 agosto 2016 22.47 -

Enjoy learning and just keep making
234 messaggi dal 08 marzo 2012
Ciao,


Secondo il codice che hai postato, non è user.ProfileId, ma user.UserProfileId.
Sembra una stupidaggine ma è importante notarlo perchè la proprietà di navigazione relativa a questa foreign key si chiama semplciemente Profile e non UserProfile.
Questo non rispetta la convenzione di nomi e perciò EF non sta capendo che quella è la proprietà di navigazione relativa a UserProfileId.
Dovresti o cambiargli nome o legarle esplicitamente usando l'attributo ForeignKey.
[ForeignKey(nameof(Profile))]
public int UserProfileId { get; set; }

Nota al margine: a questo punto è indifferente valorizzare la proprietà foreign key (UserProfileId) o la proprietà di navigazione (Profile). Entity Framework farà il cosiddetto "relationship fixup" al SaveChanges, e renderà entrambe le proprietà coerenti.


Ho rivisto il modello secondo le tue indicazioni ma continuo ad ottenere questo errore "A referential integrity constraint violation occurred: The property value(s) of 'UserProfile.UserProfileId' on one end of a relationship do not match the property value(s) of 'User.UserProfileId' on the other end." se non imposto io a mano user.UserProfileId = user.UserProfile.UserProfileId

Inoltre sempre nel DataService che ho creato se non gli imposto specificatamente queste istruzioni
ctx.Entry(user.UserProfile).State = EntityState.Unchanged;
                ctx.Entry(user).State = user.UserId == 0 ? EntityState.Added : EntityState.Modified;
                ctx.SaveChanges();
non mi funziona correttamente.

Ti riporto di seguito il nuovo modello rivisto su cui ho rigenerato il db, ma con gli stessi problemi di prima.

  public class User : GeneralObject
    {
        public User()
        {
        }

        [Key]
        public int UserId { get; set; }

        [StringLength(50), Required]
        public string Username { get; set; }

        [StringLength(350), Required]
        public string Password { get; set; }
        [StringLength(40), Required]
        public string FirstName { get; set; }
        [StringLength(40), Required]
        public string LastName { get; set; }

        [NotMapped]
        public string FullName {
            get { return string.Format("{0} {1}", LastName, FirstName); }
        }

        [StringLength(50), Required]
        public string Email { get; set; }
        [StringLength(30)]
        public string Phone { get; set; }
        [StringLength(30)]
        public string Fax { get; set; }

        public bool IsActive { get; set; }
        public bool IsAdministrator { get; set; }

        public int UserProfileId { get; set; }
        public virtual UserProfile UserProfile { get; set; }
    }
  
  public class UserProfile: GeneralObject
    {
        public UserProfile()
        {
        }

        public int UserProfileId { get; set; }

        [StringLength(50), Required]
        public string Description { get; set; }
    }
  
  public abstract class GeneralObject
    {
        public GeneralObject()
        {
            CreatedOn = DateTime.Now;
            LastModifiedOn = DateTime.Now;
        }

        [StringLength(36), Required]
        public string Guid { get; set; }

        [StringLength(36)]
        public string CreatedBy { get; set; }
        public DateTime CreatedOn { get; set; }
        [NotMapped]
        public string CreatedOnAsString {
            get { return CreatedOn.ToString("dd/MM/yyyy"); }
        }

        public DateTime LastModifiedOn { get; set; }
        [NotMapped]
        public string LastModifiedOnAsString
        {
            get { return LastModifiedOn.ToString("dd/MM/yyyy"); }
        }
        [StringLength(36)]
        public string ModifiedBy { get; set; }
    }


Hai qualche idea di cosa sto sbagliando?
234 messaggi dal 08 marzo 2012
Stavo pensando...potrebbe dipendere dal fatto che sul frontend tramite AngularJS faccio questo

<select class="select2_single form-control" ng-model="CurrentUser.UserProfile" required="required" id="user-profile" ng-options="profile.Description for profile in ProfilesList track by profile.Guid"></select>


In particolare ng-model è associato a CurrentUser.UserProfile
Questo nel momento in cui Angular mi invierà i dati di fatto avrà modificato il valore dell'id user.UserProfile.UserProfileId ma non può cambiarmi la property user.UserProfileId e quindi il disallineamento avviene.

Se non lo imposto manualmente mi da sempre errore.

Dici possa dipendere da questo?
11.886 messaggi dal 09 febbraio 2002
Contributi

Questo nel momento in cui Angular mi invierà i dati di fatto avrà modificato il valore dell'id user.UserProfile.UserProfileId ma non può cambiarmi la property user.UserProfileId e quindi il disallineamento avviene.

Non so, prima di ragionare su questo bisogna vedere come ricostruisci il grafo di oggetti lato server, che poi andrai a persistere con Entity Framework.
Se provi a persistere il grafo di oggetti ricreato dal model binder, avrai tanti oggetti che non sono tracciati dal DbContext e che perciò verranno trattati come nuovi.
A meno che ovviamente tu non stia modificando a mano il loro EntityState.
Per questo è importante vedere prima la logica che hai nell'action del controller MVC. E per questo sarebbe importante usare TryUpdateModel, perché ti dà l'opportunità di aggiornare sul posto delle entità che hai recuperato con una query LINQ (se esistenti) e sono perciò già tracciate dal contesto.


tramite AngularJS faccio questo

Usa come model la foreign key anziché la proprietà di navigazione. Poi nelle ng-options usi select as per assegnare l'id del profilo al modello.
https://docs.angularjs.org/api/ng/directive/ngOptions#-select-as-

ciao,
Moreno

Enjoy learning and just keep making
234 messaggi dal 08 marzo 2012
BrightSoul ha scritto:

Non so, prima di ragionare su questo bisogna vedere come ricostruisci il grafo di oggetti lato server, che poi andrai a persistere con Entity Framework.
Se provi a persistere il grafo di oggetti ricreato dal model binder, avrai tanti oggetti che non sono tracciati dal DbContext e che perciò verranno trattati come nuovi.
A meno che ovviamente tu non stia modificando a mano il loro EntityState.
Per questo è importante vedere prima la logica che hai nell'action del controller MVC. E per questo sarebbe importante usare TryUpdateModel, perché ti dà l'opportunità di aggiornare sul posto delle entità che hai recuperato con una query LINQ (se esistenti) e sono perciò già tracciate dal contesto.


Nel controller non applico alcuna logica per ricostruire il grafo, in quanto Angular mi rispedisce lo stesso identico oggetto che gli ho consegnato quando ha fatto la GET al controller.
Ecco il codice del controller

  public ActionResult UpsertUser(Model.User.User user)
        {
            try
            {
                Model.User.User userConnected = DataServices.Token.TokenServices.GetUser(SecurityToken.GetToken(), DataServices.Support.Config.SessionDuration);
                Model.Authorization.Authorization authorization = DataServices.Authorization.AuthorizationServices.Load(user.Guid, userConnected);
                authorization.User = userConnected;

                DataServices.User.UserServices us = new DataServices.User.UserServices();

                user.ModifiedBy = userConnected.Guid;
                us.Upsert(user);

                return new HttpStatusCodeResult(HttpStatusCode.OK);
            }
            catch (Exception ex)
            {
                log.Error(ex.ToString());
                return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
            }
        }


Il parametro user viene automaticamente "bindato" quando Angular fa la POST.
Lato client modifico i valori deile property facendo semplicemente binding tramite ng-model.

Qui invece il codice del DataService che persiste tramite EF

  public void Upsert(Model.User.User user)
        {
            using (var ctx = new ImaContext())
            {
                if (user.UserId == 0)
                {
                    user.Guid = Guid.NewGuid().ToString();
                    user.CreatedOn = DateTime.Now;
                }

                user.LastModifiedOn = DateTime.Now;

                user.UserProfileId = user.UserProfile.UserProfileId;
                ctx.UserProfiles.Attach(user.UserProfile);
                ctx.Entry(user).State = user.UserId == 0 ? EntityState.Added : EntityState.Modified;
                ctx.SaveChanges();
            }
        }


Le 3 righe di codice prima di ctx.Savehanges() sono quelle che mi tocca inserire per far funzionare il tutto.

Vedi degli errori di logica?

Grazie!
Modificato da evil80 il 12 agosto 2016 12.45 -

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.