I want to save a bunch of static records in my database with a given uuid, this is for testing purposes, so that on every system the application starts with the exact same dataset.
When inserting with SQL this is no problem but I wanted to use the CakePHP way ( I use a migrations file for this, but that does not matter).
The problem is that I give cake a data array like this and save it:
$data = [
['id' => '5cedf79a-e4b9-f235-3d4d-9fbeef41c7e8', 'name' => 'test'],
['id' => 'c2bf879c-072c-51a4-83d8-edbf2d97e07e', 'name' => 'test2']
];
$table = TableRegistry::get('My_Model');
$entities = $table->newEntities($data, [
'accessibleFields' => ['*' => true],
'validate' => false
]);
array_map([$table, 'save'], $entities );
Everything saves, but all my items have been given a different uuid, If I debug a record after saving it shows the original uuid in the entity
'new' => false,
'accessible' => [
'*' => true
],
'properties' => [
'id' => '6b4524a8-4698-4297-84e5-5160f42f663b',
'name' => 'test',
],
'dirty' => [],
'original' => [
'id' => '5cedf79a-e4b9-f235-3d4d-9fbeef41c7e8'
],
So why does cake generate a new uuid for me? and how do I prevent it
This doesn't work because primary keys are unconditionally being generated before the insert operation, see
https://github.com/cakephp/cakephp/blob/3.0.0/src/ORM/Table.php#L1486-L1490
// ...
$id = (array)$this->_newId($primary) + $keys;
$primary = array_combine($primary, $id);
$filteredKeys = array_filter($primary, 'strlen');
$data = $filteredKeys + $data;
// ...
$statement = $this->query()->insert(array_keys($data))
->values($data)
->execute();
// ...
Currently the UUID type is the only type that implements generating IDs, so providing custom IDs works with other types.
You can workaround this by for example overriding the _newId() method in your table so that it returns null, which effectively results in the existing primary key not being overwritten.
protected function _newId($primary)
{
// maybe add some conditional logic here
// in case you don't want to be required
// to always manually provide a primary
// key for your insert operations
return null;
}
Related
What I have
My belongsToMany association is similar to the one from CakePHP Cookbook. However, I have set the UNIQUE constraint on tag titles.
(Another difference, which may be irrelevant, is I have added a site_id field next to every tag in the Tags table, and another composite UNIQUE constraint is set on both tag and site_id.)
What doesn't work
Submitting a duplicate tag title results in an error.
When I debug my new Article entity before saving it, I can see that the duplicate tag titles are rejected after a validation attempt.
'tags' => [
// This submitted tag title already exists in Tags
(int) 0 => object(App\Model\Entity\Tag) id:1 {
'site_id' => (int) 2
'[new]' => true
'[accessible]' => [
'site_id' => true,
'title' => true,
'created' => true,
'modified' => true,
'site' => true,
'articles' => true,
]
'[dirty]' => [
'site_id' => true,
]
'[original]' => [
]
'[virtual]' => [
]
'[hasErrors]' => true
'[errors]' => [
'title' => [
'unique' => 'The provided value is invalid', // ← error
],
]
'[invalid]' => [
'title' => 'test',
]
'[repository]' => 'Tags'
},
// …
// This submitted tag title does *not* already exist in Tags
(int) 3 => object(App\Model\Entity\Tag) id:4 {
'title' => 'tag'
'site_id' => (int) 2
'[new]' => true
'[accessible]' => [
'site_id' => true,
'title' => true,
'created' => true,
'modified' => true,
'site' => true,
'articles' => true,
]
'[dirty]' => [
'title' => true, // ← no error
'site_id' => true,
]
'[original]' => [
]
'[virtual]' => [
]
'[hasErrors]' => false
'[errors]' => [
]
'[invalid]' => [
]
'[repository]' => 'Tags'
},
]
How I expected it to work?
The behaviour I'm looking for is if a tag already exists, then take its ID and just link the submitted article entry to that existing ID. So ON DUPLICATE KEY clause, in a way.
Is there a flag that I'm missing that would tell/allow the ORM to do this, or should I maybe start trying some ->epilog() tricks?
There is no such functionality for the ORM saving process, no, and you cannot make use of epilog() with the default ORM save() process, you'd have to actually create the insert query manually then, however you cannot really use entities then, and it wouldn't solve the validation problem, and you'd have to more or less manually apply validation and application rules (you don't want to blindly insert data into insert queries, even tough Query::values() binds data).
I'd probably suggest to check if a solution that modifies the data before marshalling would be a good fit, that would integrate transparently into process. You could use your unique index columns to look up existing rows, and inject their primary key values into the request data, then the patching/marshalling process will be able to properly look up the existing records and update them accordingly.
Depending on the specific use case this could be more work than manually constructing insert queries, but it will IMHO integrate nicer. In your specific case it's probably easier, as using manual insert queries would require you to insert data for all the different tables separately, as you cannot make use of the ORM's association saving functionality with manually constructed insert queries.
To finish things off, here's some untested quick & dirty example code to illustrate the concept:
// in ArticlesTable
public function beforeMarshal(
\Cake\Event\EventInterface $event,
\ArrayAccess $data,
\ArrayObject $options
): void {
// extract lookup keys from request data
$keys = collection($data['tags'])
->extract(function ($row) {
return [
$row['tag'],
$row['site_id'],
];
})
->toArray();
// query possibly existing rows based on the extracted lookup keys
$query = $this->Tags
->find()
->select(['id', 'tag', 'site_id'])
->where(
new \Cake\Database\Expression\TupleComparison(
['tag', 'site_id'],
$keys,
['string', 'integer'],
'IN'
)
)
->disableHydration();
// create a map of lookup keys and primary keys from the queried rows
$map = $query
->all()
->combine(
function ($row) {
return $row['tag'] . ';' . $row['site_id'];
},
'id'
)
->toArray();
// inject primary keys based on whether lookup keys exist in the map
$data['tags'] = collection($data['tags'])
->map(function ($row) use ($map) {
$key = $row['tag'] . ';' . $row['site_id'];
if (isset($map[$key])) {
$row['id'] = $map[$key];
}
return $row;
})
->toArray();
}
With the primary keys of existing records injected, marshalling, validation, rules and saving should be able to properly distinguish what's to update and what's to insert, ie you should be able to continue using the default ORM saving process just like you're used to.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Query Builder > Inserting Data
Cookbook > Database Access & ORM > Table Objects > Lifecycle Callbacks > beforeMarshal
this function should return the field of the table I want, but this doesn't happen, return all field of the table, with simply sql work fine "SELECT DISTINCT especie FROM packages"
public function listSpicies()
{
$packages = $this->Packages->find('all')
->select('especie')
->distinct('especie');
$this->set([
'success' => true,
'data' => $packages,
'_serialize' => ['success', 'data']
]);
}
I think You can use something like this:
$packages = $this->Packages->find('all' , [
'fields' => [
'anyAlias' => 'DISTINCT(espiece)'
]
])
->toArray();
Notice. If this collection is serialized and outputted as a JSON, check \App\Model\Entity\Package - if espiece is inside $_hidden array - remove this from array
I am writing my first module and have created a fieldable entity. The only property of the entity in its schema is an 'id' which is type serial, unsigned and not null. However when I use entity_create (with no property values set) followed by $entity->save() the code fails with the following SQL error:
PDOException: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'entity_id' cannot be null: INSERT INTO {field_data_field_available} (entity_type, entity_id, revision_id, bundle, delta, language, field_available_value) VALUES (:db_insert_placeholder_0, :db_insert_placeholder_1, :db_insert_placeholder_2, :db_insert_placeholder_3, :db_insert_placeholder_4, :db_insert_placeholder_5, :db_insert_placeholder_6); Array ( [:db_insert_placeholder_0] => appointments_status [:db_insert_placeholder_1] => [:db_insert_placeholder_2] => [:db_insert_placeholder_3] => appointments_status [:db_insert_placeholder_4] => 0 [:db_insert_placeholder_5] => und [:db_insert_placeholder_6] => No ) in field_sql_storage_field_storage_write() (line 514 of C:\wamp\www\Drupal\modules\field\modules\field_sql_storage\field_sql_storage.module).
This implies to me that Drupal is trying to create entries in the 'field_data' table before it has created the entry in the entity table. I set no property values because I only have the 'id' property and surely that should be auto generated. If this was done first then it would have the id for creating the entry in the field_data table. The exact code I used is:
$entity_record = entity_create($entity_name,array());
$entity_record->save();
I hope someone has clues. The only work around I can see is to count the existing records, add one and use that as the new id but I can see loads of issues with this approach (clashing ids, performance etc).
Work around code:
$entities = entity_load('my entity type');
$entity_id = count($entities) + 1;
// Create the new entity object with this ID
$entity = entity_create('my entity type, array('id' => $entity_id));
// Save the new entity ready to reload after this if block of code
$entity->save();
My hook_entity_info is:
function appointments_entity_info() {
return array(
'appointments_status' => array(
'label' => t('Appointment status'),
'default label' => t('Appointment status'),
'plural label' => t('Appointment statii'),
'entity class' => 'Entity',
'controller class' => 'EntityAPIController',
'views controller class' => 'EntityDefaultViewsController',
'base table' => 'appointments_status',
'entity keys' => array(
'id' => 'id'
),
'fieldable' => TRUE,
// Create one default bundle of the same name
'bundles' => array(
'appointments_status' => array(
'label' => t('Appointment statii'),
),
),
// Use the default label() and uri() functions
'label callback' => 'entity_class_label',
'uri callback' => 'entity_class_uri',
'module' => 'appointments',
),
);
}
Thanks
Rory
Thanks very much to #Arkreaktor for pointing me in the right direction. I added a 'bundle' property to my schema - type varchar length 255. I made no changes to hook_entity_info to refer to this property but then changed my entity create code to:
$entity = entity_create($entity_type, 'bundle' = $entity_type);
$entity->save();
$entity_id = $entity->id;
// Re-load the new entity or loading the existing entity
$entity = entity_load_single($entity_type, $entity_id);
// Rebuild the entity object from the values on the form
entity_form_submit_build_entity($entity_type, $entity, $form, $form_state);
// Save updated entity
$entity->save();
And it worked!!!
I hope this helps others.
Rory
I want use connectionManager in piece of my project.
how can I access insert id by using connectionManager::insert
for example:
$connection = ConnectionManager::get('default');
$connection->insert('cities',
[
'name' => $city,
'country' => $country_code,
]
);
There is no need to do that, this will "just work" whether you create an explicit cities table class or not:
use Cake\ORM\TableRegistry;
$table = TableRegistry::get('cities');
$entity = $table->newEntity(
[
'name' => $city,
'country' => $country_code,
]
);
$table->save($entity);
$cityId = $entity->id;
But how?
Calling insert, returns a statement object:
$connection = ConnectionManager::get('default');
$statement = $connection->insert('cities',
[
'name' => $city,
'country' => $country_code,
]
);
Which can then be used to find the last insert id:
$cityId = $statement->lastInsertId('cities');
This is one of the things you don't need to worry about using the ORM as designed as the save method takes care of that automatically.
I am trying to give a condition in cakephp 3 get method, where data will fetch by foreign id not primary key. Here I have tried below code:
$eventPasswordAll = $this->EventPasswordAll->get($id, [
'conditions' => ['event_id'=>$id],
'contain' => ['Events']
]);
But it showing me data according to id(primary key), not by event_id. How I add this condition in get methods like where event_id=my get id ?
Don't use get, use find. According to CakePHP 3.0 Table API, the get method:
Returns a single record after finding it by its primary key, if no record is found this method throws an exception.
You need to use find:
$eventPasswordAll = $this->EventPasswordAll->find('all', [ // or 'first'
'conditions' => ['event_id' => $id],
'contain' => ['Events']
]);
// or
$eventPasswordAll = $this->EventPasswordAll->find()
->where(['event_id' => $id])
->contain(['Events']);
Sometimes you want to get the id and the userid..
$this->loadModel('Jobapplications');
$application = $this->Jobapplications->find('all', [
'conditions' => ['id' => $id, 'user_id' => $user_id]
]);
$application = $application->first();