/** * Delete record represented by this object. Uses the Datamodel object * to generate possible dependencies and relationships. * * @param bool $pb_delete_related delete stuff related to the record? pass non-zero value if you want to. * @param array $pa_options Options for delete process. Options are: * hard = if true records which can support "soft" delete are really deleted rather than simply being marked as deleted * queueIndexing = * @param array $pa_fields instead of deleting the record represented by this object instance you can * pass an array of field => value assignments which is used in a SQL-DELETE-WHERE clause. * @param array $pa_table_list this is your possibility to pass an array of table name => true assignments * to specify which tables to omit when deleting related stuff */ public function delete($pb_delete_related = false, $pa_options = null, $pa_fields = null, $pa_table_list = null) { if (!is_array($pa_options)) { $pa_options = array(); } $pb_queue_indexing = caGetOption('queueIndexing', $pa_options, false); $vn_id = $this->getPrimaryKey(); if ($this->hasField('deleted') && (!isset($pa_options['hard']) || !$pa_options['hard'])) { if ($this->getMode() == ACCESS_WRITE) { $vb_we_set_transaction = false; if (!$this->inTransaction()) { $o_t = new Transaction($this->getDb()); $this->setTransaction($o_t); $vb_we_set_transaction = true; } $this->setMode(ACCESS_WRITE); $this->set('deleted', 1); if ($vn_rc = $this->update(array('force' => true))) { if (!defined('__CA_DONT_DO_SEARCH_INDEXING__') || !__CA_DONT_DO_SEARCH_INDEXING__) { $o_indexer = $this->getSearchIndexer(); $o_indexer->startRowUnIndexing($this->tableNum(), $vn_id); $o_indexer->commitRowUnIndexing($this->tableNum(), $vn_id, array('queueIndexing' => $pb_queue_indexing)); } } $this->logChange("D"); if ($vb_we_set_transaction) { $this->removeTransaction(true); } return $vn_rc; } else { $this->postError(400, _t("Mode was %1; must be write", $this->getMode(true)), "BaseModel->delete()"); return false; } } $this->clearErrors(); if (!$this->getPrimaryKey() && !is_array($pa_fields)) { # is there a record loaded? $this->postError(770, _t("No record loaded"), "BaseModel->delete()"); return false; } if (!is_array($pa_table_list)) { $pa_table_list = array(); } $pa_table_list[$this->tableName()] = true; if ($this->getMode() == ACCESS_WRITE) { $vb_we_set_transaction = false; if (!$this->inTransaction()) { $o_t = new Transaction($this->getDb()); $this->setTransaction($o_t); $vb_we_set_transaction = true; } $o_db = $this->getDb(); if (is_array($pa_fields)) { $vs_sql = "DELETE FROM " . $this->tableName() . " WHERE "; $vs_wheres = ""; while (list($vs_field, $vm_val) = each($pa_fields)) { $vn_datatype = $this->_getFieldTypeType($vs_field); switch ($vn_datatype) { # ----------------------------- case 0: # number if ($vm_val == "") { $vm_val = 0; } break; # ----------------------------- # ----------------------------- case 1: # string $vm_val = $this->quote($vm_val); break; # ----------------------------- } if ($vs_wheres) { $vs_wheres .= " AND "; } $vs_wheres .= "({$vs_field} = {$vm_val})"; } $vs_sql .= $vs_wheres; } else { $vs_sql = "DELETE FROM " . $this->tableName() . " WHERE " . $this->primaryKey() . " = " . $this->getPrimaryKey(1); } if ($this->isHierarchical()) { // TODO: implement delete of children records $vs_parent_id_fld = $this->getProperty('HIERARCHY_PARENT_ID_FLD'); $qr_res = $o_db->query("\n\t\t\t\t\tSELECT " . $this->primaryKey() . "\n\t\t\t\t\tFROM " . $this->tableName() . "\n\t\t\t\t\tWHERE\n\t\t\t\t\t\t{$vs_parent_id_fld} = ?\n\t\t\t\t", $this->getPrimaryKey()); if ($qr_res->nextRow()) { $this->postError(780, _t("Can't delete item because it has sub-records"), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } # # --- begin delete search index entries # if (!defined('__CA_DONT_DO_SEARCH_INDEXING__')) { $o_indexer = $this->getSearchIndexer(); $o_indexer->startRowUnIndexing($this->tableNum(), $vn_id); // records dependencies but does not actually delete indexing } # --- Check ->many and many<->many relations $va_one_to_many_relations = $this->_DATAMODEL->getOneToManyRelations($this->tableName()); # # Note: cascading delete code is very slow when used # on a record with a large number of related records as # each record in check individually for cascading deletes... # it is possible to make this *much* faster by crafting clever-er queries # if (is_array($va_one_to_many_relations)) { foreach ($va_one_to_many_relations as $vs_many_table => $va_info) { foreach ($va_info as $va_relationship) { if (isset($pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]]) && $pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]]) { continue; } # do any records exist? $t_related = $this->_DATAMODEL->getTableInstance($vs_many_table); $o_trans = $this->getTransaction(); $t_related->setTransaction($o_trans); $qr_record_check = $o_db->query("\n\t\t\t\t\t\t\tSELECT " . $t_related->primaryKey() . "\n\t\t\t\t\t\t\tFROM " . $vs_many_table . "\n\t\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t\t(" . $va_relationship["many_table_field"] . " = " . $this->getPrimaryKey(1) . ")\n\t\t\t\t\t\t"); $pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]] = true; //print "FOR ".$vs_many_table.'/'.$va_relationship["many_table_field"].":".$qr_record_check->numRows()."<br>\n"; if ($qr_record_check->numRows() > 0) { if ($pb_delete_related) { while ($qr_record_check->nextRow()) { if ($t_related->load($qr_record_check->get($t_related->primaryKey()))) { $t_related->setMode(ACCESS_WRITE); $t_related->delete($pb_delete_related, array_merge($pa_options, array('hard' => true)), null, $pa_table_list); if ($t_related->numErrors()) { $this->postError(790, _t("Can't delete item because items related to it have sub-records (%1)", $vs_many_table), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } } } else { $this->postError(780, _t("Can't delete item because it is in use (%1)", $vs_many_table), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } } } } # --- do deletion if ($this->debug) { echo $vs_sql; } $o_db->query($vs_sql); if ($o_db->numErrors() > 0) { $this->errors = $o_db->errors(); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } # # --- complete delete of search index entries # if (!defined('__CA_DONT_DO_SEARCH_INDEXING__')) { $o_indexer->commitRowUnIndexing($this->tableNum(), $vn_id, array('queueIndexing' => $pb_queue_indexing)); } # cancel and pending queued tasks against this record $tq = new TaskQueue(); $tq->cancelPendingTasksForRow(join("/", array($this->tableName(), $vn_id))); $this->_FILES_CLEAR = array(); # --- delete media and file field files foreach ($this->FIELDS as $f => $attr) { switch ($attr['FIELD_TYPE']) { case FT_MEDIA: $versions = $this->getMediaVersions($f); foreach ($versions as $v) { $this->_removeMedia($f, $v); } $this->_removeMedia($f, '_undo_'); break; case FT_FILE: @unlink($this->getFilePath($f)); #--- delete conversions # foreach ($this->getFileConversions($f) as $vs_format => $va_file_conversion) { @unlink($this->getFileConversionPath($f, $vs_format)); } break; } } if ($o_db->numErrors() == 0) { //if ($vb_is_hierarchical = $this->isHierarchical()) { //} # clear object $this->logChange("D"); $this->clear(); } else { if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } if ($vb_we_set_transaction) { $this->removeTransaction(true); } return true; } else { $this->postError(400, _t("Mode was %1; must be write", $this->getMode(true)), "BaseModel->delete()"); return false; } }
/** * Delete record represented by this object. Uses the Datamodel object * to generate possible dependencies and relationships. * * @param bool $pb_delete_related delete stuff related to the record? pass non-zero value if you want to. * @param array $pa_options Options for delete process. Options are: * hard = if true records which can support "soft" delete are really deleted rather than simply being marked as deleted * @param array $pa_fields instead of deleting the record represented by this object instance you can * pass an array of field => value assignments which is used in a SQL-DELETE-WHERE clause. * @param array $pa_table_list this is your possibility to pass an array of table name => true assignments * to specify which tables to omit when deleting related stuff */ public function delete($pb_delete_related = false, $pa_options = null, $pa_fields = null, $pa_table_list = null) { if (!is_array($pa_options)) { $pa_options = array(); } $vn_id = $this->getPrimaryKey(); if ($this->hasField('deleted') && (!isset($pa_options['hard']) || !$pa_options['hard'])) { $this->setMode(ACCESS_WRITE); $this->set('deleted', 1); if ($vn_rc = $this->update(array('force' => true))) { if (!defined('__CA_DONT_DO_SEARCH_INDEXING__')) { if (!BaseModel::$search_indexer) { BaseModel::$search_indexer = new SearchIndexer($this->getDb()); } BaseModel::$search_indexer->startRowUnIndexing($this->tableNum(), $vn_id); BaseModel::$search_indexer->commitRowUnIndexing($this->tableNum(), $vn_id); } } $this->logChange("D"); return $vn_rc; } $this->clearErrors(); if (!$this->getPrimaryKey() && !is_array($pa_fields)) { # is there a record loaded? $this->postError(770, _t("No record loaded"), "BaseModel->delete()"); return false; } if (!is_array($pa_table_list)) { $pa_table_list = array(); } $pa_table_list[$this->tableName()] = true; if ($this->getMode() == ACCESS_WRITE) { $vb_we_set_transaction = false; if (!$this->inTransaction()) { $o_t = new Transaction($this->getDb()); $this->setTransaction($o_t); $vb_we_set_transaction = true; } $o_db = $this->getDb(); if (is_array($pa_fields)) { $vs_sql = "DELETE FROM " . $this->tableName() . " WHERE "; $vs_wheres = ""; while (list($vs_field, $vm_val) = each($pa_fields)) { $vn_datatype = $this->_getFieldTypeType($vs_field); switch ($vn_datatype) { # ----------------------------- case 0: # number if ($vm_val == "") { $vm_val = 0; } break; # ----------------------------- # ----------------------------- case 1: # string $vm_val = $this->quote($vm_val); break; # ----------------------------- } if ($vs_wheres) { $vs_wheres .= " AND "; } $vs_wheres .= "({$vs_field} = {$vm_val})"; } $vs_sql .= $vs_wheres; } else { $vs_sql = "DELETE FROM " . $this->tableName() . " WHERE " . $this->primaryKey() . " = " . $this->getPrimaryKey(1); } if ($this->isHierarchical()) { // TODO: implement delete of children records $vs_parent_id_fld = $this->getProperty('HIERARCHY_PARENT_ID_FLD'); $qr_res = $o_db->query("\n\t\t\t\t\tSELECT " . $this->primaryKey() . "\n\t\t\t\t\tFROM " . $this->tableName() . "\n\t\t\t\t\tWHERE\n\t\t\t\t\t\t{$vs_parent_id_fld} = ?\n\t\t\t\t", $this->getPrimaryKey()); if ($qr_res->nextRow()) { $this->postError(780, _t("Can't delete item because it has sub-records"), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } # # --- delete search index entries # // TODO: FIX THIS ISSUE! // NOTE: we delete the indexing here, before we actually do the // SQL delete because the search indexer relies upon the relevant // relationships to be intact (ie. exist) in order to properly remove the indexing for them. // // In particular, the incremental indexing used by the MySQL Fulltext plugin fails to properly // update if it can't traverse the relationships it is to remove. // // By removing the search indexing here we run the risk of corrupting the search index if the SQL // delete subsequently fails. Specifically, the indexing for rows that still exist in the database // will be removed. Wrapping everything in a MySQL transaction deals with it for MySQL Fulltext, but // other non-SQL engines (Lucene, SOLR, etc.) are still affected. // // At some point we need to come up with something clever to handle this. Most likely it means moving all of the actual // analysis to startRowUnindexing() and only executing commands in commitRowUnIndexing(). For now we blithely assume that // SQL deletes always succeed. If they don't we can always reindex. Only the indexing is affected, not the underlying data. if (!defined('__CA_DONT_DO_SEARCH_INDEXING__')) { if (!BaseModel::$search_indexer) { BaseModel::$search_indexer = new SearchIndexer($this->getDb()); } BaseModel::$search_indexer->startRowUnIndexing($this->tableNum(), $vn_id); BaseModel::$search_indexer->commitRowUnIndexing($this->tableNum(), $vn_id); } # --- Check ->many and many<->many relations $va_one_to_many_relations = $this->_DATAMODEL->getOneToManyRelations($this->tableName()); # # Note: cascading delete code is very slow when used # on a record with a large number of related records as # each record in check individually for cascading deletes... # it is possible to make this *much* faster by crafting clever-er queries # if (is_array($va_one_to_many_relations)) { foreach ($va_one_to_many_relations as $vs_many_table => $va_info) { foreach ($va_info as $va_relationship) { if (isset($pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]]) && $pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]]) { continue; } # do any records exist? $t_related = $this->_DATAMODEL->getTableInstance($vs_many_table); $o_trans = $this->getTransaction(); $t_related->setTransaction($o_trans); $qr_record_check = $o_db->query("\n\t\t\t\t\t\t\tSELECT " . $t_related->primaryKey() . "\n\t\t\t\t\t\t\tFROM " . $vs_many_table . "\n\t\t\t\t\t\t\tWHERE\n\t\t\t\t\t\t\t\t(" . $va_relationship["many_table_field"] . " = " . $this->getPrimaryKey(1) . ")\n\t\t\t\t\t\t"); $pa_table_list[$vs_many_table . '/' . $va_relationship["many_table_field"]] = true; //print "FOR ".$vs_many_table.'/'.$va_relationship["many_table_field"].":".$qr_record_check->numRows()."<br>\n"; if ($qr_record_check->numRows() > 0) { if ($pb_delete_related) { while ($qr_record_check->nextRow()) { if ($t_related->load($qr_record_check->get($t_related->primaryKey()))) { $t_related->setMode(ACCESS_WRITE); $t_related->delete($pb_delete_related, array_merge($pa_options, array('hard' => true)), null, $pa_table_list); if ($t_related->numErrors()) { $this->postError(790, _t("Can't delete item because items related to it have sub-records (%1)", $vs_many_table), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } } } else { $this->postError(780, _t("Can't delete item because it is in use (%1)", $vs_many_table), "BaseModel->delete()"); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } } } } } # --- do deletion if ($this->debug) { echo $vs_sql; } $o_db->query($vs_sql); if ($o_db->numErrors() > 0) { $this->errors = $o_db->errors(); if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } # cancel and pending queued tasks against this record $tq = new TaskQueue(); $tq->cancelPendingTasksForRow(join("/", array($this->tableName(), $vn_id))); $this->_FILES_CLEAR = array(); # --- delete media and file field files foreach ($this->FIELDS as $f => $attr) { switch ($attr['FIELD_TYPE']) { case FT_MEDIA: $versions = $this->getMediaVersions($f); foreach ($versions as $v) { $this->_removeMedia($f, $v); } $this->_removeMedia($f, '_undo_'); break; case FT_FILE: @unlink($this->getFilePath($f)); #--- delete conversions # foreach ($this->getFileConversions($f) as $vs_format => $va_file_conversion) { @unlink($this->getFileConversionPath($f, $vs_format)); } break; } } if ($o_db->numErrors() == 0) { //if ($vb_is_hierarchical = $this->isHierarchical()) { //} # clear object $this->logChange("D"); $this->clear(); } else { if ($vb_we_set_transaction) { $this->removeTransaction(false); } return false; } if ($vb_we_set_transaction) { $this->removeTransaction(true); } return true; } else { $this->postError(400, _t("Mode was %1; must be write", $this->getMode(true)), "BaseModel->delete()"); return false; } }