Domain-Driven Design, part 7 — Alternative Relational Database Mapping
Product Name Story
We worked on an e-shop and the product had exactly one name. One day we faced confronted with the reality — the e-shop has to be multilingual. I imagined a new database table for every language field immediately, and I was disgusted by all the tables that had to be created. But luckily I asked myself — What is the domain?
The domain can be extracted from stories and use cases. We discussed this in our team and we extracted a couple of use cases:
- Change the product name in the language.
- Show the product name in the language
- If the name in language is missing, show an empty string
We don’t have a use case to remove a name. The product could exist without a name anyway, so creating one wasn’t a problem.
Use cases were the same for all names or descriptions in the system that had to be multilingual. So we decided to extract use cases and responsibility into a common LangValue.
LangeValue is a multilingual text, this is its responsibility. It has to be able to change the text in the language and read the text in the language. It doesn’t have an identity — LangValue is identified by all its properties, it is a value object. We decided that language identifier can be any string.
The implementation of the multilingual product name is simple. The class is simplified to be instructive.
Relational Database Storage
The LangValue works great in objects but how to save it into a relational database? Well, we could think that text in language must be in a separate table. That would be difficult to map, even impossible without changing the domain model.
Let’s take a step back. What are our use cases? We have to be able to change the text in the language and read it in the given language. This is what our class LangValue does by itself.
Do we have the use case that needs language text in a separated table? I don’t think so. So what if the LangValue would be serialized in a single field? Does it cause problems? It could be difficult to find a product by name in the language, but we do not have such a use case. So the serialization can be a good option.
In a real project we have the finding use case. We have a dedicated database with a special structure for a quick search.
We decided to use LangValue serialization.
We can serialize it by PHP
serialize function or extract the internal array and serialize it. Both styles are similar - they end up with PHP structure encoded in a string that is not readable by people and by databases.
We can use custom serialization method, but that would probably be a horrible choice.
We can also serialize the LangValue into a JSON. It is readable by people and some databases and can work with JSON data type too. This seems to be a reasonable choice.
Example of serialized object
LangValue Doctrine Mapping
We need to convert the LangValue to the JSON and vice versa.
Doctrine supports custom mapping type for this particular task. Once the custom type is registered and mapped, all objects are mapped correctly to the database. The custom type is registered in DBAL layer, so even when we hydrate arrays instead of objects, text is hydrated as LangValue object. When we hydrate scalars, we get raw data from the database — string JSONs.
If the target database supports JSON data type, the Doctrine works with it. Otherwise, it uses the text data type.
We have to add code to the Doctrine integration.
\Doctrine\DBAL\Types\Type::addType(LangValueType::LANG_VALUE, LangValueType::class);/** @var Doctrine\DBAL\Connection */
$connection = ...;
$platform = $connection->getDatabasePlatform();
Database Search in JSON
A relational database isn’t usually a good choice for a full-text search. But some databases allow us to query the JSON data type anyway.
If we want to search using JSON, we can always write a native query. Or we can create a custom DQL function that is translated into engine-specific JSON query.
SELECT * FROM Product where name->>'cs-CZ' LIKE '%query%';
SELECT * FROM Product where name->'$.cs-CZ' LIKE '%query%';
Think before mapping. Even if you are experienced, you can find some new and creative mapping styles. Focus on use cases, do the object model first and think about relations and mapping later. JSON object serialization is useful.
DOCTRINE TEAM. Custom Mapping Types — Doctrine 2 ORM 2 documentation [online]. 2018 [2018–03–14]. Available: http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/cookbook/custom-mapping-types.html