Welcome Guest, Not a member yet? Register   Sign In
MVC, MySQL Transactions, and referencing a model from within a model
#1

[eluser]pbarney[/eluser]
Guys, I'm stuck. I'm fairly new to CI (about a month), and there's much I don't understand. I seek guidance.

I've got four tables that are all interrelated (this is a very normalized database):

users (user login & password info)
contacts (contact information i.e., contact & mailing information)
clients (client-specific data)
companies (company information)

These tables are all related with foreign key constraints. For example, a user will have associated contact information, company information, and client information. I've got models for each of the tables.

Consequently, when I create a new member, there are updates to all tables, and I use a member_model that uses MySQL transactions for that.

The problem is, apparently you can't load a model within a model. It gives me an "undefined property" error.

But my question is not so much how one loads a model within a model, but rather, what is the best way to do database transactions in CI (which necessitates using multiple models), while still keeping the transactional code in the model (where it should be).

I know an easy answer is to move everything up to the controller, but then the controller would be assuming a role that is intended for a model. I believe in smart models, thin controllers, and dumb views, so I would like to keep this strictly MVC if possible.

For all I know, my approach may be flawed, as I'm still learning OOP concepts.

How would you approach this?

Here is some stripped-down code for illustration purposes:

members.php (controller)
Code:
<?php
class Members extends Controller
{
    
    function __construct()
    {
        parent::Controller();    
    }
    
    function create_member()
    {
        // $member_data contains fieldsets for all four tables
        $member_data = {magically appears after validation, scrubbing, etc}
        
        // load our members model
        $this->load->model('members_model');
        
        // create the member
        try {
            $member_data = $this->members_model->create($member_data);
            }
        catch(Exception $e)
        {
            echo $e->getMessage();
        }
    }
}
?>

members_model.php
Code:
<?php
class Members_model extends Model
{

    function __construct()
    {
        parent::Model();
        
    }

    function create($member_data)
    {
        // load necessary models
        $this->load->model('companies_model');
        $this->load->model('clients_model');
        $this->load->model('contacts_model');
        $this->load->model('users_model');
        
        // start a transaction to update all tables at once.
        // This is a manual transaction, so we must rollback on our own upon failure.
        $this->db->trans_begin();
        
        // first, add the company record and get the record id
        if ( ! $company_id = $this->companies_model->create($member_data['company']))
        {
            $this->db->trans_rollback();
            throw new Exception('Unable to add the company record.');
        }
        
        // create the client record and get the record id
        if ( ! $client_id = $this->client_model->create($member_data['client']))
        {
            $this->db->trans_rollback();
            throw new Exception('Unable to add the client record.');
        }
        
        // create the contact record and get the record id
        if ( ! $contact_id = $this->contacts_model->create($member_data['contact']))
        {
            $this->db->trans_rollback();
            throw new Exception('Unable to add the contact record.');
        }
        
        if (! $user_id = $this->users_model->create($member_data['user']))
        {
            $this->db->trans_rollback();
            throw new Exception('Unable to add the user record.');
        }
        
        // All data is submitted. Let's commit the transaction
        if ($this->db->trans_status() === FALSE)
        {
            // problem with the transaction
            $this->db->trans_rollback();
            throw new Exception('Unable to add new member data.');
        }
        else
        {
            // everything is good, commit the transaction and return the results
            $this->db->trans_commit();

            // return new id's along with original member_data record
            $member_data['company_id'] = $company_id;
            $member_data['client_id'] = $client_id;
            $member_data['contact_id'] = $contact_id;
            $member_data['user_id'] = $user_id;
            return $member_data;
        }
    }
}
#2

[eluser]mddd[/eluser]
When you load a model, it inherits all the libraries and models that were loaded before it.
That's how you can you use $this->db or $this->uri in your model: it was loaded in the controller and then distributed to the model when the model was loaded.

You can manually call this mechanism by calling $this->_assign_libraries in a model. The model will be populated with every property from the main CodeIgniter object.

So, using this code should solve the "undefined property" problem:
Code:
$this->load->model('companies_model');
$this->load->model('clients_model');
$this->load->model('contacts_model');
$this->load->model('users_model');

$this->_assign_libraries(); // this line added

Another way would be to load the 'dependent' models in the controller.
That way they are already known to CI and when you load the 'main' model it will have references the others.
In short: if model A needs a reference to model B, load B first and then A.

This whole situation is a bit weird. I'm not sure if it is solved in CI 2.0, maybe someone with more knowledge about 2.0 can tell us?
#3

[eluser]WanWizard[/eluser]
You can load a model in a model, but if the 'this' object is not assigned, some tickery might be needed:

