Tips_tutorials   >   Studio102   >   Studio 102 (All Contents)
Welcome to the
tutorial.This tutorial is provided to www.studiotips.net and click the link provided for becoming a StudioTips Member. The cost of membership is minimal and helps to cover the time and expense of creating these tutorials and maintaining the studiotips.net website.
only. If you are not currently a please go toThis tutorial continues from where we left off in the
tutorial. In this tutorial we will:In the process of doing the above we will create a tBase superclass table class with subclass tables classes for each table in the database.
The Studio 101 tutorial gave you very explicit directions for each operation you needed to complete. Knowing that you have been through the Studio 101 tutorial, the Studio 102 tutorial is less explicit when giving directions. For example, the Studio 101 tutorial gave you explicit directions like:
Contacts library > double-click tCountry table class.
> selectThe Studio 102 tutorial will give you abbreviated instructions like:
Double-click the tCountry table class.
When entering code for this tutorial you will need to create variables as you write the code. The StudioTips
for variables tell you the variable type and scope based on the variable name. Here are a few naming convention hints to help you:In this section we will add a Stateprov table to the database. A foreign key column, Country_fkey, will be included in the Stateprov table. The foreign key column will be used to store the primary key of the parent Country record.
In SQL terms, the Country table is called the referenced table, and the Country_pkey column is called the referenced column.
CREATE TABLE Stateprov (Stateprov_pkey INTEGER NOT NULL,StateProvName VARCHAR (30) NOT NULL,StateProvAbbrev VARCHAR (5),Country_fkey INTEGER NOT NULL)
CREATE CASE SENSITIVE UNIQUE INDEX Stateprov_pkey ON Stateprov (Stateprov_pkey)
CREATE CASE SENSITIVE UNIQUE INDEX StateProvName ON Stateprov (StateProvName)
CREATE INDEX Country_fkey ON Stateprov (Country_fkey)
The above SQL scripts used to create the unique indexes is specific to the Omnis data file. For creating unique index constraints on other databases check the documentation specific to the RDBMS.
I find it helpful to have some test data in the database when developing an application so lets insert a few Stateprov records using the window.
To test the join between the Stateprov and Country records:
In this section we will create a schema class which maps to the Stateprov table in the database.
We need a to create a table class which is mapped to the sStateprov schema class. We could duplicate and modify the tCountry table class, but that will result in code duplication. The custom table class methods of tStateprov will be identical or only slightly modified. We are also going to need table classes for Towncity and Contact. Duplicating the table class over and over is going to result in lot of duplicate code. Not a good thing.
A situation like is where you want to use a superclass, and create subclasses which inherit properties and methods from the superclass.
We now have a superclass table class, tBase, and two subclass table classes, tCountry and tStateprov. All of the methods and properties of tBase, except the $sqlclassname property, is currently be inherited by the subclasses.
We now need to look at the tBase methods to check if they will work for all of the subclasses. If possible, we will modify the methods to make them generic so that they will work for all the subclasses.
Quit method ''
; Assume that the first column in the schema or query class is the primary key.
Calculate ColName as $cinst.$cols.1.$name
; If the column name does not include the suffix '_pkey' report an error and set the colname variable to null.
If pos('_pkey',low(ColName))=0
OK message [sys(85)] (Icon) {Unable to find the primary key in the [$cinst.$sqlclassname] SQL class.}
Calculate ColName as #NULL
End If
Quit method ColName
; In a query class, there can be multiple table names.
Calculate Scratch as $cinst.$servertablenames
; Parse the first server table name as the base table name.
Calculate TableName as strtok('Scratch',',')
Quit method TableName
In this section we will create a query class which joins the Stateprov records to the Country records.
You can select and drag multiple columns from the
onto a schema class.I have run into situations where the Omnis data file was finicky about the order of the foriegn key and primary key. The join would fail if the primary key was first. To avoid this problem I always put the foriegn key ahead of the primary key in the $extraquerytext. To help you remember think of it as putting them in alphabetic order, fkey come before pkey.
We need to create a table class mapped to this query class. We can either create another subclass of tBase or just copy the tStateprov table class and change the $sqlclassname property to qStateprovList.
We can test the schema and query classes using the
.; Define the list binding it to the table class.
Do List.$definefromsqlclass('tStateprov')
If List.$colcount=0
OK message [sys(85)] (Icon) {The list has zero columns after $definefromsqlclass('tStateprov')}
Else
; Set the session object.
Do List.$sessionobject.$assign($ctask.dbsessionobj)
; Get all the records.
Do List.$getAllRecords() Returns FlagOK
End If
The wStateprovList has the added complication that for each Stateprov record, the user needs to be able to select a parent CountryName record and link the Stateprov record to the Country record.
When the user selects a Country record, we will need to set the foreign key, Country_fkey, in the Stateprov record to the primary key, Country_pkey, of the Country record.
Another complication is that we cannot issue a $dowork against the tStateprovList table class because it includes columns from more than one table. The reason for this is that when a record is added to the list or updated, how does Omnis Studio decide that it should insert/update a Stateprov record, but not insert/update a Country record? In this situation we are only joining two tables, imagine a query that joins 4 or 5 tables. It would be a logistics nightmare to figure out which tables to insert, update, or delete records. Therefore, the Omnis Studio built-in $dowork will not execute for query classes which include columns from more than one schema class.
It is possible to override the built-in $dowork method and come up with your own technique for looping through the smartlist and inserting/updating/deleting records for a multi-table query class. We use this technique in the StudioWorks framework to simplify our application code.
For this tutorial we will be using the tStateprovList table class (which points to qStateprovList) to fetch the records, but then merge those fetched records into a list defined from tStateprov (which points to sStateprov). Before merging the tStateprovList records we will add a CountryName column to the tStateprov defined list. Columns added to a list defined using $definefromsqlclass are not included in the insert/update/delete SQL generated by Omnis Studio.
There are numerous user interface techniques we could use. For this example:
Since the wStateprovList window is going to be similar to the wCountryList window, we can save some work by duplicating the wCountryList window and then modifying the duplicate.
; Define and build the list variables used in this window and fetch records as applicable.
Do method retDefinedList ('tStateprovList') Returns List
If List.$colcount=0
Calculate FlagOK as kFalse
Else
; Get all the Stateprov records joined to their parent Country records.
Do List.$getAllRecords() Returns FlagOK
If FlagOK
; Define a list based on the sStateprov schema.
Do method retDefinedList ('tStateprov') Returns iList
If iList.$colcount=0
Calculate FlagOK as kFalse
Else
; Add a column to the Stateprov list for the country name.
Do iList.$cols.$add('CountryName',kCharacter,kSimplechar,100)
; Merge the query based list into the schema based list.
Do iList.$merge(List,kTrue) ;; (Match column names)
; Make the list into a smartlist.
Do iList.$smartlist.$assign(kTrue)
Do method retDefinedList ('tCountry') Returns iCountryList
If iCountryList.$colcount=0
Calculate FlagOK as kFalse
Else
; Get all the records in the Country table for use by the CountryName lookups.
Do iCountryList.$getAllRecords() Returns FlagOK
; Set the list to be a smartlist so that we can save any changes to the list.
Do iCountryList.$smartlist.$assign(kTrue)
End If
End If
End If
End If
Quit method FlagOK
; Define a main list to be used for displaying Stateprov records joined to Country records.
Do List.$definefromsqlclass(pTableClassName)
If List.$colcount=0
OK message [sys(85)] (Icon) {The [pTableClassName] list has zero columns.}
Else
; Set the session object in the list variable so that the SQL statements will be issued to that session's database.
Do List.$sessionobject.$assign(dbsessionobj)
End If
Quit method List
; Define and build the list variables used in this window.
Do method buildLists Returns FlagOK
Quit method FlagOK
On evBefore
; Find the matching country in the countries list.
Do iCountryList.$search($ref.Country_pkey=iList.Country_fkey,1,0,0,0)
On evClick ;; Event Parameters - pRow ( Itemreference )
; Set the foreign key to match the selected country's primary key.
Process event and continue
Calculate iList.Country_fkey as pRow.Country_pkey
Calculate iList.CountryName as pRow.CountryName
Do $cfield.$redraw()
On evAfter
Do method event_evAfter
; Preset the flag to true.
Calculate FlagOK as kTrue
; Make sure the CountryName entered by the user matches an entry in the Countries list.
If iCountryList.$search(low($ref.CountryName)=low(iList.CountryName),1,0,0,0)
; Update the foreign key in the Stateprov list.
Calculate iList.CountryName as iCountryList.CountryName
Calculate iList.Country_fkey as iCountryList.Country_pkey
Else
; If not, ask the user whether or not they want to add the country.
Process event and continue (Discard event)
No/Yes message [sys(85)] (Icon) {The Country Name entered does not match a country in the countries list.////Do you wish to add [$cobj.$contents] as a country?}
If flag true
; Add a line to the countries list.
Do iCountryList.$add()
Do iCountryList.$line.$assign($ref.$linecount)
; Set the country name.
Calculate iCountryList.CountryName as $cobj.$contents
; Use $dowork to inserted the new country into the database.
Do iCountryList.$dowork() Returns FlagOK
If FlagOK
; Update the foreign key in the Stateprov list.
Calculate iList.Country_fkey as iCountryList.Country_pkey
End If
End If
End If
Quit method FlagOK
Phew! That was a lot work. Better take a break and treat yourself to a coffee!
In order to open the wStateprovList window we need to add a menu line to the menu.
; Find and open an instance of the state/prov list window class.
Calculate ClassName as 'wStateprovList'
Do $clib.$windows.$findname(ClassName) Returns rClass
If isnull(rClass)
OK message [sys(85)] (Icon) {Unable to find the window class '[ClassName]'.}
Else
Do rClass.$openonce('*') Returns rWin
If isnull(rWin)
OK message [sys(85)] (Icon) {Unable to open an instance of the window class [ClassName].}
End If
End If
Quit method rWin
We are ready to test the wStateprovList window.
There are many user interface approaches you can use for this window. One of the more popular approaches is to auto-suggest a list of countries by opening a droplist of countries which match what the user has typed in the CountryName field. If the user types the letter U, a droplist displaying UK and USA is opened.
Another form of auto-suggest is type-ahead which types ahead of the user in the entry field the first matching record found in the database. If the user types the letter C, the country name Canada is entered for the user in the entry field for the user. The user can continue typing a different country name, e.g. Cambodia, and when they type Cam, if Cambodia is in the database, type-ahead will enter Cambodia in the entry field for the user.
Type-ahead and auto-suggest can be combined.
A helpful feature is to remember the last country the user selected beginning with the first letter they type, and default to selecting that country. For example if they last selected USA, the next time they type the letter U, pre-select USA rather than UK.
Creating an auto-suggest type-ahead widget which defaults to the last selection for each letter of the alphabet is beyond the scope of this tutorial. The StudioWorks framework includes an auto-suggest type-ahead widget which defaults to the last selection for each letter of the alphabet for each user.In this section we will add a Towncity table to the database, insert some records, create the schema class, query class, and window class.
If all of our contacts were only located in Canada and the USA we could just link each Towncity record to a Stateprov record and then rely on the Stateprov record to join the Towncity to the correct Country.
However, not all countries have states or provinces, so we must include foreign keys in the Towncity table to the Stateprov table and the Country table.
The Stateprov_fkey foreign key in the Towncity table needs to be optional for towns which are located in a country that does not have states or provinces.
The correct way to do this in the SQL world is to enter a null value in the Stateprov_fkey and then use an outer join to select the Towncity/Stateprov/Country records.
Unfortunately the Omnis Studio data file performance for outer joins is very slow. Using outer joins in your SQL text adds some complexity to your SQL scripts. To overcome these issues I use a technique which may cause some SQL gurus to shudder. The technique is to create an empty parent record with its primary key set to zero (0). I then set the optional foreign key value in the child record to zero (0), thereby making a valid join to the empty parent record, negating the need for using outer joins in the SQL scripts. The downside on using the empty record technique is that you need to remember to exclude the empty records when selecting records from tables which contain emtpy records.
It is up to you whether you use a null value foreign key and outer joins, or a zero value foreign key and an empty record. For this tutorial we'll use the empty record technique.In this section we will add a Towncity table to the database. The foreign key columns, Country_fkey, and Stateprov_fkey will be included in the Towncity table.
CREATE TABLE Towncity (Towncity_pkey INTEGER NOT NULL,TownCityName VARCHAR (30) NOT NULL,Stateprov_fkey INTEGER NOT NULL,Country_fkey INTEGER NOT NULL)
CREATE CASE SENSITIVE UNIQUE INDEX Towncity_pkey ON Towncity (Towncity_pkey)
CREATE CASE SENSITIVE INDEX TownCityName ON Towncity (TownCityName)
CREATE INDEX Towncity_Stateprov_fkey ON Towncity (Stateprov_fkey)
CREATE INDEX Towncity_Country_fkey ON Towncity (Country_fkey)
The above indexes do not prevent the user from entering the same TownCityName twice. Different countries could have the same TownCityName so we can not make this a unique index. To prevent the user from entering a TownCityName twice for the same country we will add a composite unique index on the TownCityName and Country_fkey.
CREATE CASE SENSITIVE UNIQUE INDEX TownCityName_Country_fkey ON Towncity (TownCityName,Country_fkey)
The above SQL scripts used to create the unique indexes is specific to the Omnis data file. For creating unique index constraints on other databases check the documentation specific to the RDBMS.
I find it helpful to have some test data in the database when developing an application so lets insert a few Towncity records using the window.
Test the join between the Towncity, Stateprov, and Country records.
The reason that Country_fkey is prefixed with Towncity in the above SELECT is that there is a Country_fkey column in the Towncity table and the Stateprov table. Both tables are included in this select, so we must specify Country_fkey column's table in the SELECT.
To allow a Towncity record to use zero (0) in the Stateprov_fkey field, we need to add a Country record with the Country_pkey set to zero (0) and a Stateprov record with the Stateprov_pkey set to zero (0) and the Country_fkey set to zero (0).
To avoid selecting the empty records we need to modify the $getAllRecords method in tBase.
; Prepare the ORDER BY text.
If len(pOrderBySQL)
Calculate OrderBy as pOrderBySQL
Else
Calculate OrderBy as $cinst.$:DefaultOrderBy
End If
; Prepare the SQL text to exclude the empty zero(0) primary key record.
Calculate ColName as $cinst.$:PrimaryKeyColName
If pos("WHERE ",upp($cinst.$extraquerytext))
Calculate SQLText as con("AND ",ColName," <> 0")
Else
Calculate SQLText as con("WHERE ",ColName," <> 0")
End If
Calculate SQLText as con(SQLText,' ',OrderBy)
; Select all the records in the table.
Do $cinst.$select(SQLText) Returns FlagOK
If not(FlagOK)
OK message [sys(85)] (Icon) {Flag false after $cinst.$select([SQLText])}
Else
; Fetch all the records in the table.
Do $cinst.$fetch(kFetchAll) Returns FetchStatus
If not(FetchStatus)
Calculate FlagOK as kFalse
OK message [sys(85)] (Icon) {Flag false after $cinst.$fetch(kFetchAll)}
Else
; Set the current line to the first line.
Do $cinst.$line.$assign(1)
End If
End If
Quit method FlagOK
In this section we will create a schema class which maps to the Towncity table in the database.
We need a to create a table class which is mapped to the sTowncity schema class.
Quit method 'ORDER BY TownCityName'
We will create a query class which joins the Towncity table to the Stateprov table and to the Country table.
We need to create a table class mapped to this query class. We can either create another subclass of tBase or just copy the tTowncity table class and change the $sqlclassname property to qTowncityList.
We can test the schema and query classes using the
.; Define the list binding it to the table class.
Do List.$definefromsqlclass('tTowncity')
If List.$colcount=0
OK message [sys(85)] (Icon) {The list has zero columns after $definefromsqlclass('tTowncity')}
Else
; Set the session object.
Do List.$sessionobject.$assign($ctask.dbsessionobj)
; Get all the records.
Do List.$getAllRecords() Returns FlagOK
End If
Since the wTowncityList window is going to be similar to the wStateprovList window, we can save some work by duplicating the wStateprovList window and then modifying the duplicate.
; Define and build the list variables used in this window and fetch records as applicable.
Do method retDefinedList ('tTowncityList') Returns List
If List.$colcount=0
Calculate FlagOK as kFalse
Else
; Get all the Stateprov records joined to their parent Country records.
Do List.$getAllRecords() Returns FlagOK
If FlagOK
; Define a list based on the sStateprov schema.
Do method retDefinedList ('tTowncity') Returns iList
If iList.$colcount=0
Calculate FlagOK as kFalse
Else
; Add columns to the Towncity list for the stateprov name and country name.
Do iList.$cols.$add('StateProvName',kCharacter,kSimplechar,100)
Do iList.$cols.$add('CountryName',kCharacter,kSimplechar,100)
; Merge the query based list into the schema based list.
Do iList.$merge(List,kTrue) ;; (Match column names)
; Make the list into a smartlist.
Do iList.$smartlist.$assign(kTrue)
Do method retDefinedList ('tStateprovList') Returns iStateProvList
If iStateProvList.$colcount=0
Calculate FlagOK as kFalse
Else
; Get all the records in the Country table for use by the CountryName lookups.
Do iStateProvList.$getAllRecords() Returns FlagOK
If FlagOK
Do method retDefinedList ('tCountry') Returns iCountryList
If iCountryList.$colcount=0
Calculate FlagOK as kFalse
Else
; Get all the records in the Country table for use by the CountryName lookups.
Do iCountryList.$getAllRecords() Returns FlagOK
; Set the list to be a smartlist so that we can save any changes to the list.
Do iCountryList.$smartlist.$assign(kTrue)
End If
End If
End If
End If
End If
End If
Quit method FlagOK
; Preset the flag to true.
Calculate FlagOK as kTrue
; Make sure the CountryName entered by the user matches an entry in the Countries list.
If iStateProvList.$search(low($ref.StateProvName)=low(iList.StateProvName),1,0,0,0)
; Update the foreign key in the Stateprov list.
Calculate iList.StateProvName as iStateProvList.StateProvName
Calculate iList.Stateprov_fkey as iStateProvList.Stateprov_pkey
Calculate iList.CountryName as iStateProvList.CountryName
Calculate iList.Country_fkey as iStateProvList.Country_fkey
Else
; If not, ask the user whether or not they want to add the country.
Process event and continue (Discard event)
No/Yes message [sys(85)] (Icon) {The State/Province Name entered does not match a State/Province in the state/provinces list.////Do you wish to add [$cobj.$contents] as a State/Province in the country [iList.CountryName]?}
If flag true
; Insert a record in the StateProv table.
Do method retDefinedList ('tStateprov') Returns List
If List.$colcount=0
Calculate FlagOK as kFalse
Else
Do List.$smartlist.$assign(kTrue)
Do List.$add()
Do List.$line.$assign($ref.$linecount)
Calculate List.StateProvName as $cobj.$contents
Calculate List.Country_fkey as iList.Country_fkey
Do List.$dowork() Returns FlagOK
If FlagOK
; Rebuild the state/prov list.
Do iStateProvList.$getAllRecords() Returns FlagOK
If FlagOK
; Make sure the CountryName entered by the user matches an entry in the Countries list.
If iStateProvList.$search(low($ref.StateProvName)=low(iList.StateProvName),1,0,0,0)
; Update the foreign key in the Stateprov list.
Calculate iList.StateProvName as iStateProvList.StateProvName
Calculate iList.Stateprov_fkey as iStateProvList.Stateprov_pkey
Calculate iList.CountryName as iStateProvList.CountryName
Calculate iList.Country_fkey as iStateProvList.Country_pkey
End If
End If
End If
End If
End If
End If
Quit method FlagOK
On evBefore
; Find the matching country in the countries list.
Do iStateProvList.$search($ref.Stateprov_pkey=iList.Stateprov_fkey,1,0,0,0)
On evClick ;; Event Parameters - pRow ( Itemreference )
; Set the foreign key to match the selected country's primary key.
Process event and continue
Calculate iList.Stateprov_fkey as pRow.Stateprov_pkey
Calculate iList.StateProvName as pRow.StateProvName
Calculate iList.Country_fkey as pRow.Country_fkey
Calculate iList.CountryName as pRow.CountryName
Do $cinst.$redraw()
On evAfter
Do method event_evAfter
On evExtend
Process event and continue
; Copy the previous row value to the new row.
Do iList.$assignrow(iList.[iList.$line-1],kTrue) ;; (Match column name)
; Clear the primary key and the TownCityName columns.
Calculate iList.Towncity_pkey as #NULL
Calculate iList.TownCityName as ''
; Redraw the window to display the copied values.
Do $cinst.$redraw()
In order to open the wTowncityList window we need to add a menu line to the menu.
; Find and open an instance of the state/prov list window class.
Calculate ClassName as 'wTowncityList'
Do $clib.$windows.$findname(ClassName) Returns rClass
If isnull(rClass)
OK message [sys(85)] (Icon) {Unable to find the window class '[ClassName]'.}
Else
Do rClass.$openonce('*') Returns rWin
If isnull(rWin)
OK message [sys(85)] (Icon) {Unable to open an instance of the window class [ClassName].}
End If
End If
Quit method rWin
We are ready to test the wTowncityList window.
Well that wraps up the $extraquerytext, and how to create a user interface for setting the foreign keys when the user is adding/editing records.
tutorial. I hope that this tutorial has helped you understand foreign key relationships and how to implement them in Omnis Studio using query classes,You now have the basics but there's much, much, more to learn! Omnis Studio is a powerful development tool with many more features.
In the
tutorial you will continue developing the application and do the following:The StudioTips Members. The cost of membership is well worth the benefits of being a member.
tutorial is only available to . Omnis Studio developers are encouraged to becomeVisit www.studiotips.net to find out more and to become a . Your support is greatly appreciated!
If you this tutorial has been helpful please send an email to (doug@vencor.ca) It's always encouraging to hear from developers who have benefited from the Studio 102 tutorial. Be sure to include any suggestions for improvements or additional topics you would like to see covered.
Happy coding!
Doug Kuyvenhoven