The Same Relationship, Seen From Both Sides
Why an ORM can never assume two matching fields describe the same relationship, the fix that stops it representing one twice, and watching the whole schema generate itself.
City holds a list of Theatre objects, and Theatre holds a single City back. Whether that's one relationship, described from two sides, or two genuinely separate relationships, was left open earlier in this series.
Why an ORM Can't Assume It's the Same Relationship
The two fields could, in principle, mean something completely different from each other. Picture a Theatre holding a City meant as its "inspiration," a city its architecture is styled after, entirely separate from which city the theatre actually operates in.
That's not far-fetched. This series already showed the same two classes carrying more than one relationship at once, when Group and User needed separate references for who created a group, its members, its admins, and who it had blocked. Nothing stops City and Theatre from having two genuinely different relationships that both happen to use the same field types.
An ORM has no way to rule that out on its own. It sees two fields, one on each class, and no way to know whether they're mirror images of the same relationship or two independent ones that simply look alike.
What Happens If You Never Say Which It Is
Left unspecified, an ORM defaults to treating both fields as their own separate relationship. That means the same real-world fact, this theatre exists in this city, gets represented twice, once from each class's own foreign key or join table.
That's not just wasted storage. Updating one side, moving a theatre to a different city, has no way to guarantee the other side's copy of the same fact gets updated too. The two representations can drift apart, and the database ends up telling two different, contradictory stories about the same relationship.
The Fix: Point One Side at the Other
The fix is to tell the ORM directly, on one of the two sides, that this field isn't its own relationship at all. It's the same relationship the other class already owns, just viewed from here, and the ORM should skip representing it a second time.
That means naming the actual field, on the other class, that owns the real relationship. On Theatre, that's a plain City reference. So City's own list of Theatre gets marked as owned by that field, not as a second, independent relationship of its own.
The same fix applies anywhere a relationship gets written from both sides. Ticket holds a list of Payment, and Payment holds a single Ticket back. Once one direction is settled, the other direction doesn't need its own reasoning at all — one ticket has many payments (one-to-many) means one payment has one ticket (many-to-one), automatically, just by reading the same relationship backward. Marking Payment's side as owned by Ticket's existing list stops the ORM from inventing a second version of the same fact.
Seeing the Payoff: A Real Schema, Generated Automatically
Running this actually creates the database tables, and every one of them matches the reasoning built up across this whole series. A genuinely many-to-many relationship, like Auditorium to Feature, gets its own mapping table automatically, exactly the fix this series already reached by hand. Every other table shows up with its right columns, its right foreign keys, all without anyone writing a single CREATE TABLE statement.
That's the actual return on everything covered in this series' schema-design arc: the class diagram plus a handful of annotations is enough for the ORM to produce the whole schema on its own.
One default worth watching for: left alone, an ORM can sometimes represent even a plain one-to-many relationship through an extra mapping table, the same mechanism meant for genuine many-to-many cases, rather than a direct foreign key. That's a real inefficiency, not a bug, and it needs an explicit override to avoid.
One smaller, practical gotcha is worth a mention too: a database engine can quietly reserve certain words, so a class name that reads perfectly fine in code, like Show or Seat, can fail the moment it becomes a table name. That's an implementation detail of the specific database in use, not a design decision, and it only ever shows up once actual tables get created.
The fix doesn't mean renaming anything in the codebase. A class's name in code and the name of its table in the database don't have to match at all. Tell the ORM to map the Show class to a table actually called shows, or a row column to one called rows, and the class itself never has to change, only the one line telling the ORM what to call it underneath.
Keep reading