data
data copied to clipboard
Implement easy N:N save
Hi there,
as I am using lots of MToM relations in my project, I thought a bit about a native atk4\data MToM implementation.
Ok, first lets get a good example: Students and Lessons. A Student can have many Lessons, a Lesson can have many Students.
The only solution for mapping this I know so far is to have an extra Model which stores the relations. Like:
class StudentToLesson extends \atk4\data\Model {
public function init() {
parent::init();
$this->addFields([
['lesson_id', 'type' => 'integer'],
['student_id', 'type' => 'integer'],
]);
}
}
What I am aiming for is to directly add and remove relations by passing either id or object. Passing id:
$student = new Student($app->db);
$student->load(3);
//adds a new StudentToLesson having student_id=3 and lesson_id=1
$student->addMToMRelation('Lesson', 1);
Passing Object:
$student = new Student($app->db);
$student->load(3);
$lesson = new Lesson($app->db);
$lesson->load(1);
//adds a new StudentToLesson having student_id=3 and lesson_id=1
$student->addMToMRelation($lesson);
And of course having the same for removal and checking if the relation exists
$student->removeMToMRelation('Lesson', 1);
$student->removeMToMRelation($lesson);
$student->hasMToMRelation('Lesson', 1);
$student->hasMToMRelation($lesson);
When traversing, we'd usually like the get the lessons of a student, not the StudentToLesson records. Like:
//iterates all Lessons of the student
foreach($student->ref('Lesson') as $lesson) {
}
So how could a MToM relation be defined in model?
What about (In Student's init()):
$this->hasManyToMany(['Lesson', new Lesson()], ['StudentToLesson', 'our_field' => 'student_id', 'their_field' => 'lesson_id']);
What do others think about this possible usage?
Old content, removed
I refactored the MToM functions I have yesterday. Idea is still the same, create 1 line functions for each model like:
//in Student
public function addLesson($lesson) {
return $this->_addMToMRelation($lesson, new StudentToLesson($this->persistence), 'Lesson', , 'student_id', 'lesson_id');
}
Implementation of MToMfunctions currently is:
/*
* function used to add data to the MtoM relations like GroupToTour,
* GuestToGroup etc.
* First checks if record does exist already, and only then adds new relation.
*/
protected function _addMToMRelation($object, \atk4\data\Model $mtom_object, string $object_class, string $our_field, string $their_field):bool {
//$this needs to be loaded to get ID
if(!$this->loaded()) {
throw new \atk4\data\Exception('$this needs to be loaded in '.__FUNCTION__);
}
$object = $this->_mToMLoadObject($object, $object_class);
//set values and conditions
$mtom_object->set($our_field, $this->get('id'));
$mtom_object->set($their_field, $object->get('id'));
//no reload neccessary after insert
$mtom_object->reload_after_save = false;
//if that record already exists mysql will throw an error if unique index is set, catch here
try {
$mtom_object->save();
return $mtom_object->loaded();
}
catch(\Exception $e) {
return false;
}
}
/*
* function used to remove a record the MtoM relations like GroupToTour,
* GuestToGroup etc.
*/
protected function _removeMToMRelation($object, \atk4\data\Model $mtom_object, string $object_class, string $our_field, string $their_field):bool {
//$this needs to be loaded to get ID
if(!$this->loaded()) {
throw new \atk4\data\Exception('$this needs to be loaded in '.__FUNCTION__);
}
$object = $this->_mToMLoadObject($object, $object_class);
$mtom_object->addCondition($our_field, $this->get('id'));
$mtom_object->addCondition($their_field, $object->get('id'));
//atk needs active record to be loaded to delete
$mtom_object->tryLoadAny();
if(!$mtom_object->loaded()) {
return false;
}
$mtom_object->delete();
return true;
}
/*
* checks if a MtoM reference to the given object exists or not
*
* @param object The object to check if its referenced with $this
* @param object The MToM Refence class, e.g. GroupToTour
*
* @return bool
*/
protected function _hasMToMRelation($object, \atk4\data\Model $mtom_model, string $object_class, string $our_field, string $their_field):bool {
if(!$this->loaded()) {
throw new \atk4\data\Exception('$this needs to be loaded in '.__FUNCTION__);
}
$object = $this->_mToMLoadObject($object, $object_class);
$mtom_model->addCondition($our_field, $this->get('id'));
$mtom_model->addCondition($their_field, $object->get('id'));
$mtom_model->tryLoadAny();
return $mtom_model->loaded();
}
/*
* helper function for MToMFunctions: Loads the object if only id is passed,
* else checks if object matches rules
*/
private function _mToMLoadObject($object, string $object_class) {
//if object is passed, extract id
if(is_object($object)) {
//check if passed object is of desired type
if(!$object instanceOf $object_class) {
throw new \atk4\data\Exception('Wrong class:'.(new \ReflectionClass($object))->getName().' was passed, '.$object_class.' was expected in '.__FUNCTION__);
}
}
//we need to have an Object to get table property
else {
$object_id = $object;
$object = new $object_class($this->persistence);
$object->tryLoad($object_id);
}
//make sure object is loaded
if(!$object->loaded()) {
throw new \atk4\data\Exception('Object could not be loaded in '.__FUNCTION__);
}
return $object;
}
I'm following this, keep this up, and together with some of my own ideas this should be implemented after I'm finished with Actions. Thanks!
needs some $this->get($this->id_field)
instead of $this->get('id')
definitely :)
how about $this->id
? That should be the same as $this->get($this->id_field)
.
in some persistences, there may NOT BE the id field accessible through get(). I don't want code to rely on get($this->id_field)
With entity design, we should cascade save
to all dirty models, and if delete
is called on a child model, delete it's owner.