Search code examples
javaspringdatabase-designone-to-onespring-data-jdbc

Why is a entity - value relationship implemented as a back reference in Spring Data JDBC


In Spring Data JDBC if an entity (e.g. Customer) has a value (e.g. Address) like in the example here the value has a back reference column (column customer in table address) to the entity in the db schema:

CREATE TABLE "customer" (
  "id"                  BIGSERIAL       NOT NULL,
  "name"                VARCHAR(255)    NOT NULL,
  PRIMARY KEY (id)
);

CREATE TABLE "address" (
  "customer"            BIGINT,
  "city"                VARCHAR(255)    NOT NULL
);

The problem with this is that if you use that Address value more than once in one entity or even in different entities you have to define an extra column for each usage. Only the primary id of the entity is stored in these columns and otherwise there is no way to distinguish from which entity it is. In my actual implementation I have five of these columns for the Address value:

  "order_address"            BIGINT,    -- backreference for orderAddress to customer id
  "service_address"          BIGINT,    -- backreference for serviceAddress to customer id
  "delivery_address"         BIGINT,    -- backreference for deliveryAddress to customer id
  "installation_address"     BIGINT,    -- backreference for installationAddress to provider_change id
  "account_address"          BIGINT,    -- backreference for accountAddress to payment id

I understand how it works, but I don't understand the idea behind this back reference implementation. So can someone please shed some light on that issue? Thanks!


Solution

  • As to most good questions there are many sides to the answer.

    The historical/symmetry answer

    When it comes to references between entities Spring Data JDBC supports 1:1 (the one you ask about) and 1:N (lists, sets and maps). For the latter anything but a back-reference is just weird/wrong. And with using a back-reference for 1:1 becomes basically the same, simplifying the code, which is a good thing.

    The DML process answer

    With the back-reference, the process of inserting and deleting becomes much easier: Insert the aggregate root (customer in your example) first, then all the referenced entities. And it continues to work if those entities have further entities. Deletes work the other way round but are equally straight forward.

    The dependency answer

    Referenced entities in an aggregate can only exist as part of that aggregate. In that sense they depend on the aggregate root. Without that aggregate root there is no inner entity, while the aggregate root very often might just as well exist without the inner entity. It therefore makes sense, that the inner entity carries the reference.

    The ID answer

    With this design, the inner entity doesn't even need an id. It's identity is perfectly given by the identity of the aggregate root and in case of multiple one-to-one relationships to the same entity class, the back-reference column used.

    Alternatives

    All the reasons are more or less based on a single one-to-one relationship. I certainly agree that it looks a little weird for two such relationships to the same class and with 5 as in your example it becomes ridiculous. In such cases you might want to look in alternatives:

    Use a map

    Instead of modelling your Customer class like this:

    class Customer {
      @Id
      Long id;
      String name;
      Address orderAddress
      Address serviceAddress
      Address deliveryAddress
      Address installationAddress
      Address accountAddress
    }
    

    Use a map like this

    class Customer {
      @Id
      Long id;
      String name;
      Map<String,Address> addresses
    }
    

    Which would result in an address table like so

    CREATE TABLE "address" (
      "customer"            BIGINT,
      "customer_key"        VARCHAR(20).    NOT NULL,
      "city"                VARCHAR(255)    NOT NULL
    );
    

    You may control the column names with a @MappedCollection annotation and you may add transient getter and setter for individual addresses if you want.

    Make it a true value

    You refer to Address as a value while I referred to it as an entity. If it should be considered a value I think you should map it as an embedded like so

    class Customer {
      @Id
      Long id;
      String name;
      @Embedded(onEmpty = USE_NULL, prefix="order_")
      Address orderAddress
      @Embedded(onEmpty = USE_NULL, prefix="service_")
      Address serviceAddress
      @Embedded(onEmpty = USE_NULL, prefix="delivery_")
      Address deliveryAddress
      @Embedded(onEmpty = USE_NULL, prefix="installation_")
      Address installationAddress
      @Embedded(onEmpty = USE_NULL, prefix="account_")
      Address accountAddress
    }
    

    This would make the address table superfluous since the data would be folded into the customer table:

    CREATE TABLE "customer" (
      "id"                  BIGSERIAL       NOT NULL,
      "name"                VARCHAR(255)    NOT NULL,
      "order_city"          VARCHAR(255)    NOT NULL,
      "service_city"        VARCHAR(255)    NOT NULL,
      "deliver_city"        VARCHAR(255)    NOT NULL,
      "installation_city"   VARCHAR(255)    NOT NULL,
      "account_city"        VARCHAR(255)    NOT NULL,
      PRIMARY KEY (id)
    );
    

    Or is it an aggregate?

    But maybe you need addresses on their own, not as part of a customer. If that is the case an address is its own aggregate. And references between aggregates should be modelled as ids or AggregateReference. This is described in more detail in Spring Data JDBC, References, and Aggregates