One to many relation

To illustrate how to implement one to many relations with the DAL, define another table “thing” that refers to the table “person” which we redefine here:

  1. >>> db.define_table('person',
  2. ... Field('name'))
  3. <Table person (id, name)>
  4. >>> db.person.insert(name='Alex')
  5. 1
  6. >>> db.person.insert(name='Bob')
  7. 2
  8. >>> db.person.insert(name='Carl')
  9. 3
  10. >>> db.define_table('thing',
  11. ... Field('name'),
  12. ... Field('owner_id', 'reference person'))
  13. <Table thing (id, name, owner_id)>

Table “thing” has two fields, the name of the thing and the owner of the thing. The “owner_id” field is a reference field, it is intended that the field reference the other table by its id. A reference type can be specified in two equivalent ways, either:

  1. Field('owner_id', 'reference person')

or:

  1. Field('owner_id', db.person)

The latter is always converted to the former. They are equivalent except in the case of lazy tables, self references or other types of cyclic references where the former notation is the only allowed notation.

Now, insert three things, two owned by Alex and one by Bob:

  1. >>> db.thing.insert(name='Boat', owner_id=1)
  2. 1
  3. >>> db.thing.insert(name='Chair', owner_id=1)
  4. 2
  5. >>> db.thing.insert(name='Shoes', owner_id=2)
  6. 3

You can select as you did for any other table:

  1. >>> for row in db(db.thing.owner_id == 1).select():
  2. ... print row.name
  3. ...
  4. Boat
  5. Chair

Because a thing has a reference to a person, a person can have many things, so a record of table person now acquires a new attribute thing, which is a Set, that defines the things of that person. This allows looping over all persons and fetching their things easily:

  1. >>> for person in db().select(db.person.ALL):
  2. ... print person.name
  3. ... for thing in person.thing.select():
  4. ... print ' ', thing.name
  5. ...
  6. Alex
  7. Boat
  8. Chair
  9. Bob
  10. Shoes
  11. Carl

Inner joins

Another way to achieve a similar result is by using a join, specifically an INNER JOIN. web2py performs joins automatically and transparently when the query links two or more tables as in the following example:

  1. >>> rows = db(db.person.id == db.thing.owner_id).select()
  2. >>> for row in rows:
  3. ... print row.person.name, 'has', row.thing.name
  4. ...
  5. Alex has Boat
  6. Alex has Chair
  7. Bob has Shoes

Observe that web2py did a join, so the rows now contain two records, one from each table, linked together. Because the two records may have fields with conflicting names, you need to specify the table when extracting a field value from a row. This means that while before you could do:

  1. row.name

and it was obvious whether this was the name of a person or a thing, in the result of a join you have to be more explicit and say:

  1. row.person.name

or:

  1. row.thing.name

There is an alternative syntax for INNER JOINS:

  1. >>> rows = db(db.person).select(join=db.thing.on(db.person.id == db.thing.owner_id))
  2. >>> for row in rows:
  3. ... print row.person.name, 'has', row.thing.name
  4. ...
  5. Alex has Boat
  6. Alex has Chair
  7. Bob has Shoes

While the output is the same, the generated SQL in the two cases can be different. The latter syntax removes possible ambiguities when the same table is joined twice and aliased:

  1. db.define_table('thing',
  2. Field('name'),
  3. Field('owner_id1', 'reference person'),
  4. Field('owner_id2', 'reference person'))
  5. rows = db(db.person).select(
  6. join=[db.person.with_alias('owner_id1').on(db.person.id == db.thing.owner_id1),
  7. db.person.with_alias('owner_id2').on(db.person.id == db.thing.owner_id2)])

The value of join can be list of db.table.on(...) to join.

Left outer join

Notice that Carl did not appear in the list above because he has no things. If you intend to select on persons (whether they have things or not) and their things (if they have any), then you need to perform a LEFT OUTER JOIN. This is done using the argument “left” of the select. Here is an example:

  1. >>> rows = db().select(db.person.ALL, db.thing.ALL,
  2. ... left=db.thing.on(db.person.id == db.thing.owner_id))
  3. >>> for row in rows:
  4. ... print row.person.name, 'has', row.thing.name
  5. ...
  6. Alex has Boat
  7. Alex has Chair
  8. Bob has Shoes
  9. Carl has None

where:

  1. left = db.thing.on(...)

does the left join query. Here the argument of db.thing.on is the condition required for the join (the same used above for the inner join). In the case of a left join, it is necessary to be explicit about which fields to select.

Multiple left joins can be combined by passing a list or tuple of db.mytable.on(...) to the left parameter.

Grouping and counting

When doing joins, sometimes you want to group rows according to certain criteria and count them. For example, count the number of things owned by every person. web2py allows this as well. First, you need a count operator. Second, you want to join the person table with the thing table by owner. Third, you want to select all rows (person + thing), group them by person, and count them while grouping:

  1. >>> count = db.person.id.count()
  2. >>> for row in db(db.person.id == db.thing.owner_id
  3. ... ).select(db.person.name, count, groupby=db.person.name):
  4. ... print row.person.name, row[count]
  5. ...
  6. Alex 2
  7. Bob 1

Notice the count operator (which is built-in) is used as a field. The only issue here is in how to retrieve the information. Each row clearly contains a person and the count, but the count is not a field of a person nor is it a table. So where does it go? It goes into the storage object representing the record with a key equal to the query expression itself.

The count method of the Field object has an optional distinct argument. When set to True it specifies that only distinct values of the field in question are to be counted.