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
[WIP] Initial Implementation of Soft Delete #1192
Conversation
Open Topics:
@pleerock @NoNameProvided Since you two have been the most active in this I will defer to your opinions on these topics. Some other notes: Once we decide of the QueryBuilder debate, I will refactor entity manager to just let the QueryBuilder to handle the determination, and remove |
Darn it, it was working... Now I merged upstream/next and it's all broken again. Back to grind |
New slew of updates. I went ahead and assumed we would want to exclude soft-deleted entities in the QueryBuilder. I added the withDeleted() method to the QueryBuilder as well. Let me know your thoughts. |
Some things that need to be addressed:
|
Just merged next. Still working on this. |
@NoNameProvided okay then what about eager relations? Shall eager relations always be loaded by query builder? |
I think not |
but why not? If user marked |
Because there won't be a way to get single entity, or you want to add a |
I think the contention here is consistency vs utility. From a utility standpoint, most developers will assume that a deleted entity will not show up in anything unless you tell explicitly ask for deleted entities. This will be the use case "most" of the time, especially considering there is not way to do anything except the simplest of conditions aside from QueryBuilder. From a consistency standpoint, if relations are not going to be magically added in order to prevent the the I think this brings up a better topic, and maybe something for a 0.3 consideration. Since the 0.1 version, we are now forced to use I have two ideas on this subject.
Thoughts? |
Right now you must have to use the query builder for almost everything that is more complex than a So I would say rather include eager relations then. Yet I still think the two is not the same, loading more data is a performance problem, why not loading the deleted entries is not and excepted by default is you enabled soft delete. |
This is a tough point. Here is what I see it coming down to:
To maintain consistency, Again, I am going to go back to the fact that you can not perform anything other than super simple conditions unless you use QueryBuilder. Looking at #1101, it appears that at least some queries could be refactored into using the Overall though, I still do feel that if we are going to return an entity, it should contain all the magic. As a developer, I want my eager relations to load, my deleted entities to be excluded, etc. If I have a query that is going to be perfomance restricted, I still want the ability to say "run this query as is". I wouldn't expect that to return entities though, I would expect a more "raw" result. I am going to go back to my point above of there is no middle ground, and maybe a separation of responsibility is in order. If I know this is a paradigm shift, but as a developer using TypeORM, I want consistency over everything else. If something returns an entity|entities, it should have my relations, etc. If it does not return an entity, it should return exactly what I asked it for. Having this difference makes compartmentalizing my code way more difficult because I have to know if my Entity came from a Sorry for the wall of text here, I just feel this is a bigger topic than the simple question that is immediately in front of us. |
Having all those issues now Im not really sure if we need this functionality... Maybe its better to left this implementation on users, so they'll implement it on their own. At the end not everybody need this functionality. |
I don't agree, soft delete is a pretty basic concept, and should exist at the ORM level because it's hard to do it on the user level. You need to create a wrapper for every function in the ORM or adding it to every call manually. Neither of these is a scalable solution. |
I agree with @NoNameProvided, most ORM and ActiveRecord Implementations I have used implement some sort of soft-delete functionality. I have a user-space implementation in an app I am working on, and it's a very much not a good solution. I think we can come to some sort of agreement here. |
src/entity-manager/EntityManager.ts
Outdated
.execute(); | ||
} | ||
} else { | ||
return this.createQueryBuilder() |
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.
something wrong here
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.
will be fixed in next update
if (entityTarget === Object) | ||
throw new CannotDetermineEntityError(this.mode); | ||
|
||
if (this.mode === "restore" && metadata.softDeletedDateColumn) { | ||
entity[metadata.softDeletedDateColumn.propertyName] = null; |
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.
Please use metadata.softDeletedDateColumn.setEntityValue(entity, null)
instead. This is important if your column is in embedded.
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.
will be fixed in next update
|
||
if (this.mode === "remove" && metadata.softDeletedDateColumn) { | ||
this.mode = "save"; | ||
entity[metadata.softDeletedDateColumn.propertyName] = new Date(); |
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.
Please use metadata.softDeletedDateColumn.setEntityValue(entity, new Date())
instead. This is important if your column is in embedded.
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.
will be fixed in next update
src/persistence/SubjectExecutor.ts
Outdated
@@ -241,7 +240,8 @@ export class SubjectExecutor { | |||
const updateQueryBuilder = this.queryRunner | |||
.manager | |||
.createQueryBuilder() | |||
.update(subject.metadata.target) | |||
.withDeleted(true) | |||
.update(subject.metadata.target) |
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.
bad formatting
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.
fixed in next update
src/query-builder/QueryBuilder.ts
Outdated
@@ -457,6 +457,14 @@ export abstract class QueryBuilder<Entity> { | |||
return this; | |||
} | |||
|
|||
/** | |||
* Set with deleted mode |
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.
Please provide a better comment
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.
fixed in next update
* This column will store an soft delete date of the deleted object. | ||
* This date is set when you delete the object, and set to null when you restore it | ||
*/ | ||
export function SoftDeleteDateColumn(options?: ColumnOptions): Function { |
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.
What if people want to use a flag instead of date? Something like isRemoved: boolean
. I think we need to support that as well.
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.
If we'll do so we can simply call it SoftDeleteColumn
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.
I have always seen it as a date. I suppose we could support both options, but I think that complicates things.
TBH, I would not use it if it was only a flag. In every single project I have, I need to know when something was deleted.
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.
I had several projects where we used just a flag. And when things are getting really complex and we need a deletion date we store in a separate table where all activity on the given object is stored.
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.
Would be great if you can implement this functionality. If we'll do do it later, but we definitely need to rename decorator right now.
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.
I would argue that having it as a date has a more general scope of usage than it being only a flag:
- it gives information on when the entry was deleted (useful in debugging)
- a nice side effect is that you can expire an entry by setting delete time in the future
Unfortunately I don't have knowledge if having a boolean value over date is better performance wise so I can't argue on that point.
As a last point, library (in general terms) should provide feature that covers as much use cases as possible for the feature in question.
src/entity-manager/EntityManager.ts
Outdated
} | ||
|
||
/** | ||
* Restores a given entity. |
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.
Provide a better comment, mention that it restores entities with soft delete column
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.
Also what happens when user sends entity without soft delete column here?
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.
Checking
Also please update a changelog and add documentation regarding to this feature. |
a6ce4fa
to
a436b5a
Compare
So, I basically re-implemented from next again, because it was easier than rebasing. Also added some docs, and made the changes from the comments you put in. |
@mrkmg I want to help you... maybe can we make a list of thing we would need to implement for start working on it? this will help so other people can contribute in your work.. and give some type of status about this feature |
Just checking in on the status of this issue - definitely would love to see this in place. |
A suggestion: when in doubt see how others implemented it. I personally like the way how Doctrine Extensions implemented it. From it's docs:
|
There was a disagreement on whether the column that denotes a record was deleted should be a boolean or a date, or allow for both. I am totally willing to re-factor my this branch against HEAD again, but I do not want to spend all that time for it to sit here with no progress. In my projects, I am currently using a wrapper to add this functionality into all my Entities. |
@mrkmg @pleerock just throwing it out there - why can't we add a boolean delete flag later as a follow-on PR? Soft-delete support would be super useful to get in place, and it seems to me (an outsider here though), that we can easily add boolean functionality later on. A date-based delete flag is better than no delete flag. Since we don't have scope support (see #1601 - I'm still a little surprised that was closed) there's no way to easily add support soft-delete functionality other than explicitly modifying each query (eww). |
@mrkmg could you please share a link to that wrapper? |
Im going to left it on 0.3.0 milestone since currently we are quite busy with 0.2.0 release and want to finish all tasks of 0.2.0 scope. I'll return back to this conversation and start thinking what is the best way to implement this feature once 0.2.0 is released. (0.3.0 contains multiple issues about query builder refactoring). Maybe we'll use filters as @b3ross suggests, not yet sure, will see. |
Thanks @pleerock |
I just wanted to give my feedback on this, i am in need of this functionality and am currently working on my own approach to it just extending The repository and modifying all calls, however when it comes to the query builder, i think it could come from a chained approach, i'd imagine adding a I also think it could be configured in a way that you could set a default in the ORM config of "include deleted by default" or some other type of flag as a default control behavior. |
As a follow up for some more clarification on why you'd include deleted by default, maybe the part of the UI you're designing has the ability to show "deleted" records, and other parts would want to exclude them, this makes it so the client could use a "Soft Delete" column, while also having logic that could omit them with a |
Deleted means we'll deleted. |
@consense i disagree on the hidden field, i also disagree with the idea of moving this to the application layer exclusively, a soft delete is characterized as: https://www.pcmag.com/encyclopedia/term/57355/soft-delete
https://www.codeproject.com/Articles/46889/Soft-Delete-or-Logical-Delete-in-ORM
The whole point of using soft deletes is not to "Delete" data permanently (hard delete), its to "soft" delete the data, making it possible to restore the data until the application/developer decides to "purge" the data (hard delete) based on their own business use case. To give you an example, i work with data that has a 7 year retention policy, but records DO need to be "deleted" however this data has to be recoverable for up to 7 years. The ORM layer could very easily handle this, and as long as the application/developer is aware they need to indicate with/without deleted records when using the query builder. Some additional references when it comes to how other ORM layers are handling this (integrated & some extensions): I would like to note that there's a few options for Rails, some of them implement soft delete by overriding default behavior (Paranoid is one of these) and then there's Discard, which adds additional separate methods. My previous message was not specifically in pushing this as a direct feature of typeorm, but there was some questions on how the query builder could/should handle this: #1192 (comment) and i think that having a config option that sets the default inclusion state makes the most sense, and then having a |
Hi Umed @pleerock and thanks fot this framework :-) |
@Destreyf My comment from April was not intended to question the usefulness of having a soft-delete functionality in the ORM layer in general - quite the contrary as I am looking forward to this feature as well and the sources you quote also mirror my understanding. Still the semantics of "delete" in the English language indicate a permanent action. I would put up the hypothesis that having a get() following a delete() return the "deleted" record would confuse most users initially using the ORM and would be perceived as a bug. Your use case involving retention policies is understandable, but solving this via an ORM soft-delete feature seems to me a bit like "archiving" documents which arent used on a daily basis anymore (but need to be retrievable for years ahead) in the trash bin. Sure - they could be recovered, but maybe adding an archive room in the basement (which would be an application level managed "archive" flag on the model from a technical POV) would be more appropriate. The trash bin (or soft-delete flag - be it a bool or deleted_at timestamp) would serve more a purpose of giving the application a safety net in case of an "ah-shit-shoudlnt-have-deleted-this-please-undo" moment. Your secondary suggestion to make the default behavior configurable is of course possible, but if you have an application where "delete" doesnt mean "delete" but "archive/hide" in 90% of the cases and you set "with-delete" as default for get/fetch calls, you would effectively rob yourself off the safety-net a soft-delete feature usually provides as you couldnt undo accidentally deleting an "archived" record anymore. Anyway - no offense intended - just wanted to clarify my earlier thoughts :-) |
Hi @pleerock, @AlexMesser, I would like to know if you still think to merge that pull request to 0.3.0 release and when it should appear? You told that after release 0.2.0 you will come back to that issue.... now we have release 0.2.6 still no activity here and any reaction for that pull request.... The community of TypeOrm would like to have this feature.... |
I am still willing to re-implement this for the next release, but I want to make sure that @pleerock agrees with the API before I do the work. I have implemented it twice now, and both times it stalled. Once there is a defined API of how it will work, I will do the work to implement it. I will leave the API up to the community to decide on. You can see my thoughts above. |
Hey @dmitrirussu @pleerock @AlexMesser I would like to contribute to this feature as well. |
would love to see this coming |
Hello there, Is this still on the table ? Seems pretty well advanced to me |
In the future versions of typeorm I plan to refactor current QueryBuilder implementation where I'll reconsider this functionality. So, no plans to make it in typeorm for now. |
Any progress on the feature? |
welp looks like we have an answer on this: #534 😭 |
Let's continue discussion in #5034 and try to merge that PR. |
Addresses #534
See #1165 for previous conversation.