CodeIgniter Forums
DataMapper 1.6.0 - Printable Version

+- CodeIgniter Forums (https://forum.codeigniter.com)
+-- Forum: Archived Discussions (https://forum.codeigniter.com/forumdisplay.php?fid=20)
+--- Forum: Archived Libraries & Helpers (https://forum.codeigniter.com/forumdisplay.php?fid=22)
+--- Thread: DataMapper 1.6.0 (/showthread.php?tid=11358)



DataMapper 1.6.0 - El Forum - 10-21-2008

[eluser]gusa[/eluser]
[quote author="GregX999" date="1222930624"]Wow dude, you rock! This is really looking very usable to me now - after these few changes.

Making the join tables use the model's name is perfect!

Greg[/quote]

totally agree with greg. thank you very much for listening to our suggestions!

by the way, i change datamapper.php to allow the objects to select the database group.

in datamapper.php add the following attribute:

Code:
var $db_group = '';

then, after the call to the parent constructor (parent::Model()), add these lines:

Code:
if (!empty($this->db_group)) {
    $this->db = $this->load->database($this->db_group, true);
}

so, if you want your object to access to another database (rather than default), just declare an attribute with the database name:

Code:
class Subject extends DataMapper {
    var $db_group = 'access_control_db';
    var $has_many = array("attribute" => "attributes");

    var $validation = array(
        'name' => array('required', 'trim', 'unique', 'min_length' => 3, 'max_length' => 20),
        'password' => array('required', 'trim', 'min_length' => 3, 'max_length' => 40, 'encrypt')
    );
    // ...



DataMapper 1.6.0 - El Forum - 10-21-2008

[eluser]OverZealous[/eluser]
I've been working on what I believe is a critical addition to DataMapper: a where_related method. It was actually really simple, although I have not yet thoroughly tested it.

If you already have a DataMapper subclass, you can add the following code to it:
Code:
/**
* Get
*
* Overridden to reset the _added_related array
*
*/
function get ($limit = NULL, $offset = NULL)
{
    $ret = parent::get($limit, $offset);
    // clear the _added_related array
    $this->_added_related = array();
    return $ret;
}


// --------------------------------------------------------------------

/**
* Where Related
*
* Limits a query to a related object's field.
*
* @access    public
* @param    object
* @param    string
* @param    string
* @return    this
*/
function where_related($object, $field = NULL, $value = NULL, $or = FALSE)
{
    if (is_null($object))
    {
        show_error('where_related requires a valid related Object');
    }
    if (is_null($object->id) && is_null($field))
    {
        show_error('where_related requires a field or object id');
    }
    
    if ($field == NULL)
    {
        $field = 'id';
        $value = $object->id;
    }
    $this->_add_related($object, $field, $value, $or);
    
    return $this;
}

// --------------------------------------------------------------------

/**
* Or Where Related
*
* Limits a query to a related object's field.
*
* @access    public
* @param    object
* @param    string
* @param    string
* @return    this
*/
function or_where_related($object, $field = NULL, $value = NULL) {
    return $this->where_related($object, $field, $value, TRUE);
}


// --------------------------------------------------------------------

// used to keep track of related items
var $_added_related = array();

/**
* Related
*
* Finds all related records of this objects current record.
*
* @access    private
* @param    string
* @param    integer
* @return    void
*/
function _add_related($object, $field, $value, $or)
{

    // Prepare model
    $model = ucfirst(strtolower($object->model));
    //$object = new $model;

    $this->model = strtolower($this->model);

    // Determine relationship table name
    $relationship_table = $this->_get_relationship_table($object->prefix, $object->table, $object->model);

    // Retrieve related records
    if (empty($this->db->ar_select))
    {
        $this->db->select($this->table . '.*');
    }

    // Check if self referencing
    if ($this->table == $object->table)
    {
        if ( ! in_array($model, $this->_added_related) )
        {
            // can only perform where on id's for self-referencing
            if($field != 'id') {
                show_error('Cannot perform where_related queries on self-referencing tables unless field is id');
            }
            $this->db->join($relationship_table, $object->table . '.id = ' . $this->model . '_id', 'left');
            $this->db->where($relationship_table . '.' . $object->model . '_id = ' . $value);
            array_push($this->_added_related, $model);
        }
    }
    else
    {
        // only add the table if it wasn't already joined
        if ( ! in_array($model, $this->_added_related) )
        {
            $this->db->join($relationship_table, $this->table . '.id = ' . $this->model . '_id', 'left');
            $this->db->join($object->table, $object->table . '.id = ' . $object->model . '_id', 'left');
            array_push($this->_added_related, $model);
        }
        if($or)
        {
            $this->db->or_where($object->table . '.' . $field, $value);
        }
        else
        {
            $this->db->where($object->table . '.' . $field, $value);
        }
    }

    $this->model = ucfirst($this->model);
}

Usage:
If you want to filter a query based on the values of a related object, you call where_related before calling the get.
Example:
Code:
// get all admins
$u = new User();
$access = new Access();
$u->where_related($access, 'level', 'admin');
$u->get();

You can also use it to filter on a specific item easily:
Code:
$u = new User();
$access = new Access();
$access->where('level', 'admin')->get();
$u->where_related($access);
$u->get();

Finally, you can use standard the where format:
Code:
$u = new User();
$access = new Access();
$u->where_related($access, 'level >=', 2);
$u->get();

Limitations
There is only one real limitation. A self-referencing table can ONLY query by ID. I could not think of an easy way around this.

You can have multiple where_related or or_where_related statements.

Let me know if there are any errors, or if you find transcription errors. I made some changes while typing this, so I might have made a typo.


DataMapper 1.6.0 - El Forum - 10-21-2008

[eluser]Boyz26[/eluser]
Not sure if it is just me, but if you create a table called Battles, and the model battle, it won't work.


DataMapper 1.6.0 - El Forum - 10-21-2008

[eluser]OverZealous[/eluser]
What do you mean?

Your tables MUST be called 'battles' and your model 'Battle' - that's DataMapper Spec. The majority of the code above was copied from DataMapper's _related() method, which is used to handle normal related lookups (it would be called with $u->access->get()).

Basically, I'm simply adding a standard join to the normal query flow, along with a where statement to make the join useful.

UPDATE: Hopefully I didn't sound rude there. I am actually curious if I messed something up. I haven't had any real problems with this yet, but if you do, I want to know. I think stensi was looking to add this into the core DataMapper model.


DataMapper 1.6.0 - El Forum - 10-21-2008

[eluser]OverZealous[/eluser]
A tip for those creating self-referencing tables:
You have to be a little creative when dealing with multiple-relationship, self-referencing tables. What I mean by that is:

Say you have a generic Employee model. Employee can have many Tasks. You also can have a Manager model. The Manager model has both many Tasks (inherited from Employee, or defined explicitly) and many Employees. Employees have one Manager.

The issue comes in with the Task <> Employee and Task <> Manager relationship. By default, DataMapper will look at the correct table, since both Manager and Employee share the 'employees' table. For both models, the relationship table is correctly identified as 'employees_tasks'. BUT, the id fields use the model's name. So, for Employees, the expected id field is 'employee_id', and for managers, it's 'manager_id'.

The solution is simple. Create your join table like this (note, Postgre, so alter accordingly):
[pre]
CREATE TABLE employees_tasks (
id serial NOT NULL,
employee_id integer,
manager_id integer,
task_id integer NOT NULL
);
[/pre]

You just have to add in a relationship column for both types. The default for the unused column should be null, so you end up only storing the correct data.

The biggest drawback is a possible error if you accidentally access a Manager through the Employee model, or vice-versa, and try to save a task. Then the task will be saved incorrectly. So be careful. When Stensi gets back, maybe he can look into this, and see if there is a smarter way to handle this situation.

Happy Code Igniting.


DataMapper 1.6.0 - El Forum - 10-22-2008

[eluser]ntheorist[/eluser]
@OverZealous - Cool stuff!.. it seems like we're both working similarly on enhancing datamapper to build more effective queries and allow for more relevant data to be available upon getting a datamap.

in my last post i tried creating a join_related function, although in only joined one field from a related item, so i have it joining every field now and inserting it into the related datamaps' object fields. Although i believe very much in using aliasing, so that where clauses don't need to explicitly specify a table, and also to prevent name collisions.

before, i had it set up so it needed to be called explicitly in the controller, but now i have it called in the get() method automatically..

Code:
// in method get()..

$this->total_count = $this->db->count_all($this->table);

// Clear this object to make way for new data
$this->_clear(TRUE);

if( ! $this->table_joined)
{
    $related = array_merge($this->has_one, $this->has_many);
    $this->join_related($related);
}

// Get by built up query
$query = $this->db->get($this->table, $limit, $offset);

..and have it auto-assign the values to each related object (db values for has_one, count for has_many)...

Code:
if ($query->num_rows() > 0)
{
    $this->record_count = $query->num_rows();
    // Populate all with records as objects
    //$this->all = $this->_to_object($query->result(), $this->model, $this->fields);
    $this->all = $this->_to_object($query->result_array(), $this->model, $this->fields);
    
    $row = $query->row_array();
    
    foreach($this->has_one as $related)
    {
        if(is_object($this->{$related}))
        {
            $fields = $this->{$related}->fields;
            foreach($fields as $field)
            {    
                $lookup = $related.'_'.$field;
                if(array_key_exists($lookup, $row))
                {    
                    $this->{$related}->{$field} = $row[$lookup];
                }
            }
        }
    }
    
    foreach($this->has_many as $related)
    {
        if(is_object($this->{$related}))
        {
            $lookup = $related.'_count';
            if(array_key_exists($lookup, $row) && ! empty($row[$lookup]))
            {
                $this->{$related}->record_count = $row[$lookup];
            }
            else
            {
                $this->{$related}->record_count = 0;
            }
        }
    }
            
    
    foreach($this->fields as $field)
    {
        if(array_key_exists($field, $row))
        {
            $this->{$field} = $row[$field];
        }
    }
}

so now the query is compressed into one, and all related objects' fields are populated as if they had get() called on them explicitly.

I also have two additional bits of data, total_count and result_count, auto populated in the datamap. so $user->total_count returns a count of all the users in its table, and $user->result_count returns the number of rows returned in the query..

There was one bump, however, and that was in order for all the records of the datamap to have their related objects populated in the ->all array, i similarly adjusted the _to_object method..

Code:
function _to_object($result_array, $model, $fields)
{
    $items = array();
    
    foreach ($result_array as $row_array)
    {
        $item = new $model;
        
        foreach($item->has_one as $related)
        {
            if(is_object($item->{$related}))
            {
                $related_fields = $item->{$related}->fields;
                
                foreach($related_fields as $related_field)
                {    
                    $lookup = $related.'_'.$related_field;
                    
                    if(array_key_exists($lookup, $row_array))
                    {    
                        $item->{$related}->{$related_field} = $row_array[$lookup];
                    }
                    else
                    {
                        $item->{$related}->{$related_field} = NULL;
                    }
                
                }
            }
        }
        
        foreach($item->has_many as $related)
        {
            if(is_object($item->{$related}))
            {
                $lookup = $related.'_count';
                if(array_key_exists($lookup, $row_array) && ! empty($row_array[$lookup]))
                {
                    $item->{$related}->result_count = $row_array[$lookup];
                }
                else
                {
                    $item->{$related}->result_count = 0;
                }
            }
        }
        
        foreach($fields as $field)
        {
            if(array_key_exists($field, $row_array))
            {
                $item->{$field} = $row_array[$field];
            }
            else
            {
                $item->{$field} = NULL;
            }
        }
            
        array_push($items, $item);
    }

    return $items;
}

so, now it works with the efficient SQL, but with the same kind of ease of use originally designed in DataMapper..

Code:
//controller users

$user = new User();
$user->get();

// multiple users, one query

foreach($user->all as $u)
{
     echo $u->username .' is a '.$u->userclass->name.' with a rank of '.$u->userclass->rank.' and belongs to '.$u->usergroup->name . br();
     if($u->document->result_count > 0)
     {
          echo $u->username .' also has '.$u->document->result_count.' documents.'.br();
     }
}

still have to edit the _to_array method to spit out a multi-dimensional array containing the related information

as far as where searches.. i built up a prototype of where_related and i'm still working on that.. also for 'many' searches i want to create a where_related_count()

the other AR methods will also need attention (sort by alias, etc).. but i wont be able to do an entire overhaul of DM until i have time..

CC


DataMapper 1.6.0 - El Forum - 10-23-2008

[eluser]stensi[/eluser]
Wow, lots of great suggestions since I`ve been away! Thanks OverZealous.com for providing support in my absence. I, as well as everyone else I`m sure, really appreciate it! :-) I`m not back yet though as I`m still in Japan (and will be for another 25 or so days). Finally found time in a nearby Internet Cafe to do some general catching up. Once I`m home, I`ll have a full look at what`s been posted. I like your idea of the HTML Viewer Phil. Don`t have time right now to read up on the relationship codes but looking forward to seeing how you guys are extending DM for what you need.

It seems my host must have had a meltdown at some stage and reverted my site to a backed up copy of an old version... I don`t have access to my files from here to put the latest back up. I`ll be changing over to SliceHost.com when I get back. My current host is sooo unreliable...

Anyway, that`s all from me! My wife and I are off to dinner :-)


DataMapper 1.6.0 - El Forum - 10-23-2008

[eluser]dexcell[/eluser]
hi, i would like to ask question about datamapper.

1. for example i have 3 tables which is users, entries, languages.

users table have 2 field = (id, username)
languages table have 2 field (id, language)

normally, when i create entries table i include the user_id and language_id in it as foreign key, so the entries table will have 4 field = (id, user_id(FK), language_id(FK), content)

question:
if i'm gonna use datamapper, so i have to only create 2 field in entries table which is (id, content), then create join table named entries_users and have their id in table fields, also create entries_languages table and have their id in table fields.

is this correct?

2. for example, i have 2 table which is users and friends.

normally in my mind i will create table like this

users (id, username)

friends (user_id, friend_user_id, mute)
note: friend_user_id is also from user_id, mute is for example, i don't want this friend_user_id message showup.

can you suggest how should i build the table if i want to use datamapper

thank you Smile


DataMapper 1.6.0 - El Forum - 10-23-2008

[eluser]OverZealous[/eluser]
Yes, you have the correct idea for example 1 - you need a dedicated table with [id] for data, and a dedicated table for each relationship. There, currently, is no way to store or retrieve data stored in a relationship table.

If you do this, you almost certainly will need to see my extension of DataMapper above, which allows you to add relationships to queries (where_related). It looks like commandercool is working on a fix for the relationship tables, but I'm not sure how easy it is to implement that yet.

Also, you might want to create an in-between subclass of DataMapper so you can add tweaks to all of your models. If you look back up the thread, there are examples of how to do this. (Basically, I created a class called DataMapperExt that extends DataMapper. Then all your models subclass DataMapperExt - you can name it anything you want.)

For example 2:
It gets trickier when doing self-referencing tables, but not too difficult. There are good examples of this in the DataMapper documentation. The process is:

Create the User model like normal.
Create a Friend model. This model should subclass User, and explicitly set $table to "users".
Add "user" to Friend::$has_many in the Friend constructor
Add "friend" to User::$has_many in the User constructor
Create a table called friends_users, with the columns (id, friend_id, user_id).

Now you can get the friends of a user through $user->friends->get. Note, however, the Friend models will not be able to dig deeper without being converted into Users. There isn't an easy solution to this, yet.

Good Luck!


DataMapper 1.6.0 - El Forum - 10-23-2008

[eluser]Boyz26[/eluser]
I just updated to CI 1.7, but I think datamapper isn't working well with it:

Code:
A Database Error Occurred
Error Number: 1054

Unknown column 'archers.*' in 'field list'

SELECT `archers`.`*` FROM (`archers`) LEFT JOIN `z_archers_players` ON `archers`.`id` = `archer_id` LEFT JOIN `players` ON `players`.`id` = `player_id` WHERE `archers`.`display_name` = 'Ettst' AND `players`.`id` = 8

Does anyone else have the same problem?

Thanks!

UPDATE:
I added ->select('id, display_name, etc') to the function and it worked. Is there a way so that i don't need to select all the columns everytime? Thanks again!