Code:
$CI =& get_instance();
$CI->load->model('my_model');
$this->my_model =& $CI->my_model;

Just to understand your question:
Your transation problem (as far as I understand it) is that you want to encapsulate methods in multiple models in a single transaction, and your run into the issue that if one model calls a method in another, where to enable the transation (as the model doesn't know where it is called from)?

NOTE: @mddd's solution is a better one...
#4

[eluser]WanWizard[/eluser]
[quote author="mddd" date="1282229498"]This whole situation is a bit weird. I'm not sure if it is solved in CI 2.0, maybe someone with more knowledge about 2.0 can tell us?[/quote]
Some problem exists in 2.0.
#5

[eluser]mddd[/eluser]
Looking into the Loader class a bit, there is a method _ci_assign_to_models which is called when loading a library or database. When the library or database is loaded, it is 'distributed' to all models. I wonder why the same is not done when a model is loaded. That would solve this problem and make every model available from all other models.
#6

[eluser]WanWizard[/eluser]
I think this is because of the concept CI uses, which is that a model contains methods to deal with logical data handling tasks.

Models in CI do not have a one-2-one link with a table, a method just accesses all tables needed to do a task directly. In this concept there is no need for models loading other models.

In this concept, if you have a parent-child set of tables, you can have a parent_model method that retrieves a parent and all it's children, and a child_model that retrieves a parent from a given child. That could offcourse mean you could have redundant code, as for that last method it would be logical to call the parent_model.

You could extend the Loader class:
Code:
class MY_Loader extends CI_Loader
{
    function MY_Loader()
    {
        parent::CI_Loader();
    }

    function model($model, $name = '', $db_conn = FALSE)
    {
        // call the parent method
        parent::model($model, $name, $db_conn);

        // maks sure loaded models are updated
        $this->_ci_assign_to_models();
    }
}
#7

[eluser]mddd[/eluser]
Agreed, I don't do 1-to-1 mapping between tables and models myself, and that is not what CI is about. CI isn't a complete ORM in that sense (and I don't mind).

Also, there is something so say for the idea that a model should be independent from other models. So that all models "report to the controller", as it were. I think this is a bit of a grey area.

But still I sometimes have models that are more "supportive" of the others. In those cases it would be nice to have the 'auto distribution' better arranged for models too. I think I'll make some tweaks to the Loader class and see where it takes me Smile
#8

[eluser]pbarney[/eluser]
[quote author="WanWizard" date="1282231451"]Models in CI do not have a one-2-one link with a table, a method just accesses all tables needed to do a task directly. In this concept there is no need for models loading other models.[/quote]

[quote author="mddd" date="1282231917"]Agreed, I don't do 1-to-1 mapping between tables and models myself, and that is not what CI is about. CI isn't a complete ORM in that sense[/quote]

Okay, this is good to know. It seems that I've been learning a little too much OOP, and applying it too strictly to CI. So how then, would you explain the model as it relates to CI? Is it just a database abstraction layer for a specific controller?

If I've got a "companies" model that handles all of the CRUD for the `companies` table, should I duplicate that code in another model that is doing transactional work that affects the companies table?

OR, would I be better served by following mddd's excellent reply up above?
#9

[eluser]mddd[/eluser]
I don't link models to controllers or to database tables. I try to think of 'what blocks of information are there in my site?'.

In a typical site, you could have users (who have certain access rights that may be stored in a linked table), posts (with comments, stored in another table), images associated with posts, etcetera. You might build 2 models for that: user_model and blog_model. The user model would deal with everything about users. It would have methods like get_all_users (to give a list for an admin), get_user (to get 1 user's details), add_user, set_rights, etc. The blog_model would have methods like get_recent_posts, get_post_details (loading the post, its comments and its images), add_comment, etc.

I hope this makes sense to you. The idea is that a model could be called from many controllers that have some relationship with the 'subject' of the model. And likewise, a model might interact with a number of tables. As long as it's a logical 'piece' in the puzzle. If you had a very complex user-rights system you might break up the users and rights into two models to keep them manageble.

The bottom line is: it's up to you to decide. Whatever works for you. But CI is not really equipped (out of the box, anyway) to use models as representing individual data objects/tables. If you do like that kind of structure, you should look at Datamapper (DMZ). It is built for that!
#10

[eluser]pbarney[/eluser]
[quote author="mddd" date="1282229498"]You can manually call this mechanism by calling $this->_assign_libraries in a model. The model will be populated with every property from the main CodeIgniter object.[/quote]

Do you know if this uses a lot of extra memory?




Theme © iAndrew 2016 - Forum software by © MyBB