The ngrx-data library lacks many capabilities of a full-featured entity management system.
You may be able to work-around some of the limitations without too much effort, particularly when the shortcomings are a problem for just a few entity types.
This page lists many of the serious limitations we've recognized ourselves.
If curing them were easy, we'd have done so already. Sometimes there are acceptable, well-known solutions that just take a little more effort. Some solutions are too complicated or perform poorly. In some cases, we have no good ideas at all.
If there's enough interest in this ngrx-data, we'd like to tackle some of these problems. We could use your help.
This library (like the @ngrx/entity library on which it depends) assumes that entity property values are simple data types such as strings, numbers, and dates.
Nothing enforces that assumption. Many web APIs return entity data with complex properties. A property value could be a value type (e.g., a money type that combines a currency indicator and an amount). It could have a nested structure (e.g., an address).
This library shallow-clones the entity data in the collections. It doesn't clone complex, nested, or array properties. You'll have to do the deep equality tests and cloning yourself before asking ngrx-data to save data.
Many query APIs return an entity bundle with data for many different entity types.
This library only handles responses with a single entity or an array of entities of the same type. When you adopt the redux pattern, you're expected to "normalize" the entity data as you would a relational database.
This library lacks the tools to help you disaggregate and normalize server response data.
Entities are often related to each other via foreign keys. These relationships can be represented as a directed graph, often with cycles.
This library tacitly assumes that entities of different types are unrelated. It is completely unaware of relationships and foreign keys that may be implicit in the entity data. It's up to you to make something out of those relationships and keys.
It's not easy to represent relationships.
A Customer
entity could have a one-to-many relationship with Order
entities.
The Order
entity has an order.customer
property whose value is the primary key
of its parent Customer
.
Each Order
has related LineItems
.
A LineItems
has a one-to-one relationship with Product
.
And so it goes.
There are other cardinalities to consider (one-to-zero, one-to-zero-or-many, many-to-many, etc.).
A good solution would include an extension of the EntityMetadata
that identified relationships, their cardinalities, and their foreign keys.
It can be convenient to construct classes for Customer
and Order
that have
properties for navigating between them (navigation properties).
The domain logic for the model may argue for unidirectional navigations in some cases
and bi-directional navigations in others.
We have to be prepared for any load order. The orders could arrive before their parent customers. A good solution would tolerate that, making connections and breaking them again as entities enter and leave the cache.
There will be long chains of navigations (Customer <-> Order <-> <-> LineItem <-> Product <-> Supplier
).
How should these be implemented?
Observable selector properties seem logical but thorny performance traps
await.
You are responsible for setting the primary key of an entity you create.
If the server supplies the key, you can send the new entity to the server and rely on the server to send the entity back with its assigned key. It's up to you to orchestrate that cycle.
It's far better if the client assigns the key. You can create new records offline or recover if your connection to the server breaks inconveniently during the save.
It's easy to generate a new guid (or uuid) key. It's much harder to generate integer or semantic keys because you need a foolproof way to enforce uniqueness.
Server-supplied keys greatly complicate maintenance of a cache of inter-related entities. You'll have to find a way to hold the related entities together until you can save them.
Temporary-key generation is one approach. It requires complex key-fixup logic to replace the temporary keys in foreign key properties with the server-supplied permanent keys.
Many web APIs require you to save a bundle of entities together in a single transaction.
For example, you may have to create or update an order with all of its line items in a single request representing a single transaction.
The library doesn't support that yet.
Entities are often governed by intra- and inter-entity validation rules.
The Customer.name
property may be required.
The Order.shipDate
must be after the Order.orderDate
.
The parent Order
of a LineItem
may have to exist.
You can weave validation rules into your application logic
but you'll have to do so without the help of the ngrx-data
library.
It would be great if the library knew about the rules (in EntityMetadata
?), ran the validation rules at appropriate times, displayed validation errors on screen, and prevented the save of entities with errors.
These might be features in a future version of this library.
The representation of an entity on the server may be different than on the client.
Perhaps the camelCased property names on the client-side entity are PascalCased on the server. Maybe a server-side property is spelled differently than on the client. Maybe the client entity should have some properties that don't belong on the server entity (or vice-versa).
Today you could transform the data in both directions with HttpClient
interceptors.
But this seems like a problem that would be more easily and transparently addressed as a feature of ngrx-data
.
The library assumes that the entities in the cached entity collection are (or at least were) also entities on the server.
You don't change the Customer
name and put the updated entity in the cache. You save the updated customer entity to the server and, if the server responds with its approval, then you update the cache.
But maybe you can't connect to the server. Where do you keep the user's pending, unsaved changes?
One approach is to keep track of pending changes, either in a change-tracker or in the entity data themselves. Then you might be able to use one of the cache-only commands to put hold the entity in cache while you waited for restored connectivity.
There is no such facility in the library today.
You can add, update, or delete several entities at a time from cache.
But you can only save-add, save-update, or save-delete a single entity at a time to the server.
There is no standard web API for saving multiple entity changes.
If your web API supports such operations, you might follow the ngrx-data patterns while adding your own entity actions and ngrx effects for these operations.
The default EntityEffects
supports saving a new or existing entity but does not have an explicit
SAVEUPSERT action that would _official save
an entity which might be either new or existing.
You may be able to add a new entity with SAVE_UPDATE
or SAVE_UPDATE_ONE_OPTIMISTIC
,
because the EntityCollectionReducer
implements these actions
by calling the collection upsertOne()
method.
Do this only if your server supports upsert-with-PUT requests.
The user saves a new Customer
, followed by a query for all customers.
It the new customer in the query response?
Ngrx-data
does not coordinate save and query requests and does not guarantee order of responses.
You'll have to manage that yourself. Here's some pseudo-code that might do that for the previous example:
// add new customer, then query all customers
customerService
.addEntity(newCustomer)
.pipe(concatMap(() => customerService.queryAll()))
.subscribe(custs => (this.customers = custs));
The same reasoning applies to any request that must follow in a precise sequence.
There is no intrinsic mechanism to enforce concurrency checks when updating a record even if the record contains a concurrency property.
For example, the user saves a change to the customer's address from "123 Main Street" to "45 Elm Avenue". Then the user changes and saves the address again to "89 Bower Road". Another user changes the same address to "67 Maiden Lane".
What's the actual address in the database? What's the address in the user's cache?
It could be any of the three addresses depending on when the server saw them and when the responses arrived. You cannot know.
Many applications maintain a concurrency property that guards against updating an entity
that was updated by someone else.
The ngrx-data
library is unaware of this protocol.
You'll have to manage concurrency yourself.
Ngrx-data lacks support for accumulating changes while the application is offline and then saving those changes to the server when connectivity is restored.
The ngrx system has some of the ingredients of an offline capability. Actions are immutable and serializable so they can be stashed in browser storage of some kind while offline and replayed later.
But there are far more difficult problems to overcome than just recording changes for playback. Ngrx-data makes no attempt to address these problems.
Servers often offer a sophisticated query API for selecting entities from the server, sorting them on the server, grabbing related entities at the same time, and reducing the number of downloaded fields.
This library's getWithQuery()
command takes a query specification in the form of a parameter/value map or a URL query string.
There is no apparatus for composing queries or sending them to the server except as a query string.
BreezeJS is a free, open source, full-featured entity management library that overcomes (almost) all of the limitations described above. Many Angular (and AngularJS) applications use Breeze today.
It's not the library for you if you require a small library that adheres to reactive, immutable, redux-like principles.
Disclosure: one of the ngrx-data authors, Ward Bell, is an original core Breeze contributor.