data icon indicating copy to clipboard operation
data copied to clipboard

Implement easy N:N save

Open PhilippGrashoff opened this issue 6 years ago • 7 comments

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?

PhilippGrashoff avatar Feb 26 '19 14:02 PhilippGrashoff

Old content, removed

PhilippGrashoff avatar Feb 26 '19 14:02 PhilippGrashoff

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;
    }

PhilippGrashoff avatar Mar 19 '19 18:03 PhilippGrashoff

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!

romaninsh avatar Mar 20 '19 17:03 romaninsh

needs some $this->get($this->id_field) instead of $this->get('id') definitely :)

PhilippGrashoff avatar Mar 20 '19 19:03 PhilippGrashoff

how about $this->id ? That should be the same as $this->get($this->id_field).

DarkSide666 avatar Apr 08 '19 20:04 DarkSide666

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)

romaninsh avatar May 07 '19 22:05 romaninsh

With entity design, we should cascade save to all dirty models, and if delete is called on a child model, delete it's owner.

mvorisek avatar May 29 '22 11:05 mvorisek