Should an Eloquent relationship include soft-deleted records?

Joel Clermont (00:00):
Welcome to No Compromises, a peek into the mind of two old web devs who have seen some things. This is Joel.

Aaron Saray (00:08):
And this is Aaron.

Joel Clermont (00:16):
Kind of feel like if my memory is good and I'm remembering correctly, which I know it is, it's flawless, maybe the last couple of topics have been less technical. So today I was going to throw out one that's more technical. You know people love code, they love hearing people talk about code, $this->, all that.

Aaron Saray (00:37):
Wakes you up in the morning.

Joel Clermont (00:39):
That's right, exactly. But one of the things that we had kind of hashed out on a code review on a previous project had to do with relationships, Eloquent relationships, and whether or not those should include deleted or trashed records. And I know everyone is just like on the edge of their seat, this is super exciting. But, no, it is kind of an interesting topic, I think. And we had some healthy debate back and forth. So let me just throw this out there and see if you have a general rule. Aaron, if you do... Well actually, you know what? We should probably set up a domain to talk about so that there's an example people can get their heads around. And unless you object, why don't we just use the one from our project? I think its generic enough. It was a survey having to do with, not employment, but it had a thing called a job code. Which might be like developer or engineer or something like that.

Aaron Saray (01:36):
Sure.

Joel Clermont (01:36):
Okay. The main model was a survey and it had a related table of job codes and a particular survey had a particular job code. Does that make sense? Is that like a good enough example to talk through here?

Aaron Saray (01:52):
Yeah.

Joel Clermont (01:52):
Okay, all right. So if I fetch-

Aaron Saray (01:56):
Having worked on the project myself, I mean I can tell you that it makes perfect sense to me.

Joel Clermont (01:59):
That's right. If anybody else can't keep up, I mean that's on them, right? So if I fetch a survey and the job code, the survey arrow job code, and it's trashed, what should happen in your mind?

Aaron Saray (02:15):
Well, the default thing is if that job code was trashed, that'll be a null answer.

Joel Clermont (02:20):
Right, exactly. I think that's sort of where this discussion started. Was like that clearly wasn't the behavior you want, right? Like, you don't want... If this record had data and the data's been trashed, soft deleted, you should still show it, right?

Aaron Saray (02:36):
Yeah, I've had people say like, "Well, that's why you should never do soft deletes. That's why you have a status column." And it's like, "Well, the soft deletes in a way is a status," you know?

Joel Clermont (02:47):
Yeah.

Aaron Saray (02:47):
So, yeah, instead of hard deleting it you're just saying that this record exists but it's no longer available, its status is inactive is another way of looking at a soft delete.

Joel Clermont (02:57):
Yeah, that's a good point. Personally, I like soft deletes and I think you kind of talked me into them when we first started working together. I wouldn't do the status active thing, I'd just like delete the record. Do you recall these discussions we had early on?

Aaron Saray (03:12):
Yeah.

Joel Clermont (03:12):
Okay. All right, if you firmly believe soft deletes shouldn't exist then this whole episode will be you being further entrenched in your belief that this is a dumb idea. If not, then what's a good workaround? In that case, you can override it, right? You can chain on to the relationship definition withTrashed, you can bake that into the relationship definition. What are your thoughts there as a way to do it that gives the desired result to the end user, but also has a good developer experience working in the code of the project?

Aaron Saray (03:51):
I don't know if there's one right rule or anything like that. But I think that in general I tend to think that those relationships and things like that are going to be honoring with trashed. And for the most part, if you want to do some sort of query or retrieve the data or whatever, you can still modify how it retrieves it. Like, if you're doing like a with query you can pass a closure to it and then say that, "This with query is with trashed." The problem I have in there is it takes away some of the visibility or some of the clearness and it takes away some of the magic. So when you always have to eager load things before you use it, and these aren't one-to-many they're just like one-to-one, you start to lose some of the magic of just pointing at an item and saying, "Hey, that's what it is." So there's nothing wrong with doing it "the harder way" but it just makes it a little bit harder.
The issue I have is when you have more of a relationship... Well, I guess it works both ways. But it usually happens with like a relationship of many things. When you want to retrieve with trashed along with that, later on you might pass this object to somewhere else. So it has a thing, like job codes or whatever, and the other thing won't know that some of those are deleted, not meant to be processed, and some of them are. So whenever you mix in a little change but the variable's still called the same or the relationship's still called the same you can get into weird situations like that. Which kind of scream to me like, "Data leakage, and "security issues," and stuff like that. So in order to be consistent, I've changed my opinion. I don't think there's really any easy way to have a relationship that has with trashed on it sometimes unless it's always with trashed and it's named such a way that it indicates that. Because the majority of your relationships are not with trashed. So if you say, cars and you say Jack-in-the-boxes and you say job codes, which one of those is the one that's going to contain trashed? I don't know.

Joel Clermont (05:57):
I got to say Jack-in-the-boxes because that...

Aaron Saray (06:01):
I don't know, maybe I'm hungry. But I think there's an alternative. So if you really are using something so frequently with trashed and not something you can take out... Because, remember you could still grab properties off an object and you can use Eloquent�s tools to get a trashed relationship, assign those to properties and then pass to the variables. You could say, like, "I'm going to retrieve this survey and I'm going to just grab the survey ID off of it and I'm going to retrieve this job code and just grab the label." And then you could just pass in the ID and the label and not use that model anymore. Well, then although it's kind of a dirty and it's not a perfect model, there's really no confusion because you're not moving it forward in the life cycle, you're just putting in some properties. But if you're going to do this trashed all the time, I think it makes sense to make a relationship that indicates it's trashed. Like jobCodesWithTrashed or allAvailableJobCodes, or something.

