-
Notifications
You must be signed in to change notification settings - Fork 689
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat!: EXPOSED-320 Many-to-many relation with extra columns #2204
base: main
Are you sure you want to change the base?
Conversation
19d5e3f
to
91372bb
Compare
An intermediate table is defined to link a many-to-many relation between 2 IdTables, with references defined using via(). If this intermediate table is defined with additional columns, these are not accessible through the linked entities. This PR refactors the InnerTableLink logic to include additional columns in generated SQL, which can be accessed as a regular field on one of the referencing entities. It also allows the possibility to access the additional data as a new entity type, which wraps the main child entity along with the additional fields. This is accomplished with the introduction of InnerTableLinkEntity.
- Remove approach to set/get additional data from an existing entity object field. This requires some UX concerns answered, for example, concerning caching. Would the wrapped entity (if loaded from a query of its own table) override the entity+data loaded from the many-to-many query? Would updating the field mean the reference should also be trigger a delete+insert? - Fix issue with updating and caching new additional data
- Fix detekt issue
- Add more tests (particularly for update) & rename test classes - Refactor cache to ensure no overlap with wrapped type. Each link entity is now stored by its target column, source column, and target id (stored in entity) - Move new entity classes to own file - Refactor logic for deleting cached entities
- Fix KDocs samples
91372bb
to
0562b39
Compare
*/ | ||
abstract fun getInnerTableLinkValue(column: Column<*>): Any? |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even though an entity class is being used to accomplish this feature, the original behavior and usage of via()
entities should most likely be the same. I think it would be best to override all standard entity functions, like delete()
, all()
, new()
, findById()
, etc since their use wouldn't work (since the intermediate table does not have to be an IdTable
).
This would mean that attempting to call them on an InnerTableLinkEntity
throws an error instead. So the only way to insert or delete or retrieve these linked values would be by setting/getting the parent/child entity field declared with via()
. Basically these entities would only exist or be used through the via
field. Does that make sense?
internal val innerTableLinks by lazy { | ||
HashMap<Column<*>, MutableMap<EntityID<*>, MutableSet<InnerTableLinkEntity<*>>>>() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Most ideally, there should be a cache that stores the link entities without any relation to their referenced counterparts, solely based on some special id, which could be retrieved from the ResultRow
in wrapLinkRow()
. This is how the data
cache is for example set up for all regular entities.
This would mean either a brand new id for the entity (defeats the purpose as the point is to not introduce a new/fake id column in the intermediate table, since uniqueness is based on the 2 referencing columns) or some way to check ResultRow
values against entity values. For the latter, I did consider forcing another override where the user defines some sort of equality match between ResultRow
and InnerTableLinkEntity
, but it got a bit messy.
What the above cache does is store all InnerTableLinkEntity
s for a target column and source (column) id, so uniqueness essentially relies on 3 values: target column (e.g. task
in ProjectTasks
), source id (e.g. project
value in ProjectTasks
), and target id (e.g. TaskWithData.id
stored in the entity itself).
@obabichevjb I refactored this PR (and added more tests) as original cache was failing if the same wrapped entity was used with different additional data (for example, updating Please let me know if any API improvements could be considered, and what you think about overriding the standard entity functions to throw an error (like |
Description
Summary of the change:
Provides the functionality for DAO entities to set/get additional data to/from extra columns (non-referencing) in the intermediate table of a many-to-many relation.
Detailed description:
Why:
Additional columns (beyond the 2 reference columns) are possible on intermediate tables defined in many-to-many relations. But these columns cannot be accessed by the referencing DAO entities involved because
via()
creates anInnerTableLink
class that ignores these columns. Workarounds include adding a 'fake' id primary key to the intermediate table and creating an associated entity, or duplicating inner logic with custom classes. The latter only partially works, however, because internal cache logic requires that the target ofvia()
is anEntity
.What:
via()
to be used in the same way as before by introducing a new entity classInnerTableLinkEntity
that wraps the referenced entity (and delegates to it's id and table) along with any additional data in the intermediate table row.How:
InnerTableLinkEntity
, with associatedInnerTableLinkEntityClass
, that forces 2 overrides so users define how to properly set and get any additional data. Subclass can be aclass
ordata class
as needed.via()
andInnerTableLink
now accept a list of columns as arguments to specify which additional columns should be included. This allows users to opt-out of new functionality, by providingemptyList()
, if it breaks any current workaround.InnerTableLink
internal logic uses additional columns to generate triggered delete and insert SQL. Previously, statements would only be generated if there was a change in the reference id. Now they will be generated even if the reference id is identical, as long as any of the additional data changes. This is necessary to allow reference collections to be updated properly.EntityCache
map just forInnerTableLinkEntity
. Because the latter delegates to the wrapped entity, if the regulardata
cache is used, this special entity may override its cached wrapped entity or be incorrectly retrieved onfind()
. This map stores each entity by its target column and source id, as the intermediate table is expected to have a contract of uniqueness on the 2 reference columns.Note: The original plan was to have this implementation alongside a more standard approach, which would get/set the additional data as a delegate field on an existing entity, for example:
This worked well for safe setting and getting, but started raising questions when updates were introduced:
approved
in isolation? Meaning not as part ofSizedCollection
, but innew {}
or through a standardtask.approved = true
?Task
was already cached with itsapproved
field set by references loaded from the intermediate table, then something likeTask.all()
was loaded, the new task would override the cached task and trying to access theapproved
field would cause an exception as it would rightly not have any value. Would it be expected that this shouldn't happen?So I opted to go for the safer implementation and if users come forward requesting the design above, I'm hopeful that their use cases will answer some of these questions.
Type of Change
Please mark the relevant options with an "X":
Updates/remove existing public API methods:
Affected databases:
Checklist
Related Issues
EXPOSED-320, EXPOSED-443