How to delete many-to-many relations in yii2 (pivot table) - eloquent

I have the relation declared using ActiveRecord->hasMany()
public function getChildren()
{
return $this->hasMany(File::class, ['id' => 'child_file_id'])
->viaTable('file_image_list', ['parent_file_id' => 'id']);
}
I can get related models, but i dont know how to delete relations (not models).
Is Yii2 have something like associate or sync in Eloquent?
Can i do something like $file->children->delete() without loops?

As you have junction table file_image_list and you do not want to delete the File models, you probably want to use unlinkAll() method of BaseActiveRecord class.
If will only nullify/destroy the record from your mapping table (file_image_list), moreover you don't have to use loop for objects children.
I believe this is what you need:
/** #see File::getChildren() */
$file->unlinkAll('children', true);
Pay attention that unlinkAll(name, delete) method has two parameters:
name - is the relation name. (I wrote #see phpdoc above in order to make it possible to find usages on that relation, if you decide to change/refactor it one day)
delete - whether to delete the record in the junction table. If set to false it will just nullify foreign key from that table (so make sure that column is nullable)

Related

How to use joinwith in yii2 framework

I have created a curd called 'Status' and it is working properly. I have created a new Model(with table) called relation(id, status_id, user_id).
Now I want to join relation table to Status while showing records with some custom condition.
How to do that?
To make connection between two tables with foreign key create function in model Relation like this.
public function getStatus()
{
return $this->hasOne(Status::className(),['id'=>'status_id']);
}
To access value of status from relation model object, you can like this.
$relation->status->name_of_attribute;
$relation one object from Relation model.

Entity Framework object graph deletion with Breeze

I am encountering a recurring problem that just makes no sense, and hoping someone (in the Breeze team?) can shed some light.
The following model illustrates the entities in question.
As you can see, I'm adhering pretty strictly to Entity Framework conventions in my property names, and as a result, if I check in SQL the cascade on delete rules are set by EF code first when it creates the db.
Now, when I try to delete a BusUnit manually in SQL, the delete cascades correctly and the corresponding BusUnitDimensions are also deleted, as it should be. Likewise, if I delete a Dimension in SQL, the corresponding BusUnitDimensions are also deleted.
However, in my application, if I mark a BusUnit as setDeleted with Breeze and then try saveChanges, I get the following error.
The operation failed: The relationship could not be changed because one
or more of the foreign-key properties is non-nullable. When a change is
made to a relationship, the related foreign-key property is set to a null
value. If the foreign-key does not support null values, a new relationship
must be defined, the foreign-key property must be assigned another
non-null value, or the unrelated object must be deleted.
Strangely though, if I mark a Dimension for deletion and then save (within Breeze), the cascaded delete works correctly and both the Dimension and its corresponding BusUnitDimensions are deleted.
So, why the inconsistency? Why are the cascaded delete rules in SQL not being applied for BusUnits but yet they're working for Dimensions? I've read elsewhere that Breeze does not support cascaded deletes, but then why is my Dimensions case working?
EDIT:
I've removed my previous edits as they weren't relevant. The changes below follow on from Ward's answer...
My model now looks like this, and BusUnitDims now uses BusUnitId and DimId as a compound key, and I've added a bool, IsBud for the purposes of payload.
I haven't yet implemented deletes for BusUnits, but already if I try delete a Dim, I'm getting the same error message:
The operation failed: The relationship could not be changed because one
or more of the foreign-key properties is non-nullable. When a change is
made to a relationship, the related foreign-key property is set to a null
value. If the foreign-key does not support null values, a new relationship
must be defined, the foreign-key property must be assigned another
non-null value, or the unrelated object must be deleted.
I have noticed that cascaded deletes is no longer enabled, and in fact, to get EF to build the database I to add the following configuration:
modelBuilder.Entity<BusUnitDim>()
.HasRequired(bud => bud.BusUnit)
.WithMany(bu => bu.BusUnitDims)
.HasForeignKey(bud => bud.BusUnitId)
.WillCascadeOnDelete(false);
modelBuilder.Entity<BusUnitDim>()
.HasRequired(bud => bud.Dim)
.WithMany(d => d.BusUnitDims)
.HasForeignKey(bud => bud.DimId)
.WillCascadeOnDelete(false);
So, with cascading now explicitly not in place, I can understand why the error occurs. Does that imply that in the controller, one has to specifically mark each map for deletion when deleting a parent Dim or BusUnit and before saveChanges is called, or is there some way to configure EF to take advantage of cascaded deletes as this would hugely simplify the code in my controller?
(PS: it gets even more complex, because BusUnitDims ends up having a further join table of its own, MetricBusUnitDims to accommodate yet another entity in the model and their relationships. This is why I'm trying to get the principles right early on)
EDIT: (A CONTROLLER SOLUTION FOR BUSUNITS)
So, the following approach works for BusUnits:
function deleteBusUnit(busUnitVm) { // note that you pass in the item viewmodel, not the entity
var busUnit = busUnitVm.busUnit;
var mapVms = busUnitVm.dimMapVms;
var dimHash = createBusUnitDimHash(busUnit);
mapVms.forEach(function (mapVm) {
var map = dimHash[mapVm.dim.id];
if (map) {
datacontext.markDeleted(map);
}
});
datacontext.markDeleted(busUnit);
save().then(function() { getDBoardConfig(); });
}
}
Is this the correct approach? if so, I'll still have to figure out the following:
How to approach Dims. These are different becuase the item viewmodel is defined for BusUnits.
How to approach the situation where there is a join tabel one level down, e.g. MetricBusUnitDIm.
EDIT: (A CONTROLLER SOLUTION FOR DIMS)
function deleteDim(dim) {
return bsDialog.deleteDialog(dim.name, true)
.then(function () {
vm.busUnitVms.forEach(function (busUnitVm) {
busUnitVm.busUnit.busUnitDims.forEach(function (bud) {
if (bud.dimId === dim.id) {
datacontext.markDeleted(bud);
}
});
});
datacontext.markDeleted(dim);
save().then(function () { getDboardConfig(); });
});
}
I believe your problems are traceable to the fact that your mapping table BusUnitDimension has its own primary key, Id, as opposed to the more typical approach in which the BusUnitId and DimensionId FK properties together comprise the compound primary key of BusUnitDimension.
Observe that OrderDetails in Northwind and the HeroPoweMap in the Breeze many-to-many example have compound keys.
Your choice creates complications.
First, it becomes possible to create multiple BusUnitDimension entities representing the same association between BusUnit and Dimension (i.e., they all have the same pair of FKs). The database may be able to prevent this (it's been a long time since I looked) but whether it does or doesn't, it won't prevent you from creating those duplicates in Breeze ... and maybe not in EF either.
Secondly, it opens you up to the problem you're currently facing. If those mapping entities are in the DbContext when you perform the delete, EF may (apparently does) try to null their FK properties as it sets either BusUnit or Dimension to the deleted state.
You can get around this, as has been suggested, by making both the BusUnitId and DimensionId FK properties nullable. But that is contrary to the semantics as a BusUnitDimension must link a real BusUnit to a real Dimension; they aren't optional. The practical consequence may be that you don't get cascade delete from the EF perspective if you do this (not sure if the DB will enforce that either). That means you'd have orphaned BusUnitDimension rows in your database with one or both FKs being null. I speculate because I'm not used to getting into this kind of trouble.
Another approach would be to set their FK values to zero (I think Breeze does this for you). Of course this implies the existence of BusUnit and Dimension table rows with Id == 0, if only during the delete operation.
Btw, you could actually have such "sentinel entities" in your DB.
You must make sure that these BusUnitDimension are in the deleted state or EF (and the DB) will either reject them (referential integrity constraint) or orphan them (you'll have BusUnitDimension rows in your database with one or both FKs being zero).
Alternatively, if you know that the DB will cascade delete them, you can simply remove them from the DbContext (remove from the EntityInfoMap in the EFContextProvider). But now you have to tell the Breeze client to get rid of them too if it happens to have them hanging around.
Enough Already!
These wandering thoughts should tell you that you've got yourself in a jam here with way too much bookkeeping ... and all because you gave BusUnitDimension its own Id primary key.
It gets a lot easier if you give BusUnitDimension the compound key, {BusUnitId, DimensionId}. You must also give it a payload property (anything will do) to prevent EF from hiding it in its "many-to-many" implementation because Breeze doesn't handle that. Adding any nonsense property will do the trick.
HTH
That has nothing to do with Breeze.. The originating message is from Entity Framework..
inside BusUnitDimension Model update BusUnitId property to:
public Nullable<int> BusUnitId { get; set; }
Notice the Nullable struct..

adding entries to the "Relational" table in entity model? how do i do that?

so the story is very simple.
I have one table called Products and another Called categories. In addition, i have another table called ProductCategories that hold the relationship of catetories to their corresponding products (i.e, the table has two columns, ProductId, ColumnId).
For some reason, after adding all those table to my entity model, i don't have "Access" to it, hence i can do myentityModel.ProductCategories, so i could relational items between those two tables.
And yes, the ProductCategores table is added as "Association" to the entity model. i don't really understand that.
EDIT:
I do see that as part of creating new "Product" i can pass EntityCollection of "Category". So i do query from my entity model for a list of the matching categories that the user selected (on the webpage). so for example, i get (after query the model), an Objectset of "Category". However, i encountered two issues:
the 'AddObject' accept only EntityCollection, hence i need to re-create a set and then add all the objects from the ObjectSet to the entityCollection, in this process i need to detach it from the previous model and add it to the new collection. if not, i get an exception.
when i do the SaveChanges, i see that i get an exception that it was actually trying to Create new Category rather than adding new ProductCategory. again, am i missing something here?
Thanks.
This sounds like a Many-to-Many relationship. In your entity model, you don't need to declare the join table as a separate entity. Instead, you configure the relationship between the Products and the Categories as a Many-to-Many and add metadata about the join table. In Hibernate, you would have:
#ManyToMany(targetEntity=Categories.class, cascade={CascadeType.ALL}, fetch = FetchType.LAZY)
#JoinTable(name="tb_products_categories",
joinColumns=#JoinColumn(name="category_id"),
inverseJoinColumns=#JoinColumn(name="product_id")
)
#IndexColumn(name="join_id")
public List<Categories> getCategories() {
return categories;
}
When you query, the ORM layer takes care of determining SQL and traversing table joins.

How do I cascade deletes into a link table using the EF4 fluent API?

I have two tables in an existing (MSSQL 2008 R2) database that are related by a link table.
The two tables are "Plan" and "Tips". The link table is "PlanTipLinks".
Plans can have many tips, and tips can be associated with multiple plans (i.e. it's a many-to-many relationship). In the application, I only care about the "Plan.Tips" relationship. I don't need the Tip.Plans inverse relationship.
The foreign key references in the link table cannot be null.
I'm using the following fluent API code to map this relationship:
modelBuilder.Entity<Plan>()
.HasMany(p => p.Tips)
.WithMany()
.Map("PlanTipLinks", (p, t) =>
new
{
PlanId = p.Id,
TipId = t.Id
});
This create the correct entries in the table. Problem is that, when I delete a plan, I get a foreign key exception on the PlanTipLinks table.
Presumably I need to tell it to cascade into the PlanTipLinks table when a plan is deleted, but I'm not sure how to do that. I don't seem to be able to call the WillCascadeOnDelete method using the HasMany/WithMany methods.
What am I missing here?
As of EF CTP4, there is no way to directly turn on cascade deletes on Many to Many associations by Fluent API.
That said, if your intention is to make sure that you can delete the principle (e.g. a Plan record) without having to worry about the dependent record in the link table (i.e. PlanTipLinks) then you don't need to turn on cascades on the database since EF Code First will take care of the cascade deletes on the client side when it comes to Many to Many associations.
For example, when you delete a Plan object, code first is smart enough to first send a delete statement to get rid of the dependent record in the PlanTipLinks table and after that it will send another delete statement to delete the Plan record.
For more info, please take a look at the following post:
EF CTP4 cascade delete on many to many relationship

In Entity Framework, how do I specify a condition on an association?

I have a relational model with an associative table. But in addition to the related keys, this table also has a flag. I would like to define two associations: one where the flag is true and another where it is false. The EF designer can add a condition to an entity, but not to an association.
The associative table looks like this:
UserPrivilege
-------------
UserId int (FK1)
PrivilegeId int (FK2)
IsGranted bit
I would like to create two associations between the User entity and the Privilege entity: PrivilegesGranted and PrivilegesDenied.
You can't do this directly through the designer.
But this is possible in the XML using DefiningQuery and Create and Delete sprocs. See this old post on my blog for more: Associations with Payloads.
The only thing that is a bit interesting is I assume the PK is just UserId and PrivilegeId, which means a user can't be granted and denied for a particular privilege at the same time.
So if you write code like this:
Privilege p = user.Granted.First();
user.Granted.Remove(p);
user.Denied.Add(p);
ctx.SaveChanges();
Update ordering is important. because you are using a DefiningQuery for both associations, the EF doesn't know they are related, and that it needs to do the delete before it can do the update.
So you might end up with PK violations.
A way to address this issue is in the Insert and Delete sprocs for each association, you can essentially make them change the current row for the User and Privilege pair, if it exists update it with the correct IsGranted value, if not create it, i.e. make it an upsert.
Let me know how you go here
Alex