Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement easy N:N save #393

Open
PhilippGrashoff opened this issue Feb 26, 2019 · 7 comments
Open

Implement easy N:N save #393

PhilippGrashoff opened this issue Feb 26, 2019 · 7 comments

Comments

@PhilippGrashoff
Copy link
Contributor

PhilippGrashoff commented Feb 26, 2019

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
Copy link
Contributor Author

PhilippGrashoff commented Feb 26, 2019

Old content, removed

@PhilippGrashoff
Copy link
Contributor Author

PhilippGrashoff commented Mar 19, 2019

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

@romaninsh
Copy link
Member

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!

@PhilippGrashoff
Copy link
Contributor Author

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

@DarkSide666
Copy link
Member

DarkSide666 commented Apr 8, 2019

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

@romaninsh
Copy link
Member

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)

@mvorisek mvorisek changed the title Implement native MToM Handling - Ideas Implement easy N:N save Aug 25, 2021
@mvorisek mvorisek added the MAJOR label May 29, 2022
@mvorisek
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants