Liferay Objects and the Missing 1:1 Relationship...

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 Users can have many Roles and each Role can be assigned to many Users. In 1:many, one object can be related to one or more other objects, so a User can have multiple EmailAddresses, 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 CLOBs, 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...