Open chillu opened 3 years ago
Is this a dup of https://github.com/silverstripe/silverstripe-versioned/issues/200?
Without any core changes you should be able to create a 2nd has_many relationship on your data object that references the through-object instead of the foreign-object. You can then specify cascade_deletes on that relationship instead.
I'd suggest doing this in the short-term because I feel like we're treading on dangerous territory and wouldn't want to squeeze a quick PR in.
Is this a dup of silverstripe/silverstripe-versioned#200?
It's definitely related, but I'd say this bug is more focused in scope. It only concerns itself with the ORM functionality, not Versioned or GridField.
Without any core changes you should be able to create a 2nd has_many relationship on your data object that references the through-object instead of the foreign-object
I'm not smart enough to think through the ramnifications across the entire ORM surface around adding a pseudo-has_many relationship for this ;) I could do this as a custom getter to avoid any ORM interference though.
Anyway, it doesn't change the fact that this is a bug, right? We could clarify this workaround in the docs as a "known limitation" of course.
There are no bugs or features in this land, just the code's implications :-P
The current implementation matches what happens in many-many land AFAIK.
I agree it's confusing and potentially dangerous, but not necessarily a bug. Or more, simply "fixing" it isn't necessarily going to cause other problems.
One possible concern here: given that the "through" record is now a DataObject in its own right, can we say with absolute certainty that deleting it automatically is safe? I can’t think of a possible use case where you’d have multiple relations running through the same join record, but I like to throw spanners at things
Would/could it be possible to delete the join records, but not the end record?
This is why cascade_deletes needs to let developers configure these things.
The question is how to distinguish a relation that returns the foreign objects from a relation that returns the intermediary objects.
I'm not convinced that "(relname).Join" is the most intuitive way to refer to the intermediary relationship, as it makes it seem "further" than the foreign objects when it's actually "closer".
If, for example, you were to traverse a list of the join objects you'd probably not link to the foreign object at all. It would behave exactly like a has-many, sense my suggestion to Ingo.
On option would be to create a pair of relation methods for a manymany-through: "(relname)" and "(relname)Join". There a little bit of risk in such a string concatenation that someone overrides that relation name, but I think that's the kind of edge case that people use as a feature sometimes, so it's okay.
Not that I'm not suggesting a mere hack for cascade_deletes. You could, for example, Chuck "(relname)Join" into a gridfield. I'm not sure what the purpose of that would be - my motivation is more to keep a consistent API.
The other thing you could do is simply force users to manually create a has_many. Ingo was worried about side-effects of this, but I'm not - it's in keeping with the ORM's design to do that.
can we say with absolute certainty that deleting it automatically is safe?
To be clear, I'm not proposing that we change the current API or behaviour, even though I think it's not accounting for the most common scenario (deleting the join record rather than the other end of the relationship).
I'm not convinced that "(relname).Join" is the most intuitive way to refer to the intermediary relationship, as it makes it seem "further" than the foreign objects when it's actually "closer".
We've got (object)->Join()
already, I thought (object)->(relname)->Joins()
would be consistent? Agree that it appears "further away" than (object)->(relname)Joins()
, but it has the advantage of keeping the logic contained on ManyManyThroughList
rather than packing yet another thing into our DataObject
god class.
The other thing you could do is simply force users to manually create a has_many. Ingo was worried about side-effects of this, but I'm not - it's in keeping with the ORM's design to do that.
OK, I'll let you write the PR for this if you want to push for it as a solution ;) Lazy loading, caching, introspection, schema changes, ownership, snapshots, cascade_duplicates - it's just too much to think through for me, sorry. I'd also argue that it's counter-intuitive to developers - in 99% of cases, you'll want to ensure that join records are cleaned up when one side of the relationship is deleted, and this sounds like a lot of hoops to jump through to achieve that.
Use the ORM's built in logic to delete the through objects, with auto-mapped relationships. Wouldn't work on many_many
without through.
<?php
class ParentObject extends DataObject
{
private static $many_many = [
'Items' => [
'through' => ThroughObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
private static $cascade_deletes = [
'ItemsJoins'
];
}
Like Option 1, but leave it to devs to define this.
<?php
class ParentObject extends DataObject
{
private static $many_many = [
'Items' => [
'through' => ThroughObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
private static $has_many = [
'ThroughObjects' => ThroughObject::class
];
private static $cascade_deletes = [
'ThroughObjects'
];
}
Create new logic on the ManyManyThroughList. Wouldn't work on many_many
without through.
<?php
class ParentObject extends DataObject
{
private static $many_many = [
'Items' => [
'through' => ThroughObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
private static $cascade_deletes = [
'Items.Joins'
];
}
Do what Damian suggested on a similar discussion around orphaned many_many relationships, and simply move this logic into DataObject->onAfterDelete()
. This would change existing logic, and even though it'd be what devs expect 99% of the time, we'd need to make this opt-in for existing projects. Has the advantage of working on many_many
as well.
<?php
class ParentObject extends DataObject
{
private static $many_many = [
'Items' => [
'through' => ThroughObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
// no cascade_deletes, feature is built-into onAfterWrite()
}
Rather than as has_many
, you could also just write custom code around MyThroughObject::get()->filter(<foreign-keys>)
, which is how I've solved this right now in my use case.
<?php
class ParentObject extends DataObject
{
private static $many_many = [
'Items' => [
'through' => ThroughObject::class,
'from' => 'Parent',
'to' => 'Child',
]
];
private static $cascade_deletes = [
'ThroughObjects'
];
public function ThroughObjects()
{
return ThroughObject::get()->filter('ParentID', $this->ID);
}
}
Somewhat related to this, I just found today that Option 2 is currently required for versioned many-many-through to work properly. Without the extra has_many
and cascade_deletes
, it’s impossible to “unlink” a many-many-through relation in the live stage. E.g. document -> assignedtag -> tag (with all 3 models versioned):
assignedtag_Live
tableMore info on why that is the case: https://github.com/silverstripe/silverstripe-versioned/issues/116#issuecomment-367130601
I came here to add to the conversation, but @sminnee already said everything I was about to say, to +1 to his evaluation.
I'd say at a minimum, the current behaviour needs to be clearly documented which I don't think it currently is. I don't think we can make any functional changes outside of a major release.
Going forward though, it's worth looking at our options.
The intuitive thing for me (and this could be done in a major release) would be that $cascade_deletes
works the same way for all relations (i.e. it deletes the record at the end of the relationship), and that possibly a new separate config is used (let's call it $cascade_unlink_relation
for conversation's sake) to cascade delete the relation.
There should be no reason for this configuration to differentiate between a basic many_many
relation and a many_many
through relation.
The docs claim that
cascade_deletes
supports all types of relationships (inclmany_many
). But in the case ofmany_many
through
, it deletes the end of that relationship rather than the join record. In the vast majority of cases, this won't be the correct behaviour.I'd suggest that we solve this by a new
ManyManyThroughList->getJoins()
method which returns all the join DataObject records. And then incascade_deletes
, we explicitly defineMyManyMany.Joins
.See failing unit test below.
PRs