Welcome Guest, Not a member yet? Register   Sign In
DataMapper 1.6.0
#41

[eluser]GregX999[/eluser]
Yes, that's what I was trying to get at when I made the restaurant/menu examples.

Code:
$r->items->where('name', 'chicken')->get();

However, this makes it seem like you're after all the items with chicken that the restaurant "$r" has. (The "where" is acting on the items of a particular restaurant.)

Perhaps something like this to get all the restaurants that have an item with "chicken":

Code:
$r->have('items', array('name'=>'chicken'))->get();

("have" or "has" - would it matter?)

Both of the examples above would be quite useful.

Greg
#42

[eluser]stensi[/eluser]
Good suggestion. This is something I noticed along the way but haven't had time to look at much.

The solution I'm favouring is to have the related object start off completely empty on first access and you'd have to populate it much like you do the normal objects.

For example, let's say we have an authors table with a One to Many relationship between the books table. If we wanted to see what books an author has made:

Code:
// Get author
$a = new Author();
$a->where('name', $name)->get();

// If you wanted to access all books by this author, you'd now do it like this
$a->book->get();

foreach ($a->book->all as $b)
{
    echo $b->name . '<br />';
}

// Or you could do it like this if you wanted to access a specific genre of books
$a->book->where('genre', $genre)->get();

foreach ($a->book->all as $b)
{
    echo $b->name . '<br />';
}

So yeah, the related objects work like any DataMapper object, except its queries will always relate to its parent object.

Thoughts?

UPDATE

I've got the above way working properly now. I've also changed the get() method to be chainable, so you can do it this way if you want:

Code:
// Get author
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->where('genre', $genre)->get()->all as $b)
{
    echo $b->name . '<br />';
}
#43

[eluser]GregX999[/eluser]
That's pretty slick.

Would this work to get a list of bookstores that sell an author's books?

Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->bookstore->where('city', $city)->get()->all as $bs)
{
    echo $bs->name . '<br />';
}

And can I get a list of an author's books available in a certain city like this?

Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->bookstore->where('city', $city)->book->where('author', $a)->get()->all as $b)
{
    echo $b->name . '<br />';
}

(I swear, I'm not trying to be a smart-ass!!)

Greg
#44

[eluser]stensi[/eluser]
No problem :-) Happy to have your questions since they've helped to tighten up and improve DataMapper.

For the above question, unfortunately no, those wont work as is.

Since related objects will be empty from now on, you'll need to do a get() to populate it with at least one record before going into a deeper related object (otherwise there's no parent record for the deeper related object to know what to relate to).

So in your first snippet of code, you need a get() after the book, for example, I'll limit to getting 10 books:

Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->get(10)->bookstore->where('city', $city)->get()->all as $bs)
{
    echo $bs->name . '<br />';
}

Now, the above would only look at the bookstores for the first book, since there's nothing above to tell us to loop through all the books, and look at all the bookstores for each of those books. That's an important thing to take note of. There's no simple way I know of that would allow you to foreach through 1st level related objects and 2nd level (or more) related objects in the one foreach. I don't think that's do-able.

Also note that related objects are only aware of their parent object and no higher (they're not aware of their parents parent object etc).

If you wanted to look at all books the author has, and all the bookstores they're in, you'd have to do:

Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->get()->all) as $b)
{
    foreach ($b->bookstore->where('city', $city)->get()->all as $bs)
    {
        echo $bs->name . '<br />';
    }
}

or you could do it this way:

Code:
$a = new Author();
$a->where('name', $name)->get();

$a->book->get();

foreach ($a->book->all) as $b)
{
    $b->bookstore->where('city', $city)->get();

    foreach ($b->bookstore->all as $bs)
    {
        echo $bs->name . '<br />';
    }
}

Onto your second snippet, that wont work for the reasons stated above, but also, because you're missing a get() call after your first where call and you're using the original author object as part of a where clause (not possible - you would need to pass the authors name instead).

Am I right in seeing you're almost doing a full relationship loop, from the author through his books, the bookstores they belong to, and then the books in the bookstores that belong to the author?

Not that you'd ever really want to do this Wink but here's how it can be done:

Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->book->get()->all as $b)
{
    foreach ($b->bookstore->where('city', $city)->get()->all as $bs)
    {
        foreach ($bs->book->where('author', $a->name)->get()->all as $book)
        {
            echo $book->name . '<br />';
        }
    }
}

or

