602 lines
20 KiB
HTML
602 lines
20 KiB
HTML
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
|
|
<html xmlns="http://www.w3.org/1999/xhtml">
|
|
<head>
|
|
<title>GT::SQL::Relation - manage multiple table joins</title>
|
|
<link rev="made" href="mailto:root@penguin.office.gossamer-threads.com" />
|
|
|
|
<style type="text/css">
|
|
/* $MVD$:fontset("Untitled Font Set 1","ARIEL","HELVETICA","HELV","SANSERIF") */
|
|
/* $MVD$:fontset("Arial","Arial") */
|
|
/* $MVD$:fontset("Arial Black","Arial Black") */
|
|
/* $MVD$:fontset("Algerian","Algerian") */
|
|
|
|
|
|
body {
|
|
background-color: white;
|
|
font-family: Verdana, Arial, sans-serif;
|
|
font-size: small;
|
|
color: black;
|
|
}
|
|
|
|
|
|
p {
|
|
background-color : white;
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
h1 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : medium;
|
|
background-color : white;
|
|
color : maroon;
|
|
}
|
|
|
|
|
|
h2 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : medium;
|
|
font-weight : bold;
|
|
color : blue;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
h3 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : medium;
|
|
color : black;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
h4 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : small;
|
|
color : maroon;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
h5 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : small;
|
|
color : blue;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
h6 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : small;
|
|
color : black;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
ul {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
ol {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
dl {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
li {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
th {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
td {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
dl {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
dd {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
dt {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
code {
|
|
font-family : Courier;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
|
|
pre {
|
|
font-family : Courier;
|
|
font-size : small;
|
|
color : black;
|
|
}
|
|
|
|
.mvd-H1 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : bold;
|
|
font-size : 14.0pt;
|
|
background-color : transparent;
|
|
background-image : none;
|
|
color : maroon;
|
|
}
|
|
|
|
|
|
.mvd-H2 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : 12.0pt;
|
|
color : blue;
|
|
}
|
|
|
|
|
|
p.indent {
|
|
font-family : "Verdana, Arial, sans-serif";
|
|
list-style-type : circle;
|
|
list-style-position : inside;
|
|
color : black;
|
|
margin-left : 16.0pt;
|
|
}
|
|
|
|
|
|
.mvd-P-indent {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
list-style-type : circle;
|
|
list-style-position : inside;
|
|
color : black;
|
|
margin-left : 16.0pt;
|
|
}
|
|
|
|
|
|
pre.programlisting {
|
|
font-size : 9.0pt;
|
|
list-style-type : disc;
|
|
margin-left : 16.0pt;
|
|
margin-top : -14.0pt;
|
|
}
|
|
|
|
|
|
.mvd-PRE-programlisting {
|
|
font-size : 9.0pt;
|
|
list-style-type : disc;
|
|
margin-left : 16.0pt;
|
|
margin-top : -14.0pt;
|
|
}
|
|
|
|
|
|
.mvd-PRE {
|
|
font-size : 9.0pt;
|
|
}
|
|
|
|
|
|
p.note {
|
|
margin-left : 28.0pt;
|
|
}
|
|
|
|
|
|
.mvd-P-note {
|
|
margin-left : 28.0pt;
|
|
}
|
|
|
|
|
|
.mvd-H4 {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-weight : normal;
|
|
font-size : 9.0pt;
|
|
color : black;
|
|
margin-left : 6.0pt;
|
|
margin-top : -14.0pt;
|
|
}
|
|
|
|
|
|
.mvd-P {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
font-size : 10.0pt;
|
|
color : black;
|
|
}
|
|
|
|
.mvd-BODY {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
background-color : white;
|
|
}
|
|
|
|
|
|
p.indentnobullet {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
list-style-type : none;
|
|
}
|
|
|
|
|
|
.mvd-P-indentnobullet {
|
|
font-family : Verdana, Arial, sans-serif;
|
|
list-style-type : none;
|
|
}
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
<body style="background-color: white">
|
|
|
|
<p><a name="__index__"></a></p>
|
|
<!-- INDEX BEGIN -->
|
|
|
|
<ul>
|
|
|
|
<li><a href="#name">NAME</a></li>
|
|
<li><a href="#synopsis">SYNOPSIS</a></li>
|
|
<li><a href="#description">DESCRIPTION</a></li>
|
|
<ul>
|
|
|
|
<li><a href="#how_it_works">How it works</a></li>
|
|
<li><a href="#select_statements">SELECT statements</a></li>
|
|
<li><a href="#select_options">SELECT options</a></li>
|
|
<li><a href="#listing_the_relation_columns">Listing the relation columns</a></li>
|
|
<li><a href="#relation_primary_key">Relation primary key</a></li>
|
|
<li><a href="#foreign_keys_management">Foreign keys management</a></li>
|
|
<li><a href="#inserting_data">Inserting data</a></li>
|
|
<li><a href="#deleting_data">Deleting data</a></li>
|
|
<li><a href="#updating_records">Updating records</a></li>
|
|
<li><a href="#selecting_records">Selecting Records</a></li>
|
|
</ul>
|
|
|
|
<li><a href="#copyright">COPYRIGHT</a></li>
|
|
<li><a href="#version">VERSION</a></li>
|
|
</ul>
|
|
<!-- INDEX END -->
|
|
|
|
<hr />
|
|
<p>
|
|
</p>
|
|
<h1><a name="name">NAME</a></h1>
|
|
<p>GT::SQL::Relation - manage multiple table joins</p>
|
|
<p>
|
|
</p>
|
|
<hr />
|
|
<h1><a name="synopsis">SYNOPSIS</a></h1>
|
|
<pre>
|
|
my $relation = $DB->table('Company', 'Employees');
|
|
my $sth = $relation->select( {
|
|
Company.Name => 'Gossamer Threads',
|
|
Employees.Name => 'Alex Krohn'
|
|
}, ['Employees.Salary', 'Company.City'] );
|
|
my ($salary, $city) = $sth->fetchrow_array;
|
|
print "Alex works in $city and earns $salary!\n";</pre>
|
|
<p>
|
|
</p>
|
|
<hr />
|
|
<h1><a name="description">DESCRIPTION</a></h1>
|
|
<p>This module aims at emulating a set of tables that are related to each other
|
|
via the use of foreign keys just as if it was one big table.</p>
|
|
<p>The module interface should be as compatible as possible with GT::SQL::Table,
|
|
thus you should be familiar with GT::SQL::Table before even reading this.</p>
|
|
<p>This documentation explains the differences between GT::SQL::Relation and
|
|
GT::SQL::Table and how the module internally works as well.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="how_it_works">How it works</a></h2>
|
|
<p>GT::SQL supports the concept of foreign keys (also known as external
|
|
references). Basically, two tables that are linked together using external
|
|
references can look like that:</p>
|
|
<pre>
|
|
.-------------. .---------.
|
|
| EMPLOYEE | | COMPANY |
|
|
`-------------' `---------'
|
|
| ID | .--->ID |
|
|
| COMPANY_ID ----' | NAME |
|
|
| NAME | `---------'
|
|
| SALARY |
|
|
`-------------'</pre>
|
|
<p>In this example, the COMPANY_ID attribute relates the fact that a an EMPLOYEE
|
|
belongs to such or such COMPANY.</p>
|
|
<p>Utilizing a Relation object can make these tables look like that:</p>
|
|
<pre>
|
|
.----------------------.
|
|
| EMPLOYEE-COMPANY |
|
|
`----------------------'
|
|
| EMPLOYEE.ID |
|
|
| EMPLOYEE.COMPANY_ID |
|
|
| EMPLOYEE.NAME |
|
|
| EMPLOYEE.SALARY |
|
|
| COMPANY.NAME |
|
|
`----------------------'</pre>
|
|
<p>The first thing that can be seen from there is that COMPANY.ID has disappeared
|
|
from this ``Virtual'' table.</p>
|
|
<p>Indeed, as for a given ``joined'' record this value must be the same in both
|
|
tables, representing the values twice would have been a useless source of
|
|
confusion.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="select_statements">SELECT statements</a></h2>
|
|
<p>Selecting from a Relation object is pretty simple using the GT::SQL module. As
|
|
the interface is (almost) the same as <a href="glist.cgi?do=admin_gtdoc&topic=/GT/SQL/Table.html">the GT::SQL::Table manpage</a>, the GT::SQL wrapper
|
|
returns Table or Relation objects depending on the arguments that are passed to
|
|
table.</p>
|
|
<pre>
|
|
# This gives me a GT::SQL::Table object for
|
|
# the EMPLOYEE table.
|
|
my $emp = $sql->table('EMPLOYEE');</pre>
|
|
<pre>
|
|
# This gives me a GT::SQL::Relation object for
|
|
# the relation EMPLOYEE-COMPANY tables
|
|
my $emp_cmp = $sql->table('EMPLOYEE','COMPANY');</pre>
|
|
<p>From there, performing a select is pretty simple:</p>
|
|
<pre>
|
|
# select all the people from a real cool company
|
|
my $sth = $emp_cmp->select( { COMPANY.NAME => "Gossamer Threads" } )</pre>
|
|
<p>Internally, the generated SQL query would look like:</p>
|
|
<pre>
|
|
SELECT EMPLOYEE.ID, EMPLOYEE.COMPANY_ID, EMPLOYEE.NAME
|
|
EMPLOYEE.SALARY, COMPANY.NAME
|
|
FROM EMPLOYEE, COMPANY
|
|
WHERE COMPANY.NAME = 'Gossamer Threads' AND
|
|
EMPLOYEE.COMPANY_ID = COMPANY.ID</pre>
|
|
<p>Note that the join condition is computed and automatically appended at the end
|
|
of the query, so you do not have to worry about this.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="select_options">SELECT options</a></h2>
|
|
<p>The select options for relation are similar to that of table, you have
|
|
<code>select_options()</code> which will be set for the next query done. Example:</p>
|
|
<pre>
|
|
$relation->select_options("LIMIT 10");</pre>
|
|
<p>This would append 'LIMIT 10' to your next select query. Another useful thing
|
|
is join_on(). <code>join_on()</code> allows you to specify the FK relation for the nextr
|
|
select. This overrides what is in the def files. It is useful for allowing you
|
|
to have one table which will be join differently depending on what you are
|
|
doing. The argument to this are the same as to fk().
|
|
Example:</p>
|
|
<pre>
|
|
$relation->join_on( remote_table => { local_column => remote_column } );</pre>
|
|
<p>The FK relation will be changed to this the next time you call <code>select()</code> but
|
|
then it will be cleared.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="listing_the_relation_columns">Listing the relation columns</a></h2>
|
|
<p>* As previously said, the <code>cols()</code> method when invoked on a GT::SQL::Relation
|
|
object does not return all the columns, removing the duplicate external
|
|
references. So, how does it decides which column to keep and which one to
|
|
return?</p>
|
|
<p>In the EMPLOYEE-COMPANY example we have the constraint
|
|
EMPLOYEE.COMPANY_ID => COMPANY.ID and it keeps COMPANY_ID, i.e. the foreign key
|
|
instead of the key itself.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="relation_primary_key">Relation primary key</a></h2>
|
|
<p>* The <code>pk()</code> method has to return the table primary key. The property of a primary
|
|
key is that it is a non-null unique record identifier. When <code>pk()</code> is invoked on
|
|
a Relation object, this base definition is applied to construct the object
|
|
primary key.</p>
|
|
<p>To find a unique set of fields that makes a good primary key for a Relation
|
|
object, the following, simple algorithm is used:</p>
|
|
<pre>
|
|
. .
|
|
. for each table .
|
|
. if the table is not referenced by another table that .
|
|
. is in the current relation .
|
|
. do .
|
|
. append the current table's primary key fields to .
|
|
. the Relation primary key fields .
|
|
. end-do .
|
|
. end-if .
|
|
. end-for .
|
|
. .</pre>
|
|
<p>This algorithm selects all the tables that represent the ``many'' in one-to-many
|
|
relations, and for all these tables add a list of fields which ensure a record
|
|
uniqueness.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="foreign_keys_management">Foreign keys management</a></h2>
|
|
<p>* When invoked on a GT::SQL::Table object, the <code>fk()</code> method returns a hash which
|
|
has the following general structure:</p>
|
|
<pre>
|
|
{
|
|
target_table_1 => {
|
|
source_col_1 => target_col_1,
|
|
source_col_2 => target_col_2
|
|
},
|
|
target_table_2 => {
|
|
source_col_1 => target_col_1
|
|
}
|
|
}</pre>
|
|
<p>The GT::SQL::Relation module returns a hash which has the same structure. The
|
|
only difference is that it does not returns the external references which are
|
|
managed internally.</p>
|
|
<p>This is done for two reasons: As one field is removed from a Relation table, it
|
|
would not have been very logical to return a structure that point to
|
|
non-existent fields.</p>
|
|
<p>Moreover, these internal references from the ``Relation'' point of view have
|
|
nothing to do with the external world and thus should not be shown.</p>
|
|
<p>(i.e. EMPLOYEE.COMPANY_ID |===> COMPANY.ID would not count in our example)</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="inserting_data">Inserting data</a></h2>
|
|
<p>The interface for inserting data in a Relation is the same as the one that is
|
|
being used for Table. However, because rows are being inserted in a relation
|
|
one-to-many, things internally work a bit differently.</p>
|
|
<p>The Relation <code>insert()</code> method takes an optional argument, which can be
|
|
'complete' or 'abort' (default being complete).</p>
|
|
<p><code>insert()</code> splits the relation columns into separate records that can be inserted
|
|
in a single table. However, some of the records may exist already!</p>
|
|
<p>for example, if we perform:</p>
|
|
<pre>
|
|
$sql = shift; # our GT::SQL object
|
|
$rel = $sql->table(qw/EMPLOYEE COMPANY/);
|
|
$rel->insert({
|
|
'EMPLOYEE.NAME' => $your_name,
|
|
'EMPLOYEE.SALARY' => $big_buck,
|
|
'COMPANY.NAME' => "Gossamer Threads"
|
|
});</pre>
|
|
<p>Obviously the company ``Gossamer Threads'' already exists, but you were not in
|
|
the ``EMPLOYEE'' table. Thus, when 'complete' is specified (it is the default
|
|
option), the program will not complain if a record to insert already exists but
|
|
just warns and continue the insertion work.</p>
|
|
<p>In other words, Gossamer Threads exists already and it will not be inserted
|
|
twice, but the employee will still be inserted and will belong to this company.</p>
|
|
<p>On the other hand, if you specify ``abort'', then no data is inserted if a
|
|
record that has to be inserted would trigger an error in GT::SQL::Table.</p>
|
|
<p>This feature can be useful if you want to insert a relation record assuming
|
|
that none of the entities that you specify should exist.</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="deleting_data">Deleting data</a></h2>
|
|
<p>Deleting data from a Relation object works using the following pattern:</p>
|
|
<pre>
|
|
. .
|
|
. for each row that matches the delete condition .
|
|
. do .
|
|
. split the row in table-based records .
|
|
. for each table that contains foreing keys from the .
|
|
. current relation object .
|
|
. do .
|
|
. delete the record .
|
|
. end-do .
|
|
. .
|
|
. for each table that is being referenced by another .
|
|
. table in the current relation object .
|
|
. do .
|
|
. delete the record unless there exists .
|
|
. some "referencing" data. .
|
|
. end-do .
|
|
. .</pre>
|
|
<p>As I feel that this explanation is probably very confusing, let us see how it
|
|
works using our classical example (The salary column has been removed).</p>
|
|
<pre>
|
|
.-------------------------------------------------------------.
|
|
| EMPLOYEE.ID | COMPANY_ID | EMPLOYEE.NAME | COMPANY.NAME |
|
|
`-------------------------------------------------------------'
|
|
| 1 | 1 | Alex | Gossamer Threads |
|
|
|-------------|------------|---------------|------------------|
|
|
| 2 | 1 | Scott | Gossamer Threads |
|
|
|-------------|------------|---------------|------------------|
|
|
| 3 | 1 | Aki | Gossamer Threads |
|
|
`-------------------------------------------------------------'</pre>
|
|
<p>Now let us say that we do the following:</p>
|
|
<pre>
|
|
# remove all the crazy geeks
|
|
$relation->delete({ 'EMPLOYEE.NAME' => 'Scott' });</pre>
|
|
<p>This will remove ``Scott'' from the EMPLOYEE table, but of course
|
|
Gossamer Threads will not be deleted because there still exists Alex and Aki
|
|
that would reference it.</p>
|
|
<p>Now if we do:</p>
|
|
<pre>
|
|
$relation->delete({ 'COMPANY.NAME' => 'Gossamer Threads' });</pre>
|
|
<p>or even</p>
|
|
<pre>
|
|
my $condition = new GT::SQL::Condition;
|
|
$condition->add(qw/EMPLOYEE.NAME LIKE %/);
|
|
$relation->delete($condition);</pre>
|
|
<p>Then we have generated a condition that matches all the employees, this means
|
|
that when the last record will be deleted, then the company Gossamer Threads
|
|
will have no more employees and therefore will be deleted.</p>
|
|
<p>(Yeah, well, this is for the purpose of this example, of course this will never
|
|
happen in real life :) )</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="updating_records">Updating records</a></h2>
|
|
<p>Currently, there exists a limitation on updating records in a Relation, which
|
|
is that only the records that represent the ``many'' part of the Relation are
|
|
updated.</p>
|
|
<p>The way it proceeds to perform the update is pretty simple:</p>
|
|
<pre>
|
|
. .
|
|
. for each row that matches the update condition .
|
|
. do .
|
|
. split the row in table-based records .
|
|
. for each table that contains foreing keys from the .
|
|
. current relation object .
|
|
. do .
|
|
. update the record .
|
|
. end-do .
|
|
. .</pre>
|
|
<p>That means that this will work:</p>
|
|
<pre>
|
|
# SALARY being a property of EMPLOYEE, it will be updated
|
|
# because EMPLOYEE references COMPANY and therefore is a
|
|
# "many"
|
|
$relation->update({ SALARY => $big_bill },
|
|
{ 'COMPANY.NAME' => 'Gossamer Threads' });</pre>
|
|
<pre>
|
|
# nope, you cannot use Relation to update the COMPANY table that
|
|
# way, this will not do anything.
|
|
$relation->update({ 'COMPANY.NAME' => 'New_Name' },
|
|
{ 'COMPANY.NAME' => 'Gossamer Threads' });</pre>
|
|
<p>Who would like to change such a great name anyway ?</p>
|
|
<p>
|
|
</p>
|
|
<h2><a name="selecting_records">Selecting Records</a></h2>
|
|
<p>Select behaves exactly like <a href="glist.cgi?do=admin_gtdoc&topic=/GT/SQL/Table.html">the GT::SQL::Table manpage</a> select. The only difference is
|
|
the ability to specify LEFT JOINs. For instance, if you want to see a list of
|
|
Employees who don't belong to a company, you can do:</p>
|
|
<pre>
|
|
my $relation = $DB->table('Employees', 'Company');
|
|
my $cond = GT::SQL::Condition->new('Company.ID', 'IS', \'NULL');
|
|
my $sth = $relation->select('left_join', $cond);</pre>
|
|
<p>The order of tables specified in the relation constructor is important!</p>
|
|
<p>In selecting columns, calling functions utilizing fully qualified column names
|
|
will cause GT::SQL::Relation to fail. Simply turn the values into references
|
|
like below.</p>
|
|
<pre>
|
|
my $sth = $relation->select("MIN(Company.ID)"); # will fail</pre>
|
|
<pre>
|
|
my $sth = $relation->select(\"MIN(Company.ID)"); # will work</pre>
|
|
<p>
|
|
</p>
|
|
<hr />
|
|
<h1><a name="copyright">COPYRIGHT</a></h1>
|
|
<p>Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
|
|
<a href="http://www.gossamer-threads.com/">http://www.gossamer-threads.com/</a></p>
|
|
<p>
|
|
</p>
|
|
<hr />
|
|
<h1><a name="version">VERSION</a></h1>
|
|
<p>Revision: $Id: Relation.pm,v 1.102 2004/08/28 03:53:43 jagerman Exp $</p>
|
|
|
|
</body>
|
|
|
|
</html>
|