33A relationship is a named link between two resource types, including a direction.
44They are similar to [ navigation properties in Entity Framework Core] ( https://docs.microsoft.com/en-us/ef/core/modeling/relationships ) .
55
6- Relationships come in three flavors: to-one, to-many and many- to-many.
6+ Relationships come in two flavors: to-one and to-many.
77The left side of a relationship is where the relationship is declared, the right side is the resource type it points to.
88
99## HasOne
@@ -22,10 +22,14 @@ public class TodoItem : Identifiable<int>
2222
2323The left side of this relationship is of type ` TodoItem ` (public name: "todoItems") and the right side is of type ` Person ` (public name: "persons").
2424
25- ### Required one -to-one relationships in Entity Framework Core
25+ ### One -to-one relationships in Entity Framework Core
2626
27- By default, Entity Framework Core generates an identifying foreign key for a required 1-to-1 relationship.
28- This means no foreign key column is generated, instead the primary keys point to each other directly.
27+ By default, Entity Framework Core tries to generate an * identifying foreign key* for a one-to-one relationship whenever possible.
28+ In that case, no foreign key column is generated. Instead the primary keys point to each other directly.
29+
30+ ** That mechanism does not make sense for JSON: API , because patching a relationship would result in also
31+ changing the identity of a resource. Naming the foreign key explicitly fixes the problem, which enforces
32+ to create a foreign key column.**
2933
3034The next example defines that each car requires an engine, while an engine is optionally linked to a car.
3135
@@ -51,18 +55,19 @@ public sealed class AppDbContext : DbContext
5155 builder .Entity <Car >()
5256 .HasOne (car => car .Engine )
5357 .WithOne (engine => engine .Car )
54- .HasForeignKey <Car >()
55- .IsRequired ();
58+ .HasForeignKey <Car >();
5659 }
5760}
5861```
5962
6063Which results in Entity Framework Core generating the next database objects:
64+
6165``` sql
6266CREATE TABLE "Engine " (
6367 " Id" integer GENERATED BY DEFAULT AS IDENTITY,
6468 CONSTRAINT " PK_Engine" PRIMARY KEY (" Id" )
6569);
70+
6671CREATE TABLE "Cars " (
6772 " Id" integer NOT NULL ,
6873 CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
@@ -71,34 +76,126 @@ CREATE TABLE "Cars" (
7176);
7277```
7378
74- That mechanism does not make sense for JSON: API , because patching a relationship would result in also
75- changing the identity of a resource. Naming the foreign key explicitly fixes the problem by forcing to
76- create a foreign key column.
79+ To fix this, name the foreign key explicitly:
7780
7881``` c#
7982protected override void OnModelCreating (ModelBuilder builder )
8083{
8184 builder .Entity <Car >()
8285 .HasOne (car => car .Engine )
8386 .WithOne (engine => engine .Car )
84- .HasForeignKey <Car >(" EngineId" ) // Explicit foreign key name added
85- .IsRequired ();
87+ .HasForeignKey <Car >(" EngineId" ); // <-- Explicit foreign key name added
8688 }
8789```
8890
8991Which generates the correct database objects:
92+
9093``` sql
9194CREATE TABLE "Engine " (
9295 " Id" integer GENERATED BY DEFAULT AS IDENTITY,
9396 CONSTRAINT " PK_Engine" PRIMARY KEY (" Id" )
9497);
98+
9599CREATE TABLE "Cars " (
96100 " Id" integer GENERATED BY DEFAULT AS IDENTITY,
97101 " EngineId" integer NOT NULL ,
98102 CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
99103 CONSTRAINT " FK_Cars_Engine_EngineId" FOREIGN KEY (" EngineId" ) REFERENCES " Engine" (" Id" )
100104 ON DELETE CASCADE
101105);
106+
107+ CREATE UNIQUE INDEX "IX_Cars_EngineId " ON " Cars" (" EngineId" );
108+ ```
109+
110+ #### Optional one-to-one relationships in Entity Framework Core
111+
112+ For optional one-to-one relationships, Entity Framework Core uses ` DeleteBehavior.ClientSetNull ` by default, instead of ` DeleteBehavior.SetNull ` .
113+ This means that Entity Framework Core tries to handle the cascading effects (by sending multiple SQL statements), instead of leaving it up to the database.
114+ Of course that's only going to work when all the related resources are loaded in the change tracker upfront, which is expensive because it requires fetching more data than necessary.
115+
116+ The reason for this odd default is poor support in SQL Server, as explained [ here] ( https://stackoverflow.com/questions/54326165/ef-core-why-clientsetnull-is-default-ondelete-behavior-for-optional-relations ) and [ here] ( https://learn.microsoft.com/en-us/ef/core/saving/cascade-delete#database-cascade-limitations ) .
117+
118+ ** Our [ testing] ( https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1205 ) shows that these limitations don't exist when using PostgreSQL.
119+ Therefore the general advice is to map the delete behavior of optional one-to-one relationships explicitly with ` .OnDelete(DeleteBehavior.SetNull) ` . This is simpler and more efficient.**
120+
121+ The next example defines that each car optionally has an engine, while an engine is optionally linked to a car.
122+
123+ ``` c#
124+ #nullable enable
125+
126+ public sealed class Car : Identifiable <int >
127+ {
128+ [HasOne ]
129+ public Engine ? Engine { get ; set ; }
130+ }
131+
132+ public sealed class Engine : Identifiable <int >
133+ {
134+ [HasOne ]
135+ public Car ? Car { get ; set ; }
136+ }
137+
138+ public sealed class AppDbContext : DbContext
139+ {
140+ protected override void OnModelCreating (ModelBuilder builder )
141+ {
142+ builder .Entity <Car >()
143+ .HasOne (car => car .Engine )
144+ .WithOne (engine => engine .Car )
145+ .HasForeignKey <Car >(" EngineId" );
146+ }
147+ }
148+ ```
149+
150+ Which results in Entity Framework Core generating the next database objects:
151+
152+ ``` sql
153+ CREATE TABLE "Engines " (
154+ " Id" integer GENERATED BY DEFAULT AS IDENTITY,
155+ CONSTRAINT " PK_Engines" PRIMARY KEY (" Id" )
156+ );
157+
158+ CREATE TABLE "Cars " (
159+ " Id" integer GENERATED BY DEFAULT AS IDENTITY,
160+ " EngineId" integer NULL ,
161+ CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
162+ CONSTRAINT " FK_Cars_Engines_EngineId" FOREIGN KEY (" EngineId" ) REFERENCES " Engines" (" Id" )
163+ );
164+
165+ CREATE UNIQUE INDEX "IX_Cars_EngineId " ON " Cars" (" EngineId" );
166+ ```
167+
168+ To fix this, set the delete behavior explicitly:
169+
170+ ```
171+ public sealed class AppDbContext : DbContext
172+ {
173+ protected override void OnModelCreating(ModelBuilder builder)
174+ {
175+ builder.Entity<Car>()
176+ .HasOne(car => car.Engine)
177+ .WithOne(engine => engine.Car)
178+ .HasForeignKey<Car>("EngineId")
179+ .OnDelete(DeleteBehavior.SetNull); // <-- Explicit delete behavior set
180+ }
181+ }
182+ ```
183+
184+ Which generates the correct database objects:
185+
186+ ``` sql
187+ CREATE TABLE "Engines " (
188+ " Id" integer GENERATED BY DEFAULT AS IDENTITY,
189+ CONSTRAINT " PK_Engines" PRIMARY KEY (" Id" )
190+ );
191+
192+ CREATE TABLE "Cars " (
193+ " Id" integer GENERATED BY DEFAULT AS IDENTITY,
194+ " EngineId" integer NULL ,
195+ CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
196+ CONSTRAINT " FK_Cars_Engines_EngineId" FOREIGN KEY (" EngineId" ) REFERENCES " Engines" (" Id" ) ON DELETE SET NULL
197+ );
198+
102199CREATE UNIQUE INDEX "IX_Cars_EngineId " ON " Cars" (" EngineId" );
103200```
104201
0 commit comments