Code:
$a = new Author();
$a->where('name', $name)->get();

$a->book->get();

foreach ($a->book->all as $b)
{
    $b->bookstore->where('city', $city)->get();

    foreach ($b->bookstore->all as $bs)
    {
        $bs->book->where('author', $a->name)->get();

        foreach ($bs->book->all as $book)
        {
            echo $book->name . '<br />';
        }
    }
}
#45

[eluser]GregX999[/eluser]
Could you get rid of the requirement to call "get" on a relationship by checking to see if that relationship has been populated, and if not, by populating it?

So you could do this:
Code:
$a = new Author();
$a->where('name', $name)->get();

foreach ($a->books as $b){
...
}

Is there a way that $a could know that "books" is empty so it should perform a "get" on it. Or maybe if $a just knows it "has many" books it can just always do the "get" if that function already handles caching.

Then I would think you COULD chain relationships (as long as each relationship was a "has one" - except for the last on in the chain which could be anything):
Code:
// When viewing an ad, gets other ads posted by the same user:
$a = new Ad();
$a->where('item', $item)->get();

foreach ($a->user->ads as $ua){
...
}

Also, can you make custom methods that return an object then use them in a chain?
Code:
// In the Author class:
function most_popular_book()
{
return $b // $b is an object of class Book
}

// In a controller or view:
$a = new Author;
echo $a->most_popular_book->publish_date;

Greg
#46

[eluser]stensi[/eluser]
I can change back to the default behaviour in the currently released version, that it auto-populates the related objects if they're not already populated. If already, populated it returns the existing data in it.

It's going to be very difficult to allow both ways though. The reason I was changing it so you have to manually populate it was so it wouldn't automatically fill with all records. The best I think I can do is limit the auto-populate some how, maybe with just 1 record? ...

If I get it working that way, it'll make it easier for me in terms of the documentation Smile since the existing code will still work. The new version on its way will then basically just allow you to fine tune your related objects population if desired, otherwise they work like they currently do.

I don't think I'll be able to chain custom objects like your last snippet there. I'll have a look into it though, but I expect that's going to be a bit tricky to pull off.

For now, the stuff I'm finishing off in order are:

- Better loading/handling of Related Objects.
- Allow validation of non-Database Table fields and allow custom field labels for all error messages.
- Manual Transactions for InnoDB or BDB Database Tables (if I have time, give the option to automate the transactions).
#47

[eluser]GregX999[/eluser]
The "->all->get()" works well enough for me. I was just thinking of have the "->all->get()" be the default behavior for a relation that has yet to be populated.

So that:
$a->book

Would be the same as:
$a->book->all->get();

But you could still do stuff like:
$a->book->where('pages > 100')->get();
or
$a->books;

But that's not important. I just thought it might be a neat thing.

Which reminds me of something I forgot earlier... It would be nice if you could name relationships so that you could use $a->books instead of $a->book. Or if you had a Classified_Ad model you could use $member->ads instead of $member->classified_ad.

Oh, and so models can reference themselves. For example, a "Employee" could have one "Supervisor" and have many "Underlings", both of which are also Employees.

Greg
#48

[eluser]stensi[/eluser]
Just for clarification, it's the other way round (related_object->get()->all) if you want to populate then access the all array.

The reason I was changing it to not auto-populate by default, was because of memory concerns, such as it auto-populating with 100,000 records when the developer most likely wont need that many, so forcing them to choose to get all or narrow it down is better for them. Still, if they don't populate it, I'll see if I can default it to populate with all, when you try and access a property. Can't make any promises that I'll be able to pull that off. Not having much luck so far Undecided

UPDATE
Still not having luck with the auto-populate if not populated thing. Mainly because of the chaining, since when a __get kicks off from when a related object is accessed, I have no way of telling if the next part of the chain is ->all, which if its empty I should auto-populate, or if it's a ->where or ->get() etc, which means an auto-populate is not needed since the developer is doing one.

I might have to just add a boolean setting that all related objects are auto-populated on first access if set to TRUE. If FALSE, you have to manually populate it yourself (better performance wise if you've got lots of records and don't want them all loaded). I'll be defaulting to FALSE.
______________________

Sorry but I wont be changing it to allow different names for the relationships. The reason it's the singular is because accessing $a->book is accessing the first book object and $a->book->all is an array of all the book objects returned. This is how it should be for the DataMapper pattern, and keeping things similar to the usage shown at DataMapper.org.

