discourse-legacysite-perl/site/glist/templates/help/GT/SQL/Relation.html
2024-06-17 22:24:05 +10:00

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-&gt;table('Company', 'Employees');
my $sth = $relation-&gt;select( {
Company.Name =&gt; 'Gossamer Threads',
Employees.Name =&gt; 'Alex Krohn'
}, ['Employees.Salary', 'Company.City'] );
my ($salary, $city) = $sth-&gt;fetchrow_array;
print &quot;Alex works in $city and earns $salary!\n&quot;;</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 | .---&gt;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-&gt;table('EMPLOYEE');</pre>
<pre>
# This gives me a GT::SQL::Relation object for
# the relation EMPLOYEE-COMPANY tables
my $emp_cmp = $sql-&gt;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-&gt;select( { COMPANY.NAME =&gt; &quot;Gossamer Threads&quot; } )</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-&gt;select_options(&quot;LIMIT 10&quot;);</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-&gt;join_on( remote_table =&gt; { local_column =&gt; 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 =&gt; 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 =&gt; {
source_col_1 =&gt; target_col_1,
source_col_2 =&gt; target_col_2
},
target_table_2 =&gt; {
source_col_1 =&gt; 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 |===&gt; 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-&gt;table(qw/EMPLOYEE COMPANY/);
$rel-&gt;insert({
'EMPLOYEE.NAME' =&gt; $your_name,
'EMPLOYEE.SALARY' =&gt; $big_buck,
'COMPANY.NAME' =&gt; &quot;Gossamer Threads&quot;
});</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 &quot;referencing&quot; 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-&gt;delete({ 'EMPLOYEE.NAME' =&gt; '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-&gt;delete({ 'COMPANY.NAME' =&gt; 'Gossamer Threads' });</pre>
<p>or even</p>
<pre>
my $condition = new GT::SQL::Condition;
$condition-&gt;add(qw/EMPLOYEE.NAME LIKE %/);
$relation-&gt;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
# &quot;many&quot;
$relation-&gt;update({ SALARY =&gt; $big_bill },
{ 'COMPANY.NAME' =&gt; 'Gossamer Threads' });</pre>
<pre>
# nope, you cannot use Relation to update the COMPANY table that
# way, this will not do anything.
$relation-&gt;update({ 'COMPANY.NAME' =&gt; 'New_Name' },
{ 'COMPANY.NAME' =&gt; '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-&gt;table('Employees', 'Company');
my $cond = GT::SQL::Condition-&gt;new('Company.ID', 'IS', \'NULL');
my $sth = $relation-&gt;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-&gt;select(&quot;MIN(Company.ID)&quot;); # will fail</pre>
<pre>
my $sth = $relation-&gt;select(\&quot;MIN(Company.ID)&quot;); # 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>