3
3
A relationship is a named link between two resource types, including a direction.
4
4
They are similar to [ navigation properties in Entity Framework Core] ( https://docs.microsoft.com/en-us/ef/core/modeling/relationships ) .
5
5
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.
7
7
The left side of a relationship is where the relationship is declared, the right side is the resource type it points to.
8
8
9
9
## HasOne
@@ -22,10 +22,14 @@ public class TodoItem : Identifiable<int>
22
22
23
23
The left side of this relationship is of type ` TodoItem ` (public name: "todoItems") and the right side is of type ` Person ` (public name: "persons").
24
24
25
- ### Required one -to-one relationships in Entity Framework Core
25
+ ### One -to-one relationships in Entity Framework Core
26
26
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.**
29
33
30
34
The next example defines that each car requires an engine, while an engine is optionally linked to a car.
31
35
@@ -51,18 +55,19 @@ public sealed class AppDbContext : DbContext
51
55
builder .Entity <Car >()
52
56
.HasOne (car => car .Engine )
53
57
.WithOne (engine => engine .Car )
54
- .HasForeignKey <Car >()
55
- .IsRequired ();
58
+ .HasForeignKey <Car >();
56
59
}
57
60
}
58
61
```
59
62
60
63
Which results in Entity Framework Core generating the next database objects:
64
+
61
65
``` sql
62
66
CREATE TABLE "Engine " (
63
67
" Id" integer GENERATED BY DEFAULT AS IDENTITY,
64
68
CONSTRAINT " PK_Engine" PRIMARY KEY (" Id" )
65
69
);
70
+
66
71
CREATE TABLE "Cars " (
67
72
" Id" integer NOT NULL ,
68
73
CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
@@ -71,34 +76,126 @@ CREATE TABLE "Cars" (
71
76
);
72
77
```
73
78
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:
77
80
78
81
``` c#
79
82
protected override void OnModelCreating (ModelBuilder builder )
80
83
{
81
84
builder .Entity <Car >()
82
85
.HasOne (car => car .Engine )
83
86
.WithOne (engine => engine .Car )
84
- .HasForeignKey <Car >(" EngineId" ) // Explicit foreign key name added
85
- .IsRequired ();
87
+ .HasForeignKey <Car >(" EngineId" ); // <-- Explicit foreign key name added
86
88
}
87
89
```
88
90
89
91
Which generates the correct database objects:
92
+
90
93
``` sql
91
94
CREATE TABLE "Engine " (
92
95
" Id" integer GENERATED BY DEFAULT AS IDENTITY,
93
96
CONSTRAINT " PK_Engine" PRIMARY KEY (" Id" )
94
97
);
98
+
95
99
CREATE TABLE "Cars " (
96
100
" Id" integer GENERATED BY DEFAULT AS IDENTITY,
97
101
" EngineId" integer NOT NULL ,
98
102
CONSTRAINT " PK_Cars" PRIMARY KEY (" Id" ),
99
103
CONSTRAINT " FK_Cars_Engine_EngineId" FOREIGN KEY (" EngineId" ) REFERENCES " Engine" (" Id" )
100
104
ON DELETE CASCADE
101
105
);
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
+
102
199
CREATE UNIQUE INDEX "IX_Cars_EngineId " ON " Cars" (" EngineId" );
103
200
```
104
201
0 commit comments