Code:
// Get first author object
$a = new Author();
$a->get(1);

// Populate related object with a maximum of 10 objects
$a->book->get(10);

// Show properties of the first related book
echo $a->book->id . '<br />';
echo $a->book->name . '<br />';
echo $a->book->description . '<br />';

// Show properties of all related books
foreach ($a->book->all as $b)
{
    echo $b->id . '<br />';
    echo $b->name . '<br />';
    echo $b->description . '<br />';    
}

With the self referencing thing, I haven't tried that yet but my first thought would be that you'd have to have a Model for employees who have a One to Many relationship with other employees (supervisor has multiple employees to supervise), and a Model for employees who have a One to One with other employees (employees have one supervisor).

This would require you have a joining table of "employees_employees".

Supervisor Model
Code:
&lt;?php

class Supervisor extends DataMapper
{
    var $table = "employees";

    var $has_many = array("underling" => "employees");

    function Supervisor()
    {
        parent::DataMapper();
    }
}

Underling Model
Code:
&lt;?php

class Underling extends DataMapper
{
    var $table = "employees";

    var $has_one = array("supervisor" => "employees");

    function Underling()
    {
        parent::DataMapper();
    }
}

I haven't tested that but I'm hoping it will work, lol :-) Probably won't since I suspect it will be looking for the same join key of (employee_id) and which you obviously can't have two of.

UPDATE

Version 1.3 has been released!

View the Change Log to see what's changed.

In short, the related objects has had an overhaul and automatic population of the related objects is now off by default (you now populate them in much the same way you do the normal objects) but can be turned back on easily if desired. Validation has been improved, with the ability to properly label fields for use in error messages and you can now validate non-Database Table fields.
#49

[eluser]GregX999[/eluser]
Great work on getting 1.3 out Stensi!

If "self-relational" joins they wouldn't work due to both "id" fields being named the same, I think that's a big issue. I think they are an important thing to be able to use (I certainly use them in many projects - such as assigning "related products" to products in an e-commerce site).

Also,
Following the supervisor/underling example, what would you do for an employee that has both a supervisor and underlings?
Or in the case of a social networking site, how would you handle having "friends" (since each friend needs to point to the other)?

Maybe the reference "id" names in the join table can be named after the model instead of the database table - so you'd have supervisor_id and underling_id instead of two employee_id fields??



You explanation for wanting to use "book" (and "book->all") instead of "books" makes total sense.

Greg
#50

[eluser]stensi[/eluser]
Thanks Greg :-)

Actually, now that you mention it you're right! It does use the model names to figure out the id fields in the joining tables. So the tables for the above self referencing example would be:

employees
id
name

employees_employees
id
supervisor_id
underling_id

The only problem, now that I've tested it, is that the JOIN query fails with the below error due to the JOIN having the employees table on both sides:

Code:
Not unique table/alias: 'employees'

SELECT employees.* FROM (`employees`) LEFT JOIN `employees_employees` ON employees.id = underling_id LEFT JOIN `employees` ON employees.id = supervisor_id WHERE employees.id = 1

I'll see if I can figure this out. Hopefully it's just a matter of having a different JOIN query if it looks like it's going to be a self reference. If I get this working, I'll look at getting it to work for that 3rd type of employee scenario you mentioned.

UPDATE

Yep! It just needed the different type of JOIN query for a self reference :-)

I'll post the fixed version for that sometime today.

I'm testing out the 3rd type of employee and it's looking like I might need to change things around so it uses the pluralised version of the model for the table name, rather than just the table name, for joining tables. So the joining table between Supervisors and Underlings would change from employees_employees to become:

supervisors_underlings
id
supervisor_id
underling_id

This would make it easier when you have more than two types of employees self referencing, such as a Manager, who would then have a joining table for each type of employee it could join with, example:


managers_underlings
id
manager_id
underling_id

managers_supervisors
id
manager_id
supervisor_id

If I go down this route, I can remove the need for developers having to specify the $table value for any of their models or in the $has_many and $has_one arrays, and I'll just make DataMapper use CodeIgniter's Inflector helper for automatically figuring out ALL of the singular/plural work.

It's smart enough to know that "Countries" is the plural of "Country" but unfortunately it's not smart enough to know "People" is the plural of "Person", and in those cases, this one particularly, you'd have to have "Person" as your Model and "Persons" as your Table.




Theme © iAndrew 2016 - Forum software by © MyBB