Joel Clermont (06:59):
So one of the distinctions I heard you make, and I agree with, is it's different if it's, like in our example, survey arrow job code, it's a single required field that's there. Versus maybe let's say client is where the job codes that are available for that client are defined, client->jobCodes. That feels different making it include trashed items than survey->jobCode. Is that a correct assumption?

Aaron Saray (07:29):
I mean, you're right. But I would say different as in worse. So I'm just saying it's already bad enough for the ->jobCode to be a trashed model. Because if that's the case that means that every time I use a relationship or a single thing, I then have to check the boolean trashed method on them. Because if it was a trashed... Like, if I'm going to use that for something I have to check every relationship because it's not clear which relationships are going to be a single trashed item versus a non-trashed item.

Joel Clermont (07:56):
Yeah, interesting. I wonder if context also matters like in the domain. For example, if every survey must have a job code and job codes are a thing that are frequently managed and you know they're going to kind of come and go, does that change your opinion at all? Like just kind of knowing the data and how it's managed. Because-

Aaron Saray (08:20):
No.

Joel Clermont (08:20):
No? Okay.

Aaron Saray (08:22):
And that one I think I would have two relationships. I would have job code, which is just a standard relationship that honors deleted ones. And I would have display job code or something like that, which is that same relationship but with trashed. So that I know that if I want to deal with a real model, if it exists it's on job code. And if I just want to have the information to display it, it's going to be on the one called display. Which may not necessarily be a trashed or un-trashed model, it's just something that I'm clearly saying as not for use in the rest of the domain.

Joel Clermont (08:57):
Yeah. I kind of like display better than putting trashed in the name, I don't know. At least for the one-to-one relationship. For the one-to-many, you know all job codes or job codes with trashed... I forget what you came up with, that seems less bad to me. It also seems like less useful. Like you wouldn't reach for that as much. Whereas in the survey job code, you really... I think it's actually more rare that you would need to know or care if it's trashed or not. Maybe my thinking is too shortsighted, but you know what I mean? And maybe it's because I'm thinking about displaying it and maybe you're thinking more about editing it or making other changes to it that it could actually matter if it's trashed or not.

Aaron Saray (09:38):
I think the difference is I'm thinking of it in a decoupled mode and you're thinking about it in the context of the application.

Joel Clermont (09:43):
Yeah, right.

Aaron Saray (09:43):
That's great if you do a lot of greenfield projects, like you know some of the projects we've had that you worked on. Or other projects that I've primarily taken on which have been older Laravel applications. There's a lot, context doesn't matter because it was a bunch of different developers and so no one really remembers or understands what the rule is for this class.

Joel Clermont (10:06):
Yeah, exactly. And I think that's probably why this is such a difficult one to make a global rule for. Because it is so team specific. So maybe within a team just agree on what it's going to be. Yeah, the element of least surprise, I've heard it described when we're trying to design an API or something like that, that applies here. Like, don't do something that maybe is convenient in the particular use case where you're doing that work and making that decision. But then is confusing a year from now to some other developer that has joined the team and is like, "Well, why would you include trashed on this one relationship but not on this other on?" So I think that's a valid point.
I know I mentioned we recently went on a family road trip and one of the goals... Like our final destination was the Grand Canyon, right? I'd never been there. In fact, most of the people in the family haven't been there. But along the way down, on the way back, we kind of took our time and one of the things we wanted to do was hit some national parks. Like, on the way there and the way back.

Aaron Saray (11:16):
Sure.

Joel Clermont (11:16):
And I think in total we hit... I say hit that sounds a little too aggressive. We stopped at, we enjoyed the majesty OF...

Aaron Saray (11:25):
You spread germs too.

Joel Clermont (11:27):
Spread germs too, five or six. I can't remember now. But anyways, our youngest child who is nine really was not having fun. In fact, I have a nice collection of photos where he's just like laying down at national parks. Like there's one he's laying down in front of this just awesome vista of the Grand Canyon. Anyways-

Aaron Saray (11:53):
Was he planking?

Joel Clermont (11:55):
After the... No, that would've been cooler. This was like the laying down out of sheer boredom and like, "I just don't want to be here."

Aaron Saray (12:04):
I see. It's like father, like son. Because you said that would've been cooler implying that the first thing was at least somewhat cool.

Joel Clermont (12:10):
All right, you got me. No, planking would've been at least a little more interesting. It would've been a little dated, but anyways. I think it was the second park we stopped at, I was trying to get in his head like, "Why don't you think this is cool?" You know some of it's being young. And he's like, "Dad, there's like no playgrounds at these parks. There's like no slides, there's nothing." Because in his mind when I say, if we're at home park, "Hey, we're going to the park." He's not thinking of like the trees or maybe the nature there. There's like something to do fun, to play on. By the end of it, he had gotten over that misconception but it just like was cracking me up at first. Because I was getting frustrated. I'm like, "We're spending this money and this time to go all these awesome places and you're like bored?" I'm like, "What is wrong with it?"

Aaron Saray (13:00):
"Where is the swings?"

Joel Clermont (13:01):
Yeah, exactly. The Grand Canyon, you can look high and low there is not any swings or slides there.

Aaron Saray (13:11):
We talked about the ease of use and confusion that could come with, you know, having inconsistent relationships. But there's also a security aspect to some of this stuff and we really like to focus on security.

Joel Clermont (13:22):
In fact, we put together a free eBook with some security tips. You can find it at masteringlaravel.io and just navigate to the security section.

No Compromises, LLC