Complex Objects and Tables
Instance Variable with a table - One-to-one
We will look at an example where an object has data in two different tables in the database. We add an address to the Person class. Each person will only have one address. An Address is a class with instance variables for a street and a city. Here are the declarations of each class:
Object subclass: #Person
instanceVariableNames: 'firstName lastName id address '
classVariableNames: ''
poolDictionaries: ''
Object subclass: #Address
instanceVariableNames: 'street city id '
classVariableNames: ''
poolDictionaries: ''
In the database we will use two tables, PEOPLE and ADDRESSES to store the data. Two tables are used since more than one person may have the same address. First we need to create the GLORP descriptor for the database. Here is the definition of the descriptor class:
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
Now we need to provide the list of classes and tables. Here are the relevant methods:
GlorpTutorialDescriptor >>allTableNames
^#( 'PEOPLE' 'ADDRESSES' )
GlorpTutorialDescriptor >>constructAllClasses
^(super constructAllClasses)
add: Person;
add: Address;
yourself
The definition of the ADDRESSES table is straightforward.
GlorpTutorialDescriptor >>tableForADDRESSES: aTable
aTable createFieldNamed: 'street' type: (platform varChar: 50).
(aTable createFieldNamed: 'city' type: (platform varChar: 50)).
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey
In the PEOPLE table we need to add a foreign key. Here is how it is done.
GlorpTutorialDescriptor >>tableForPEOPLE: aTable
| addressId |
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
(aTable createFieldNamed: 'first_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'last_name' type: (platform varChar: 50)).
addressId := aTable createFieldNamed: 'address_id' type: platform int4.
aTable addForeignKeyFrom: addressId to: ((self tableNamed: 'ADDRESSES') fieldNamed: 'id').
Each class also needs a descriptor entry. The Address class has the same structure as the Person class in the last example. So the descriptor does not have anything new.
GlorpTutorialDescriptor >>descriptorForAddress: aDescriptor
| table |
table := self tableNamed: 'ADDRESSES'.
aDescriptor table: table.
(aDescriptor newMapping: DirectMapping) from: #street
to: (table fieldNamed: 'street').
(aDescriptor newMapping: DirectMapping) from: #city
to: (table fieldNamed: 'city').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (table fieldNamed: 'id').
In the Person class we need information about the address. The last two entries below do this. The OneToOneMapping tells GLORP to use a different table (via a one-to-one map) to get the data for address. Note we do not provide information about which table to get the data for the address instance variable. That information will be added in a different location.
GlorpTutorialDescriptor >>descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: OneToOneMapping)
attributeName: #address.
We still need to let GLORP know how to deal with the address instance variable in the Person class. This is done using a class model. In the method we provide information about each instance variable.
GlorpTutorialDescriptor >>classModelForAddress: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #street.
aClassModel newAttributeNamed: #city.
GlorpTutorialDescriptor >>classModelForPerson: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #lastName.
aClassModel newAttributeNamed: #address type: Address.
Of interest is “address”. The line:
aClassModel newAttributeNamed: #address type: Address.
tells GLORP that the instance variable address holds an object of type Address. Since the descriptor contains information about the Address class, this is how GLORP knows how to map the address instance variable of Person to the database.
Now we can log on to the database and create the tables.
login := Login new
database: OracleODBCPlatform new;
username: 'taylor';
password: 'foo';
connectString: 'orcl';
yourself
accessor := DatabaseAccessor forLogin: login.
accessor login.
session := GlorpSession new.
session system: (GlorpTutorialDescriptor forPlatform: login database).
session accessor: accessor.
accessor dropTables: session system allTables.
session inTransactionDo:
[
session system platform areSequencesExplicitlyCreated
ifTrue:
[session system allSequences do:
[:each |
accessor createSequence: each
ifError: [:error | Transcript show: error messageText]]].
session system allTables do:
[:each |
accessor createTable: each
ifError: [:error | Transcript show: error messageText]]]
Once the tables are created we can add some data. Here we create a Person object with an address and add it to the database.
session beginUnitOfWork.
person := Person first: 'Sam' last: 'Whitney'.
address := Address new.
address
street: 'Maple';
city: 'SD'.
person address: address.
session register: person.
session commitUnitOfWork
Just for the record here is the SQL generated during the above transaction.
Begin Transaction
select nextval('ADDRESSES_id_SEQ') from ADDRESSES limit 1
select nextval('PEOPLE_id_SEQ') from PEOPLE limit 1
INSERT INTO ADDRESSES (street,city,id) VALUES ('Maple','SD',2)
(0.004 s)
INSERT INTO PEOPLE (id,first_name,last_name,address_id) VALUES (2,'Sam','Whitney',2)
(0.003 s)
Commit Transaction
With some data in the database we can make queries on the data. Here is a simple query. Since the statement just reads data there is no need to put this statement in a transaction or unit of work.
foundPerson := session readOneOf: Person where: [:each | each address city = 'SD'].
The query uses the following SQL to get the data from the database:
SELECT t1.id, t1.first_name, t1.last_name, t1.address_id
FROM (PEOPLE t1 INNER JOIN ADDRESSES t2 ON (t1.address_id = t2.id))
WHERE (t2.city = 'SD')
Alert readers will note that only the id is read from the ADDRESSES table. The Person object created by the query has a proxy for its instance variable. The data for the address is not fetched from the database until it is actually used. After adding the accessor methods to the People and Address classes the following statement:
foundPerson address street
will trigger the proxy to fetch the Address object. The SQL used to do that is:
SELECT t1.street, t1.city, t1.id
FROM ADDRESSES t1
WHERE (t1.id = 1)
Using a unit of work any change to the Person object will be saved to the database. Here we change the Address object in the Person object. When the unit of work is committed the address data is updated.
session beginUnitOfWork.
foundPerson := session readOneOf: Person where: [:each | each address city = 'SD'].
foundPerson address street: 'Pine'.
session commitUnitOfWork.
Here is the SQL used to update the data. Note that GLORP is making an unneeded request for the next value in the ADDRESSES is sequence.
Begin Transaction
select nextval('ADDRESSES_id_SEQ') from ADDRESSES limit 1
UPDATE ADDRESSES SET city = 'SD',street = 'Pine' WHERE id = 1
(0.093 s)
Commit Transaction
Once we are done with the session we logout:
accessor logout
Duplicate Rows
Adding a Person object to the database also adds the person’s address if the address is new. If two people have the same address, it is possible to add the same address twice. For example suppose we first do:
session inUnitOfWorkDo:
[roger := Person first: ‘Roger last: ‘Whitneyy’.
address := Address street: 'Campanile Way' city: 'SD'.
roger address: address.
session register: roger].
And at some later time we do:
session inUnitOfWorkDo:
[sam := Person first: ‘Sam’ last: ‘Hinton’.
address := Address street: 'Campanile Way' city: 'SD'.
sam address: address.
session register: sam].
Now the address table will have two rows with the same data. If you do not want this to occur, you must take care. You should read the address from the database and then add it to the Person object.
Orphan Rows
Although adding a person to the database will add its address to the database if needed, deleting a person does not delete its address. That is if one first does:
session inUnitOfWorkDo:
[roger := Person first: ‘Roger last: ‘Whitneyy’.
address := Address street: 'Campanile Way' city: 'SD'.
roger address: address.
session register: roger].
And then does
roger := session readOneOf: Person
where: [:each | each firstName = 'Roger'].
session delete: roger.
The address associated with roger is still in the address table. So, you must explicitly delete each object from the database.
PseudoVariables
In our descriptor we can define pseudovariables for a class. These are variables that do not exist in the class, but we can use in a GLORP query about the class. Below we define a pseudovariable addressId for the address_id column in the PEOPLE table.
GlorpTutorialDescriptor >>descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: DirectMapping)
fromPseudoVariable: #addressId
to: (personTable fieldNamed: 'address_id').
(aDescriptor newMapping: OneToOneMapping)
attributeName: #address
In a query we can then use the pseudovariable as if it were an instance variable. Here is an example use of the psuedovariable.
roger := session readOneOf: Person where: [:each |
each addressId = 1].
In general we prefer not to deal with database id’s
Collections as Instance Variable - One-to-many
Often an object will contain a collection. The way this is handled in a database is to create a separate table for the collection. Each row in the table contains a foreign key indicating who owns the row. GLORP handles this with a one-to-many map. To illustrate this we give the Person class a collection of email addresses. An EmailAddress has a username and host. We create EMAIL_ADDRESSES table with columns:
| |
id | Primary Key for table |
user_name | User name of the email address |
host | The email host |
person_id | Foreign key to person table |
Here is the definition of the EmailAddress class.
Object subclass: #EmailAddress
instanceVariableNames: 'userName host id '
classVariableNames: ''
poolDictionaries: ''
“EmailAddress class method”
name: aNameString host: aHostString
^super new
userName: aNameString;
host: aHostString;
yourself
The class also has standard accessor methods to set and get userName and host.
The Person class definition:
Object subclass: #Person
instanceVariableNames: 'firstName lastName id emailAddresses '
classVariableNames: ''
poolDictionaries: ''
“Person class method”
first: firstNameString last: lastNameString
^super new
setFirst: firstNameString
last: lastNameString;
yourself
“Person instance methods”
setFirst: firstNameString last: lastNameString
firstName := firstNameString.
lastName := lastNameString.
emailAddresses := OrderedCollection new.
addEmailAddress: anEmailAddress
emailAddresses add: anEmailAddress
emailAddresses
^emailAddresses
The Person class also has get and set methods for lastName and firstName.
To save space the example does not include the address added in the one-to-one section. Since the only thing new here is the one-to-one mapping the example is given in full without comment. Here is the Descriptor:
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
allTableNames
^#( 'PEOPLE' 'EMAIL_ADDRESSES')
constructAllClasses
^(super constructAllClasses)
add: Person;
add: EmailAddress;
yourself
classModelForEmailAddress: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #userName.
aClassModel newAttributeNamed: #host.
classModelForPerson: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #lastName.
aClassModel newAttributeNamed: #emailAddresses collectionOf: EmailAddress.
descriptorForEmailAddress: aDescriptor
| table |
table := self tableNamed: 'EMAIL_ADDRESSES'.
aDescriptor table: table.
(aDescriptor newMapping: DirectMapping) from: #userName to: (table fieldNamed: 'user_name').
(aDescriptor newMapping: DirectMapping) from: #host to: (table fieldNamed: 'host').
(aDescriptor newMapping: DirectMapping) from: #id to: (table fieldNamed: 'id').
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: OneToManyMapping)
attributeName: #emailAddresses.
tableForEMAIL_ADDRESSES: aTable
| personId |
aTable createFieldNamed: 'user_name' type: (platform varChar: 50).
(aTable createFieldNamed: 'host' type: (platform varChar: 50)).
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
personId := aTable createFieldNamed: 'person_id' type: platform int4.
aTable addForeignKeyFrom: personId to: ((self tableNamed: 'PEOPLE') fieldNamed: 'id').
tableForPEOPLE: aTable
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
(aTable createFieldNamed: 'first_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'last_name' type: (platform varChar: 50)).
Order of the Email Addresses
When the email addresses are read for a Person we can determine the order in which they are read from the database. This is done by telling the mapping the order to use when reading values. In the descriptor for the Person we indicate that the addresses are to be sorted by the userName. The ordering is done on the database. Multiple orderBy: messages can be sent to a mapping.
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: ToManyMapping)
attributeName: #emailAddresses;
orderBy: #userName
The read order can be specified by indicating the table and column explicitly. There is no reason to do this in the current example. However, when we have association (tie or link) tables a mapping will involve three tables. In that case being able to indicate the table and column can be useful. Here is how to specify the table and column.
(aDescriptor newMapping: ToManyMapping)
attributeName: #emailAddresses;
orderBy: [:each | (each getTable: 'EMAIL_ADDRESSES') getField: 'user_name']
Specifying the Type of Collection
When reading a person from the database as below, the email addresses in the Person object are stored in an ordered collection.
foundPerson := session readOneOf: Person where: [:each | each firstName = 'Sam'].
As of GLORP 0.3.111 it is possible to specify in the class model which type of collection GLORP will use. Below we indicate that a SortedCollection should be used. We can use all the standard collection classes except a Dictionary, which requires extra information. Using a dictionary is covered in the next section.
GlorpTutorialDescriptor>>classModelForPerson: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #lastName.
aClassModel newAttributeNamed: #emailAddresses collection: SortedCollection of: EmailAddress.
Note:
This feature does not work in versions of GLORP before 0.3.111 without a lot more work.
Dictionaries as Instance Variables - One-to-many
Mapping a dictionary to a database table is slightly more complex then mapping an ordered collection. To illustrate this we will create an address book that stores instances of Person. The address book will store people in a dictionary. Keys will be a string that users associate a particular person with. Here is the AddressBook class.
Object subclass: #AddressBook
instanceVariableNames: 'id title entries '
classVariableNames: ''
poolDictionaries: ''
”AddressBook class methods”
title: aString
^super new
setTitle: aString;
yourself
“AddressBook instance methods”
at: aString
^entries at: aString
at: aString put: aPerson
entries at: aString put: aPerson
title
^title
setTitle: aString
title := aString.
entries := Dictionary new.
The Person class just has the instance variables firstName, lastName and id. The class definition is below. You can add whatever methods you want to the class.
Object subclass: #Person
instanceVariableNames: 'firstName lastName id'
classVariableNames: ''
poolDictionaries: ''
We need three tables in the database. First we need one to hold the Person object data. Second we need a table to for address books. Each row in the ADDRESS_BOOK table represents one address book. In this case the table just has two columns, one for a primary key and another for the title of the address book. The third table, ADDRESS_BOOK_LINKS, is an association (link or tie) table. Each row in the association table represents one entry in an address book.
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
allTableNames
^#( 'PEOPLE' 'ADDRESS_BOOK' 'ADDRESS_BOOK_LINKS' )
constructAllClasses
^(super constructAllClasses)
add: Person;
add: AddressBook;
yourself
Note how we indicate that the entries instance variable is a dictionary.
classModelForAddressBook: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #title.
aClassModel newAttributeNamed: #entries collection: Dictionary of: Person.
classModelForPerson: aClassModel
aClassModel newAttributeNamed: #lastName.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #id.
The address book descriptor includes a dictionary mapping which ties the data from the other two tables to the address book.
descriptorForAddressBook: aDescriptor
| table linkTable |
table := self tableNamed: 'ADDRESS_BOOK'.
linkTable := self tableNamed: 'ADDRESS_BOOK_LINKS'.
aDescriptor table: table.
aDescriptor addMapping: (DirectMapping from: #id to: (table fieldNamed: 'id')).
aDescriptor addMapping: (DirectMapping from: #title to: (table fieldNamed: 'title')).
aDescriptor addMapping: ((BasicDictionaryMapping new)
attributeName: #entries;
referenceClass: Person;
keyField: (linkTable fieldNamed: 'person_key');
relevantLinkTableFields: (Array with: (linkTable fieldNamed: 'person_id')))
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #id to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: DirectMapping)
from: #firstName to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName to: (personTable fieldNamed: 'last_name').
tableForADDRESS_BOOK: aTable
(aTable createFieldNamed: 'title' type: (platform varChar: 50)).
(aTable createFieldNamed: 'id' type: (platform sequence)) bePrimaryKey.
Each row in the ADDRESS_BOOK_LINKS represents one entry in an address book. It has foreign keys to the other tables to indicate which person and which address book the entry is in. It also stores the key for the entry in the dictionary.
tableForADDRESS_BOOK_LINKS: aTable
| personId bookId |
personId := aTable createFieldNamed: 'person_id' type: platform int4.
aTable
addForeignKeyFrom: personId
to: ((self tableNamed: 'PEOPLE') fieldNamed: 'id').
bookId := aTable createFieldNamed: 'address_book_id' type: (platform varChar: 50).
aTable
addForeignKeyFrom: bookId
to: ((self tableNamed: 'ADDRESS_BOOK') fieldNamed: 'id').
aTable createFieldNamed: 'person_key' type: (platform varChar: 30).
tableForPEOPLE: aTable
(aTable createFieldNamed: 'first_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'last_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'id' type: (platform sequence)) bePrimaryKey.
The following shows an example of adding an AddressBook to the database.
session inUnitOfWorkDo:
[addresses := AddressBook title: 'work'.
addresses
at: 'musicMan' put: (Person first: 'Sam' last: 'Hinton');
at: 'author' put: (Person first: 'Martin' last: 'Fowler');
at: 'self' put: (Person first: 'Roger' last: 'Whitney').
session register: addresses].
Embedded Values – Two Objects One table
There are times when we do not want a separate table for each object. These objects are ones whose identity does not depend on identity like money or date range. In this case we store multiple objects in a single table. As an example we will use a Name object to represent the name of a Person object. First we have an outline of the Person and Name classes. Note that the Name class does not need an id. In the database it is stored in the same row as the rest of the Person object data, which does have a database id.
Object subclass: #Person
instanceVariableNames: 'name age id'
classVariableNames: ''
poolDictionaries: ''
“Person Class Methods”
first: firstNameString last: lastNameString
^self new
setFirst: firstNameString
last: lastNameString;
yourself
“Person Instance Methods”
setFirst: firstNameString last: lastNameString
name := Name first: firstNameString last: lastNameString
Object subclass: #Name
instanceVariableNames: 'first last '
classVariableNames: ''
poolDictionaries: ''
“Name Class Methods”
first: firstNameString last: lastNameString
^self new
setFirst: firstNameString
last: lastNameString;
yourself
“Name Instance Methods”
setFirst: firstNameString last: lastNameString
first := firstNameString.
last := lastNameString
The descriptor defines a Person table with four columns. Note the use of the EmbeddedValueOneToOneMapping in descriptorForPerson:.
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
allTableNames
^#( 'PEOPLE' )
constructAllClasses
^(super constructAllClasses)
add: Person;
add: Name;
yourself
classModelForName: aClassModel
aClassModel newAttributeNamed: #first.
aClassModel newAttributeNamed: #last.
classModelForPerson: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #name type: Name.
aClassModel newAttributeNamed: #age.
descriptorForName: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping) from: #first to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping) from: #last to: (personTable fieldNamed: 'last_name').
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping) from: #age to: (personTable fieldNamed: 'age').
(aDescriptor newMapping: DirectMapping) from: #id to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: EmbeddedValueOneToOneMapping) attributeName: #name.
tableForPEOPLE: aTable
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
(aTable createFieldNamed: 'first_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'last_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'age' type: (platform int)) defaultValue: 39.
When you read a Person object from the database, as below, the Name object for the Person name instance variable is not a proxy. There is no performance reason to read just part of the row, so the Name object data is read and the Name object is instantiated.
result := session readOneOf: Person where: [:each | each name first = ‘Roger’].
Currently it does not seem possible to use the embedded value as the primary key of the table.
One Object – Multiple Tables
There are times when the data for one object is located in multiple tables. Normalizing a database can be one cause of this. As an example we will use a Person class with the instance variables firstName, lastName and id. The database tables are PEOPLE and NAMES with the columns indicated below. The last_name_id column is the foreign key to the NAMES table.
PEOPLE
NAMES
We could create a class to represent the Names table, but in this example we will only use a Person class. Here is the definition of the class. You can add whatever methods you want to the class.
Object subclass: #Person
instanceVariableNames: 'firstName lastName id '
classVariableNames: ''
poolDictionaries: ''
Clearly, a join on the two tables is needed to get the data we need for a Person object. The method descriptorForPerson: contains the description of the join. The rest of the descriptor is straightforward.
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
allTableNames
^#( 'PEOPLE' 'NAMES' )
constructAllClasses
^(super constructAllClasses)
add: Person;
yourself
classModelForPerson: aClassModel
aClassModel newAttributeNamed: #lastName.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #id.
descriptorForPerson: aDescriptor
| personTable namesTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
namesTable := self tableNamed: 'NAMES'.
aDescriptor table: namesTable.
aDescriptor addMultipleTableJoin: (
Join
from: (personTable fieldNamed: 'last_name_id')
to: (namesTable fieldNamed: 'id')).
(aDescriptor newMapping: DirectMapping) from: #id to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: DirectMapping) from: #firstName to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping) from: #lastName to: (namesTable fieldNamed: 'last').
tableForNAMES: aTable
(aTable createFieldNamed: 'last' type: (platform varChar: 50)).
(aTable createFieldNamed: 'id' type: (platform sequence)) bePrimaryKey.
tableForPEOPLE: aTable
| lastNameId |
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
aTable createFieldNamed: 'first_name' type: (platform varChar: 50).
lastNameId := aTable createFieldNamed: 'last_name_id' type: platform int4.
aTable
addForeignKeyFrom: lastNameId
to: ((self tableNamed: 'NAMES') fieldNamed: 'id')
Association (Tie) Tables – Many-to-many
Association tables, sometimes called tie or link tables, are common. For example consider a database for a bookstore for books on order. We will have table of books and a table of customers. To reuse part of previous examples, we will make this a table of people. An individual may have many books on order. Also a given book may be on order by many different people. This is handled by using an association table. Each row of the table can represent the order of a book by one person. The table at least contains a column to refer to a Person in the PEOPLE table and a column to refer to a Book in the BOOKS table.
In the example the association table is called BOOKS_ON_ORDER. To keep the example small the Book class only contains an id and title instance variables. We do not show the standard accessor methods for the Book class. We add an instance variable booksOnOrder to the Person class. We show all but the needed accessor methods in the Person class.
Object subclass: #Book
instanceVariableNames: 'id title'
classVariableNames: ''
poolDictionaries: ''
Object subclass: #Person
instanceVariableNames: 'firstName lastName id booksOnOrder '
classVariableNames: ''
poolDictionaries: ''
“Class Method”
first: firstNameString last: lastNameString
^self new
setFirst: firstNameString
last: lastNameString;
yourself
“Instance Methods”
setFirst: firstNameString last: lastNameString
firstName := firstNameString.
lastName := lastNameString.
booksOnOrder := OrderedCollection new.
addBook: aBook
booksOnOrder add: aBook
books
^booksOnOrder
The descriptor has several new things. First of all it has more tables than classes.
DescriptorSystem subclass: #GlorpTutorialDescriptor
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
allTableNames
^#( 'PEOPLE' 'BOOKS_ON_ORDER' 'BOOKS')
constructAllClasses
^(super constructAllClasses)
add: Person;
add: Book;
yourself
classModelForBook: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #title.
classModelForPerson: aClassModel
aClassModel newAttributeNamed: #id.
aClassModel newAttributeNamed: #firstName.
aClassModel newAttributeNamed: #lastName.
descriptorForBook: aDescriptor
| table |
table := self tableNamed: 'BOOKS'.
aDescriptor table: table.
(aDescriptor newMapping: DirectMapping)
from: #title
to: (table fieldNamed: 'title').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (table fieldNamed: 'id').
The new thing here is the ManyToManyMapping mapping. The amazing thing is that we do not need to include the name of the association table. GLORP will deduce which table is the association table. We will see later how it explicitly list the association table.
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book
The tables are straightforward. The BOOKS table and PEOPLE table have primary keys and the columns for title and names.
tableForBOOKS: aTable
(aTable createFieldNamed: 'title' type: (platform varChar: 100)).
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey
tableForPEOPLE: aTable
(aTable createFieldNamed: 'id' type: platform sequence) bePrimaryKey.
(aTable createFieldNamed: 'first_name' type: (platform varChar: 50)).
(aTable createFieldNamed: 'last_name' type: (platform varChar: 50)).
The BOOKS_ON_ORDER table is the association or tie table. Each row represents one book on order for a person. So it has two foreign keys, one to the PEOPLE table and one to the BOOKS table.
tableForBOOKS_ON_ORDER: aTable
| custKey bookKey |
custKey := aTable createFieldNamed: 'customer_id' type: (platform int4).
aTable addForeignKeyFrom: custKey
to: ((self tableNamed: 'PEOPLE') fieldNamed: 'id').
bookKey := aTable createFieldNamed: 'BOOK_ID' type: (platform int4).
aTable addForeignKeyFrom: bookKey
to: ((self tableNamed: 'BOOKS') fieldNamed: 'id').
This is the end of the GlorpTutorialDescriptor class. Here is sample code to add some books and people to the database.
session beginUnitOfWork.
books := #( 'Code Complete' 'Palm OS' 'Cat in the Hat' ) collect: [:each | Book title: each ].
session registerAll: books.
session register: (Person first: 'Sam' last: 'Hinton').
session register: (Person first: 'Martin' last: 'Fowler').
session commitUnitOfWork
An example of reading books.
cat := session readOneOf: Book where: [:each | each title = 'Cat in the Hat'].
code := session readOneOf: Book where: [:each | each title = 'Code Complete'].
Now to have some people order books.
session beginUnitOfWork.
sam := session readOneOf: Person where: [:each | each firstName = 'Sam'].
sam
addBook: cat;
addBook: code.
martin := session readOneOf: Person where: [:each | each firstName = 'Martin'].
martin addBook: cat.
session commitUnitOfWork.
We can of course read a person and see what books they have on order.
sam := session readOneOf: Person where: [:each | each firstName = 'Sam'].
sam books
A more interesting query is to find all people that have a particular book or books on order. The following query will not work.
waitingForCat := session
readManyOf: Person
where:
[:each |
each booksOnOrder detect: [:book | book title = 'Cat in the Hat']].
While the where block would work if we were dealing with Smalltalk objects. However the where: block is used to generate SQL. There are limited messages that can be sent to the parameter of the where block. One class of messages that can be sent is instance variables of the class. So we can use ‘where: [:each | each booksOnOrder’. Since booksOnOrder is collection we can send it either anySatisfyJoin: or anySatisfyExists:. We will look at both.
First we have:
waitingForCat := session
readManyOf: Person
where:
[:each |
each booksOnOrder anySatisfyJoin: [:book | book title like: 'Cat%']].
This generates the following SQL query on the database.
SELECT DISTINCT t1.id, t1.first_name, t1.last_name
FROM
((PEOPLE t1 INNER JOIN BOOKS_ON_ORDER t3 ON (t3.customer_id = t1.id))
INNER JOIN BOOKS t2 ON (t3.BOOK_ID = t2.id))
WHERE (t2.title LIKE 'Cat%')
A second way is to use:
waitingForCode := session readManyOf: Person
where:
[:each |
each booksOnOrder anySatisfyExists: [:book | book title like: 'Code%']].
Or equivalently
waitingForCode := session readManyOf: Person
where:
[:each |
each booksOnOrder anySatisfy: [:book | book title like: 'Code%']].
This results in the following SQL:
SELECT t1.id, t1.first_name, t1.last_name
FROM PEOPLE t1
WHERE EXISTS
(SELECT t2.id
FROM BOOKS_ON_ORDER s1t1, BOOKS t2
WHERE
((t2.title LIKE 'Code%') AND
((s1t1.BOOK_ID = t2.id) AND
(s1t1.customer_id = t1.id))))
You can decide which is better for your situation.
Ordered Reading
A Person object contains a collection of books in the booksOnOrder instance variable. We can specify the order in which this collection is read from the database when we read a Person object. In the following modification of descriptorForPerson: the orderBy: message sent to the ManyToManyMapping orders the books by the title instance variable. Actually the ordering is done on the database side on the column mapped to the title instance variable of the Book class.
descriptorForPerson: aDescriptor
| personTable |
personTable := self tableNamed: 'PEOPLE'.
aDescriptor table: personTable.
(aDescriptor newMapping: DirectMapping)
from: #firstName
to: (personTable fieldNamed: 'first_name').
(aDescriptor newMapping: DirectMapping)
from: #lastName
to: (personTable fieldNamed: 'last_name').
(aDescriptor newMapping: DirectMapping)
from: #id
to: (personTable fieldNamed: 'id').
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
orderBy: #title
We can also use a block to indicate the order.
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
orderBy: [:each | each title]
While the block syntax is a bit more verbose it allows us to reverse the order as the following illustrates.
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
orderBy: [:each | (each title) descending];
It is also possible to explicitly specify the field on the table to be used to determine the order.
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
orderBy: [:each | (each getTable: 'BOOKS') getField: 'title']
UseLinkTable
If one looks at this example closely it is not clear how GLORP determines which table is the tie or link table. In the descriptor for the ManyToManyMapping one just provides the name of the instance variable (booksOnOrder) and the type of elements that will be held in the instance variable (Book). If you look at the examples that come with GLORP you will find places where the message “useLinkTable” is sent to the ManyToManyMapping as is done below. This tells GLORP that it needs to find a link table for this mapping. However, since ManyToManyMapping automatically sets the useLinkTable to true, this message is not need except to remind the reader that a link table is used.
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
orderBy: [:each | (each title) descending];
useLinkTable
Read Write mappings
There are times when the link table is populated via other means and we want to insure that we do not add or deleted link table entries. This can be done by making the mapping read only as is done below.
(aDescriptor newMapping: ManyToManyMapping)
attributeName: #booksOnOrder;
referenceClass: Book;
readOnly: true
Once this is done we cannot add or delete items from the link table via this mapping. It can be done other ways. However the following code will not add the books to the Sam’s list of books on order. No exceptions are raised, so Sam is added to the PEOPLE table.
cat := session readOneOf: Book where: [:each | each title = 'Cat in the Hat'].
code := session readOneOf: Book where: [:each | each title = 'Code Complete'].
session inUnitOfWorkDo:
[sam := Person first: 'Sam' last: 'Hinton'.
session register: sam
sam
addBook: cat;
addBook: code]