Date queste classi:
public class A
{
public int Id {get; set;}
public int? BId {get; set;}
public B B {get; set;}
}
public class B
{
public int Id {get; set;}
public int? AId {get; set;}
public A A {get; set;}
}
Quindi con l'API Fluent
modelBuilder.Entity<A>()
.HasOne(a => a.B)
.WithOne(b => b.A)
.HasForeignKey<A>(a => a.BId);
Quando si creano oggetti e li si aggiunge al database, le cose sembrano seguire nelle tabelle corrispondenti:
Quando recupero i dati usando EF Core:
Cosa devo fare per impostare B.AId?
Queste relazioni 0..1 : 0..1
sono generalmente definite tra entità di cui nessuna è un'entità principale ovvia. Mi piace l'esempio di auto e driver, che è un po 'più immaginabile di A e B.
Il modello che stai dopo assomiglia a questo:
Esistono due chiavi esterne comuni, entrambe con un indice univoco per applicare 1: 1 a livello di database.
Il HasOne - WithOne
non può essere utilizzato qui, perché richiede sempre un'istruzione HasForeignKey
per stabilire quale entità è principale. Questo configura anche un solo campo come chiave esterna. Nel tuo esempio, B.AId
è solo un campo regolare. Se non gli dai un valore, EF no.
La mappatura del modello sopra è un po 'più ingombrante di HasOne - WithOne
:
var carEtb = modelBuilder.Entity<Car>();
var driverEtb = modelBuilder.Entity<Driver>();
carEtb.HasOne(c => c.Driver).WithMany();
carEtb.HasIndex(c => c.DriverID).IsUnique();
driverEtb.HasOne(d => d.Car).WithMany();
driverEtb.HasIndex(c => c.CarID).IsUnique();
Quindi ci sono due associazioni 0..1: n rese uniche dagli indici sulle chiavi esterne.
Che crea il seguente modello di database:
CREATE TABLE [Drivers] (
[ID] int NOT NULL IDENTITY,
[Name] nvarchar(max) NULL,
[CarID] int NULL,
CONSTRAINT [PK_Drivers] PRIMARY KEY ([ID])
);
CREATE TABLE [Cars] (
[ID] int NOT NULL IDENTITY,
[Brand] nvarchar(max) NULL,
[Type] nvarchar(max) NULL,
[DriverID] int NULL,
CONSTRAINT [PK_Cars] PRIMARY KEY ([ID]),
CONSTRAINT [FK_Cars_Drivers_DriverID] FOREIGN KEY ([DriverID])
REFERENCES [Drivers] ([ID]) ON DELETE NO ACTION
);
CREATE UNIQUE INDEX [IX_Cars_DriverID] ON [Cars] ([DriverID])
WHERE [DriverID] IS NOT NULL;
CREATE UNIQUE INDEX [IX_Drivers_CarID] ON [Drivers] ([CarID])
WHERE [CarID] IS NOT NULL;
ALTER TABLE [Drivers] ADD CONSTRAINT [FK_Drivers_Cars_CarID] FOREIGN KEY ([CarID])
REFERENCES [Cars] ([ID]) ON DELETE NO ACTION;
Crea due chiavi esterne nullable sia indicizzate da un indice filtrato univoco. Perfezionare!
Ma...
EF non la vede come una relazione bidirezionale uno-a-uno. E giustamente. I due FK sono solo questo, due FK indipendenti. Tuttavia, in considerazione dell'integrità dei dati, la relazione deve essere stabilita da entrambe le parti: se un conducente reclama un'automobile (imposta driver.CarID
), l'auto deve essere collegata anche al conducente (impostare car.DriverID
), altrimenti un altro conducente potrebbe essere collegato ad esso.
Quando l'auto esistente e i driver sono accoppiati, potrebbe essere utilizzato un piccolo metodo di supporto, ad esempio in Car
:
public void SetDriver(Driver driver)
{
Driver = driver;
driver.Car = this;
}
Tuttavia, quando sia Car
un Driver
sono creati e associati in un unico processo, questo è maldestro. EF lancia una InvalidOperationException
:
Impossibile salvare le modifiche perché è stata rilevata una dipendenza circolare nei dati da salvare: "Car [Aggiunto] <- Car {'CarID'} Driver [Aggiunto] <- Driver {'DriverID'} Car [Aggiunto] '.
Il che significa che uno degli FK può essere impostato in una sola volta, ma l'altro può essere impostato solo dopo aver salvato i dati. Ciò richiede due chiamate SaveChanges
racchiuse da una transazione in un pezzo di codice piuttosto imperativo:
using (var db = new MyContext())
{
using (var t = db.Database.BeginTransaction())
{
var jag = new Car { Brand = "Jaguar", Type = "E" };
var peter = new Driver { Name = "Peter Sellers", Car = jag };
db.Drivers.Add(peter);
db.SaveChanges();
jag.Driver = peter;
db.SaveChanges();
t.Commit();
}
}
Quindi ora il motivo per cui vado da queste parti spiegando tutto questo: a mio parere, le associazioni 0..1 : 0..1
dovrebbero essere modellate da una tabella di giunzioni con chiavi esterne uniche:
Utilizzando una tabella di giunzione -
Questo modello può essere implementato da questo modello di classe:
public class Car
{
public int ID { get; set; }
public string Brand { get; set; }
public string Type { get; set; }
public CarDriver CarDriver { get; set; }
}
public class Driver
{
public Driver()
{ }
public int ID { get; set; }
public string Name { get; set; }
public CarDriver CarDriver { get; set; }
}
public class CarDriver
{
public int CarID { get; set; }
public Car Car { get; set; }
public int DriverID { get; set; }
public virtual Driver Driver { get; set; }
}
E la mappatura:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var carDriverEtb = modelBuilder.Entity<CarDriver>();
carDriverEtb.HasKey(cd => new { cd.CarID, cd.DriverID });
carDriverEtb.HasIndex(cd => cd.CarID).IsUnique();
carDriverEtb.HasIndex(cd => cd.DriverID).IsUnique();
}
Ora è possibile creare driver e auto e le relative associazioni in un'unica chiamata SaveChanges
:
using (var db = new MyContext(connectionString))
{
var ford = new Car { Brand = "Ford", Type = "Mustang" };
var jag = new Car { Brand = "Jaguar", Type = "E" };
var kelly = new Driver { Name = "Kelly Clarkson" };
var peter = new Driver { Name = "Peter Sellers" };
db.CarDrivers.Add(new CarDriver { Car = ford, Driver = kelly });
db.CarDrivers.Add(new CarDriver { Car = jag, Driver = peter });
db.SaveChanges();
}
L'unico inconveniente è che la navigazione da Car
a Driver
vv è un po 'comoda. Bene, vedi di persona quale modello ti soddisfa meglio.