Blogs
Turns out it's not really missing at all!
Introduction
So I've been a developer for a long time (longer than I care to admit). When I started, being a developer actually meant you had to wear many hats, one of which has included database administration and object modeling.
When modeling in a database, there's different approaches obviously, but for OLTP applications (Online Transaction Processing), it is common to use 3NF or higher degree of normalization to minimize data duplication (whereas in an OLAP, or Online Analytic Processing, application such as reporting, a denormalized or flattened approach to modeling is typical).
In a standard DBMS, you have three different types of relations
which are used for relating objects - 1:1
,
1:many
, and many:many
. Their use cases are
typically pretty clear. In many:many
, objects can be
related to many other objects, so User
s can have many
Role
s and each Role
can be assigned to many
User
s. In 1:many
, one object can be related
to one or more other objects, so a User
can have multiple
EmailAddress
es, but an EmailAddress
record
can only relate to one User
.
And then there's the 1:1
relationship where one
object can only be related to one other object. You'll see this
typically when you want to separate and group related data to
separate objects for organizational and usage scenarios such as when
a User
is related to a UserProfile
. This
separation allows us to track details about the User
separately from their profile data (settings, color preferences, etc).
Although the profile data could be part of the User
object, best practice would have you separate them into another object
and avoid having a single object with many unrelated columns. And
because the profile data is used sparingly vs general user
information, separating them as different objects means we make the
data available when necessary and avoid when it isn't.
A 1:1
relationship is a requirement for data
modeling in an OLTP application and serves many useful scenarios.
Liferay Object Relationships
When you are saddled with this kind of background like I am, you will initially be frustrated when you start using Liferay Objects...
Why? Well, when you are defining your User
Object,
your Role
object and your EmailAddress
object, you'll have no problems on the Relationship tab when it comes
time to define the relationships.
When you're done with the UserProfile
object and
are ready to establish that 1:1
relationship though,
you won't find it, and that's going to be frustrating. I mean,
1:many
and many:many
is there, so why leave
out the 1:1
relationship?
And, if you're friends with Marco Leo (like I
am) and you find yourself in a passing conversation with him (as I
recently did) and you ask him about the missing 1:1
relationship in Liferay Objects (like I recently did), you may be
surprised by his answer in that it isn't missing at all, it's not necessary.
When modeling using Liferay Objects, you shouldn't be thinking of it as a database object modeling activity, you should always be thinking of it instead as a microservice modeling activity.
When building microservices, it is best to pass a complete object of complete data and avoid forcing a caller to invoke multiple services to gather and join data together. In microservice modeling, each call is significantly more expensive than the data fields that are part of the response.
Show Me the Use Cases
If you push back on Marco (as I did), you'll get his response,
"What is your use case for 1:1
relationships?"
and that's where things can get kind of interesting, especially given
that he knows what's on the roadmap for Liferay Objects.
So I cracked open the list of common 1:1
use cases
and started throwing them at him...
Separation of Concerns
The first use case I tried was the User:UserProfile
example. It seems perfectly reasonable to model these as separate
objects when you think about it from a database object modeling
perspective. For Liferay Objects, though, this is a hidden performance
issue just waiting to bite you.
Why? Because now to retrieve the data the external caller needs to make two web service calls back to Liferay. Multiply this by your expected number of concurrent users and you can see how this can multiply the load your Liferay system has to support.
When you look at the Liferay User
ServiceBuilder
implementation, you'll find some 1:1
relationships that
are managed in there. It makes sense there though because the DB
retrievals have almost unmeasurable overhead. But from a headless
perspective, overhead associated with each call comes with a high
cost, and multiple calls comes with significant penalties.
"But user and profile are two different things that I logically want to separate..." might be your counter argument (it was mine). Remember these records are used at different times during an application, they have different usage patterns and are not typically needed at the same time. Keeping them separate helps to understand these inherent differences.
Marco's reply to this was "Oh, so you want a fieldset capability for an object..." And this really struck me. It struck me as in yeah, I didn't need a separate object, per se, but certainly the ability to group fields and treat as a unit would allow me to solve the separation of concerns issue.
Optional Data
Another use case is where there is "optional data",
and the UserProfile
could be seen as this. The profile
data is going to be needed at certain points in an application's
lifecycle, but most of the time when the User
record is
needed, the UserProfile
would be optional so why
retrieve it?
And while this is true from a database object modeling perspective, remember in Liferay Objects modeling, we want to reduce (or eliminate) extra calls to the backend because those have high costs involved with them.
From that perspective, a single call to fetch a
User
record might come back with a whole lot of data
that you don't always need, but better that than to have to make
additional calls to fetch it later.
And, since the Liferay Headless API includes the
restrictFields
parameter, if you know there are fields
that you will not be using in your application, then you can exclude
them from the return so you don't have to just ignore them instead.
Sensitive Data Segmentation
This is one of those use cases that I thought I had Marco on...
You will often use a 1:1
relationship when you have
data related to an object that has different permissions than the
object itself.
For example, the User
object will typically be
public, but a UserPassport
that holds their passport
details would have different access permissions.
With Objects, we also don't have the ability to define permissions on a per-field basis, so splitting them into different Objects (that can be permissioned separately) is the only option...
"Oh, so you need field-level permissions..." was Marco's reply. I couldn't tell if he was noting that as a future requirement or if it was sime kind of hint of something already on the roadmap that would solve this concern.
But yes, field-level permissions would be a way to set necessary access controls on the data, and this would be an ideal solution when embracing the microservice Object modeling technique.
Performance Optimization
With database object modeling, the only cost you typically face
is the data retrieval for an object, and size of the object being
retrieved matters. Small objects with numeric and short text columns
perform better than large objects with a bunch of CLOB
s,
so if you can separate infrequently used large columns into their own
entities with a 1:1
relationship, you can get some
performance improvements in the right cases.
This however is actually the inverse when defining microservices. Here the number of calls are the things to limit, not the response object sizes. Separating an Object into two or more Objects (and requests) due to field sizes will always be the worst option vs a single call to retrieve the complete object.
Modularity and Extensibility
This use case focuses on the challenges an application faces when an object changes. When you add a new column to a database table, this can affect all applications which use this data, and this in turn can have a large impact on application maintenance.
Embracing 1:1
relationships, when your changes
happen in an object that maybe isn't used by all of your
applications, this can reduce the maintenance impact. Even better,
if you create a new object that has a 1:1
relationship with the existing object, there is zero impact to your
current suite of applications because they would remain unaware of
the new object.
If we had to change the UserProfile
object, well
that only affects applications that use it. If the User
object were to change, that affects all applications.
With microservice calls, we really don't worry about this, right? I mean, if we're using a named field in the response, as long as that named field still exists as the Object changes, our applications are just fine and will happily ignore the new field(s). Meanwhile, new apps that need the new field(s) will find them and be able to use them.
Normalization
Normalization has historically been important in data modeling. Disk and memory were always the more expensive aspects in computer systems, so reducing data duplication (and maintenance duplication) have always been important.
The various forms of data normalization were defined to reduce data duplication (amongst other things) and were defined to support OLTP application performance.
For OLAP systems, a flattened model is more appropriate to simplify querying for data to report on, and focuses on simplifying reporting and analytics querying but embracing data duplication as one of the side effects.
And microservice Object design actually matches the OLAP model much more than it does the OLTP model. The ideal microservice will return all of the data that you need in a single call, so this means that your Object design should also represent a flattened object in lieu of a more elegant and normalized object graph.
Decomposing Complex Objects
This is really more a reflection on us, yeah? Imagine having a
Car
object that maybe has 100 fields with details about
the car, the engine, and various other details on the Car
instance. It gets to be a bit to wrap your head around...
Decomposing this into a Car
object that has a
1:1
relationship to the Engine
object and
other unique objects for the various systems is much easier for us
humans to follow and understand.
But from a Liferay Objects perspective, that single
Car
object with all of the fields in it is the better
performing implementation than to decompose it into smaller Objects
that each must be retrieved using separate, individual web service calls.
Why Does It Still Feel Like
1:1
is Missing?
Personally I think this is because of the terminology Liferay used for the relationships.
Database object modelers all understand the 1:many
and many:many
relationships, so embracing that
terminology meant those folks could understand Liferay Objects
relationships quickly.
I don't think when they chose that terminology that those of us
with a history in database object modeling would immediately see the
missing 1:1
relationship and that it would bug us that it
wasn't there...
So yeah, I think it is because of the terminology used to describe the relationships...
There's a competitor of Liferay in one of our markets that uses
different terminology for their object relationships... The
1:many
is called a Lookup and the
many:many
is called a Complex (because of the
complexity in the relationship I guess).
Because they used a different terminology, it might have taken
folks a while to learn what those meant, but they didn't have folks
asking about a 1:1
relationship...
Conclusion
So yeah, there is no 1:1
relationship in Liferay
Objects, and there isn't going to be one on the roadmap.
And that's okay, I think. If those of us with the database object modeling background can focus on thinking about the best way to design Objects for a microservice, we can get beyond terminology issues and create the best Objects for our solutions.
I still have some things to talk with Marco about though... I didn't try to get details about the "fieldset" or "field level permissions" things that he was hinting about... Maybe I can find him at the upcoming Devcon in Budapest in November (where I'll be leading a workshop) and get some more details out of him...