I'm developing an application in Zend Framework to handle the rentals for a commercial property rental company. The company has multiple buildings which each have multiple floors, which each have multiple units.
The models I've setup just extend Zend_Db_Table_Abstract, and I've set them up with $_dependentTables and $_referenceMaps with cascading delete, such that when I delete a floor, all the units within it are deleted too, and when I delete a building, all the floors in it are deleted. However, when I delete a building and the floors are deleted, the delete is not cascaded through to each floor's units. (edit: I'm using MySQL, so I am not able to use referencial integrity at the db level.)
I've looked at how the deletes are cascaded, and it appears they aren't cascading because the cacaded deletes are executed using a Zend_Db_Table object, not a Zend_Db_Table_Row object (which you have to use to achieve cascading).
Is there any way I can update the system so that the delete cascades all the way down? Is there a way I can modify the relationships of my classes, or would I need to use something like Doctrine?
(I guess I could override the delete() method for the row of each table or something, but I just wondered if this is possible using the relationships functionality of ZF?)
If it helps, here's the relevant parts of the class definitions:
class Buildings extends Zend_Db_Table
{
protected $_dependentTables = array('Floors');
}
class Floors extends Zend_Db_Table
{
protected $_dependentTables = array('Units');
protected $_referenceMap = array(
'Building' => array(
'columns' => 'building_id',
'refTableClass' => 'Buildings',
'refColumns' => 'id',
'onDelete' => self::CASCADE,
));
}
class Units extends Zend_Db_Table
{
protected $_referenceMap = array(
'Floor' => array(
'columns' => 'floor_id',
'refTableClass' => 'Floors',
'refColumns' => 'id',
'onDelete' => self::CASCADE,
));
}
Just to be sure... Are you using a RDBMS that doesn't support referencial integrity?
For my taste, it's easier (and more portable, in case you decide to access the DB from another application in the future) to declare the ON DELETE CASCADE in your RDBMS (provided that it allows it), instead of emulating it with the framework.
It seems that the Zend Framework documentation also advices in this sense:
http://framework.zend.com/manual/en/zend.db.table.relationships.html#zend.db.table.relationships.cascading
Related
Couldn't find this in the docs.
Is there any standard way, without creating a custom widget, or overriding the view template, to show a Many to Many relationships in a CRUD's showOperation in Backpack for Laravel? If the answer is NO, what would be your approach to implement it?
Let's say I have a Course Model, and a User model, and there is a Many to Many between both
class Course extends Model
{
public function students()
{
return $this->belongsToMany(User::class, 'course_students');
}
}
class User extends Model
{
public function courses()
{
return $this->belongsToMany(Course::class, 'course_students');
}
}
In the Show Operation for the Course. How do I show a Table with all students?
Indeed, you can use the relationship column for this
Excerpt:
Output the related entries, no matter the relationship:
1-n relationships - outputs the name of its one connected entity;
n-n relationships - enumerates the names of all its connected entities;
Its name and definition is the same as for the relationship field
type:
[
// any type of relationship
'name' => 'tags', // name of relationship method in the model
'type' => 'relationship',
'label' => 'Tags', // Table column heading
// OPTIONAL
// 'entity' => 'tags', // the method that defines the relationship in your Model
// 'attribute' => 'name', // foreign key attribute that is shown to user
// 'model' => App\Models\Category::class, // foreign key model
],
Backpack tries to guess which attribute to show for the related item.
Something that the end-user will recognize as unique. If it's
something common like "name" or "title" it will guess it. If not, you
can manually specify the attribute inside the column definition, or
you can add public $identifiableAttribute = 'column_name'; to your
model, and Backpack will use that column as the one the user finds
identifiable. It will use it here, and it will use it everywhere you
haven't explicitly asked for a different attribute.
In the RC1 of EntityFramework 7, released yesterday, Cascade Delete was added.
To disable it per relationship, I can use :
builder.Entity<Site>().HasOne(e => e.Person)
.WithMany(x => x.Sites).Metadata.DeleteBehavior = DeleteBehavior.Restrict;
I want to disable it globally for a DbContext, but I didn't find a way. How can I do ?
Someone stated on the github project forum that the only way to do it right now is to iterate through all relationships in the method OnModelCreating(ModelBuilder builder), and set the DeleteBehavior property to DeleteBehavior.Restrict :
foreach (var relationship in builder.Model.GetEntityTypes().SelectMany(e => e.GetForeignKeys()))
{
relationship.DeleteBehavior = DeleteBehavior.Restrict;
}
Right now conventions are not configurable. The current CascadeDelete convention only applies to required relationships. Relationships Conventions: Cascade Delete on efproject.net (Official EF7 docs) You could disable the required relationship explicitly if you understand well the consequences.
modelBuilder.Entity<Site>()
.HasOne(p => p.Person)
.WithMany(b => b.Sites)
.IsRequired(false);
Otherwise (and recommended), you need to set the On Delete behavior explicitly ( as you already discovered).
modelBuilder.Entity<Site>()
.HasOne(p => p.Person)
.WithMany(b => b.Sites)
.OnDelete(DeleteBehavior.Restrict);
I have 2 entities,
News
FileAttachment
I wanted to configure using code-first fluent API so that Each News can have 0,1 or more than 1 attachments.
here is what i'm using right now
public NewsMap()
{
this.ToTable("News"); // Table Name
this.HasKey(m => m.Id); // Primary Key
// Field Definition
this.Property(m => m.Title).HasMaxLength(255).IsRequired();
this.Property(m => m.Body).HasColumnType("Text").IsRequired();
this.Property(m => m.Summary).HasMaxLength(1000).IsRequired();
this.Property(m => m.AuthorId).IsRequired();
this.Property(m => m.CreatedOn).IsRequired();
this.Property(m => m.UpdatedOn).IsRequired();
this.HasMany(m => m.Attachments).WithMany().Map(m => m.MapLeftKey("NewsId").MapRightKey("AttachmentId"));
}
public class FileAttachmentMap : EntityTypeConfiguration<FileAttachment>
{
public FileAttachmentMap()
{
this.ToTable("FileAttachments"); // Table Name
this.HasKey(m => m.Id); // Primary Key
// Field Definition
this.Property(m => m.DisplayName).HasMaxLength(256).IsRequired();
this.Property(m => m.PhysicalFileName).HasMaxLength(256).IsRequired();
this.Property(m => m.Extension).HasMaxLength(50).IsRequired();
this.Property(m => m.IsImage).IsRequired();
this.Property(m => m.ThumbTiny).HasMaxLength(275).IsOptional();
this.Property(m => m.ThumbSmall).HasMaxLength(275).IsOptional();
this.Property(m => m.ThumbMid).HasMaxLength(275).IsOptional();
this.Property(m => m.ByteSize).IsRequired();
this.Property(m => m.StorageType).IsRequired();
this.Property(m => m.CreatedOn).IsRequired();
this.Property(m => m.UpdatedOn).IsRequired();
}
}
This mapping correctly generates an intermediate table named NewsFileAttachment with two fields :
NewsId
AttachmentId
On News Entity when i call News.Attachments.Add(Attachment); it correctly adds records in both Attachment & NewsAttachment tables.
When i remove some list item from News.Attachments it correctly removes record from NewsAttachment table, but it doesn't delete record in FileAttachment table. I wanted to remove that too.
Can someone please suggest a better Fluent API configuration to achieve this?
Thanks,
Amit
EDIT
In my case FileAttachment stores files for various purpose. i've Blog entity that too have attachments. So, two intermediate tables BlogAttachments & FileAttachments. Now if i use WithOptional as (I can't use WithRequired as i need BlogId & NewsId both in FileAttachment table), i can get rid off intermediate table, but still delete doesn't delete record from FileAttachment table, it just make NewsId/BlogId NULL.
Any suggestion? Main thing is I do not wanted to create separate tables with all the fields i have in FileAttachment table.
That's expected - as it creates many-to-many and extra table - the cascade only applies to that table.
There is no direct 'FK' relationship in between your News and
Attachment, as it goes through a join table. And thus you cannot expect for e.g. attachment to be deleted, if the news does - as attachment could have other news relating to it.
See also this one - it's somewhat relevant.
One to Many Relationship with Join Table using EF Code First
i.e. if your structure permits don't explicitly create many-to-many (don't put collection on both sides, or similar in fluent config).
In your case providing your 'attachments' are not reusable in between News - then just put a collection navigation property in the News - and leave attachment w/o any - or make a 'FK', single instance navigation from Attachment (like a 'Parent') if you need it.
On the other side, if an attach... could be parented by different
news records - then you shouldn't have cascade delete anyways.
note: check your generated migration script - or SQL/Db - to see exactly what it creates - and make sure there is no intermediate table created - and only one 'FK' going from 'attachment' to 'news'.
edit:
modelBuilder.Entity<News>()
.HasMany(c => c.Attachments)
.WithOptional() // or WithRequired (test to see which is better for you)
.WillCascadeOnDelete(true);
...and make one public ICollection<FileAttachment> Attachments {get;set;} in the News.
(actually the collection property is all you need - but configuration is to be safe you get what you want)
That'd make you 1-to-many (or many-to-one), which is the nature of your data (as you said in comments) - and you can have cascade deletes.
My Product entity has the following structure:
private $id;
private $title;
/**
* #ManyToOne(targetEntity="Category")
* #JoinColumn(name="cat_id", referencedColumnName="id")
*/
private $category;
Category have nested structure. And each level of nesting is shown in 5 separate fields:
In class form code, I solve it in this way:
$builder
->add('cat_1', 'entity', array(
...
'query_builder' => function() { return someSelectLogic1(); }
))
->add('cat_2', 'entity', array(
...
'query_builder' => function() { return someSelectLogic2(); }
))
->add('cat_3', 'entity', array(
...
'query_builder' => function() { return someSelectLogic3(); }
))
->add('cat_4', 'entity', array(
...
'query_builder' => function() { return someSelectLogic4(); }
))
->add('cat_5', 'entity', array(
...
'query_builder' => function() { return someSelectLogic5(); }
))
Now I need to know which field is filled in the last turn and pass the value of that field in the entity property.
In all that I do not like:
complex logic to determine which field with category was filled at the end
each of these fields is not tied to the entity 'mapped' => false
1) What the right way to organize code of my form?
2) And is there a way to bring these fields into a separate class which will deal with the logic of determining which category was chosen in the end?
I would suggest the following:
1) Create a new custom form field type and put all those entity in there.
This process is not much different from ordinary creation of form type. Just enclose those fields in it's own buildForm() and that should do the trick. Docs.
2) Mark all those entity fields with property "property_path => false".
Clearly you wont be storing these values inside your model.
3) Add two more fields: chosen and lastOne.
Now, this might be tricky: I would either set the chosen to text type (basically, generic type) or would use entity as well. If you go for entity you would need to include all possible answers from all entity fields. As for the lastOne set it to text as it will reflect which field (by name) was selected last.
Either way, those two fields will be invisible. Don't forget to set property_path to false for lastOne field.
4) Finally, add ValueTransformer (docs) which will contain logic to "see" which field was selected last.
Now, I dealt with it only once and don't understand it just quite yet, so your best bet would be trial and error with examples from official docs, unfortunately.
What basically you should do is to, within value-transformer, read the value of field lastOne. This will give you the name of field which was selected last. Then, using that value, read the actual last value selected. Last, set that value (object, if you've went for entity type, or it's ID otherwise) to chosen field.
That should basically do the thing.
As for the JS, I don't know if you're using any framework but I will assume jQuery. You will need to set lastOne field as your selecting items in your form.
$(function(){
$('#myform').find('select').on('change', function(){
var $this = $(this);
$this.closest('form').find('#__ID_OF_YOUR_LASTONE_FIELD').val($this.attr('name'));
});
});
I'm sorry I cannot provide you with code samples for PHP right now. It's a bit late here and will do my best to further update this answer tomorrow.
Example:
class Products extends Zend_Db_Table_Abstract
{
protected $_name = 'products';
protected $_referenceMap = array(
'Bug' => array(
'columns' => array('bug_id'),
'refTableClass' => 'Bugs',
'refColumns' => array('bug_id')
)
);
}
$object = new Products();
$select = $object->select()->from()->Join('Bug');
Instead of defining the full join statement
As far as I can tell $_referenceMap is not used in that way. $_referenceMap defines the relationship that a table row has with other tables.
That's why the associated findDependentRowset(), findManyToManyRowset() and findParentRow() are found in Zend_db_Table_Row_Abstract. These methods create the Joins.
So to get the dependent rows from Bugs, using a select object, you would do something like this, assuming that Products has a one-to-many relationship with Bugs;
class Products extends Zend_Db_Table_Abstract
{
protected $_name = 'products';
protected $_dependentTables = array('Bugs');
}
class Bugs extends Zend_Db_Table_Abstract
{
protected $_referenceMap = array(
'Products' => array(
'columns' => array('bug_id')
,'refTableClass' => 'Products'
,'refColumns' => array('bug_id')
)
);
}
To get dependent rows you first have to fetch a parent row.
$products = new Products();
$productRow = $products->find(123)
->current();
You can refine the join by using Zend_Db_Select
$select = $products->select()
->where('foo_bar = ?', 'cheese')
->limit(2);
Finally querying the dependent rows by passing in the select object instead instead of a rule key.
$bugRowset = $productRow->findDependentRowset('Bugs', 'Products', $select);
I think this will work, I'll have to check tomorrow morning.
This is usefull for one row, but not for the whole table (or several rows). I usually need queries affecting more than one row... Zend should implement the option mentioned by Phliplip, or something similar:
$select = $object->select()->from()->Join('Bug');
Note: I mean, using only one query