Subversion-Projekte lars-tiefland.prado

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 1
<com:TContent ID="body">
2
<h1>Complex Properties</h1>
3
<p>In a relational database, one table will often refer to another. Likewise,
4
some of your business objects may include another object or list of objects.
5
Types that nest other types are called "complex types". You may not want a
6
statement to return a simple type, but a fully-formed complex type.</p>
7
 
8
<p>In the database, a related column is usually represented via a 1:1
9
relationship, or a 1:M relationship where the class that holds the complex
10
property is from the "many side" of the relationship and the property itself
11
is from the "one side" of the relationship. The column returned from the
12
database will not be the property we want; it is a key to be used in another
13
query.</p>
14
 
15
<p>From the framework's perspective, the problem is not so much loading a complex
16
type, but loading each "complex property". To solve this problem, you can
17
specify in the Result Map a statement to run to load a given property. In
18
the following example, the "category" property of the
19
"select-product-result" element is a complex property.</p>
20
 
21
<com:TTextHighlighter Language="xml" CssClass="source">
22
<resultMap id="select-product-result" class="product">
23
    <result property="id" column="PRD_ID"/>
24
    <result property="description" column="PRD_DESCRIPTION"/>
25
    <result property="category" column="PRD_CAT_ID" select="selectCategory"/>
26
</resultMap>
27
 
28
<resultMap id="select-category-result" class="category">
29
    <result property="id" column="CAT_ID"/>
30
    <result property="description" column="CAT_DESCRIPTION"/>
31
</resultMap>
32
 
33
<select id="selectProduct" parameterClass="int" resultMap="select-product-result">
34
   select * from PRODUCT where PRD_ID = #value#
35
</select>
36
 
37
<select id="selectCategory" parameterClass="int" resultMap="select-category-result">
38
   select * from CATEGORY where CAT_ID = #value#
39
</select>
40
</com:TTextHighlighter>
41
 
42
<p>In the above example, the framework will use the "selectCategory"
43
statement to populate the "category" property. The value of each category is
44
passed to the "selectCategory" statement, and the object returned is set to
45
the category property. When the process completes, each Product instance will
46
have the the appropriate category object instance set.</p>
47
 
48
<h1>Avoiding N+1 Selects (1:1)</h1>
49
<p>A problem with the above example may be that whenever you load a
50
Product, two statements execute: one for the Product and one for the Category.
51
For a single Product, this issue may seem trivial. But if you load 10
52
products, then 11 statements execute. For 100 Products, instead of one
53
statement product statement executing, a total of 101 statements execute. The
54
number of statements executing for the above example will always be
55
N+1: 100+1=101.</p>
56
 
57
<p>One way to mitigate the problem is to cache the "selectCategory" statement.
58
We might have a hundred products, but there might only be five categories.
59
Instead of running a SQL query or stored procedure, the framework will return
60
the category object from it cache. A 101 statements would still run, but they
61
would not be hitting the database. See <a href="?page=Manual.CacheModels">Cache Models</a>
62
more details about caches.</p>
63
 
64
<p>Another solution is to use a standard SQL join to return the columns you need
65
from the another table. A join can bring all the columns we need over from the
66
database in a single query. When you have a nested object, you can reference
67
nested properties using a dotted notation, like "category.description".</p>
68
 
69
<p>The following example solves the same problem as the previous
70
example, but uses a join instead of nested properties.</p>
71
 
72
<com:TTextHighlighter Language="xml" CssClass="source">
73
<resultMap id="select-product-result" class="product">
74
    <result property="id" column="PRD_ID"/>
75
    <result property="description" column="PRD_DESCRIPTION"/>
76
    <result property="category" resultMapping="Category.CategoryResult" />
77
</resultMap>
78
 
79
<statement id="selectProduct" parameterClass="int" resultMap="select-product-result">
80
    select *
81
    from PRODUCT, CATEGORY
82
    where PRD_CAT_ID=CAT_ID
83
    and PRD_ID = #value#
84
</statement>
85
</com:TTextHighlighter>
86
 
87
<div class="info"><b class="tip">Lazy Loading vs. Joins (1:1):</b>
88
<p>It's important to note that using a join is not always better. If you are in a
89
situation where it is rare to access the related object (e.g. the category
90
property of the Product class) then it might actually be faster to avoid the
91
join and the unnecessary loading of all category properties. This is
92
especially true for database designs that involve outer joins or nullable
93
and/or non-indexed columns. In these situations it might be better to use the
94
sub-select solution with lazy loading enabled. The general rule of thumb is:
95
use the join if you're more likely going to access the associated properties
96
than not. Otherwise, only use it if lazy loading is not an option.</p>
97
 
98
<p>If you're having trouble deciding which way to go, don't worry. No matter
99
which way you go, you can always change it without impacting your application
100
source code. The two examples above result in
101
exactly the same object graph and are loaded using the exact same method call
102
from the application. The only consideration is that if you were to enable
103
caching, then the using the separate select (not the join) solution could
104
result in a cached instance being returned. But more often than not, that
105
won't cause a problem (your application shouldn't be dependent on instance
106
level equality i.e. "<tt>===</tt>").</p>
107
</div>
108
 
109
<h1>Complex Collection Properties</h1>
110
<p>It is also possible to load properties that represent lists of complex
111
objects. In the database the data would be represented by a M:M relationship,
112
or a 1:M relationship where the class containing the list is on the "one
113
side" of the relationship and the objects in the list are on the "many
114
side"". To load a <tt>TList</tt> of objects, there is no change to the statement
115
(see example above). The only difference required to cause the SQLMap
116
DataMapper framework to load the property as a <tt>TList</tt> is that the property
117
on the business object must be of type <tt>TList</tt>. For example, if a Category
118
has a <tt>TList</tt> of Product instances, the mapping would look like this
119
(assuming Category has a property called "ProductList" of <tt>TList</tt>.):</p>
120
 
121
<com:TTextHighlighter Language="xml" CssClass="source">
122
<resultMap id="select-category-result" class="Category">
123
    <result property="Id" column="CAT_ID"/>
124
    <result property="Description" column="CAT_DESCRIPTION"/>
125
    <result property="ProductList" column="CAT_ID" select="selectProductsByCatId"/>
126
</resultMap>
127
 
128
<resultMap id="select-product-result" class="Product">
129
    <result property="Id" column="PRD_ID"/>
130
    <result property="Description" column="PRD_DESCRIPTION"/>
131
</resultMap>
132
 
133
<statement id="selectCategory" parameterClass="int"
134
            resultMap="select-category-result">
135
    select * from CATEGORY where CAT_ID = #value#
136
</statement>
137
 
138
<statement id="selectProductsByCatId" parameterClass="int"
139
          resultMap="select-product-result">
140
   select * from PRODUCT where PRD_CAT_ID = #value#
141
</statement>
142
</com:TTextHighlighter>
143
 
144
<h1>Avoiding N+1 Select Lists (1:M and M:N)</h1>
145
<p>This is similar to the 1:1 situation above, but is of even greater concern due
146
to the potentially large amount of data involved. The problem with the
147
solution above is that whenever you load a Category, two SQL statements are
148
actually being run (one for the Category and one for the list of associated
149
Products). This problem seems trivial when loading a single Category, but if
150
you were to run a query that loaded ten (10) Categories, a separate query
151
would be run for each Category to load its associated list of Products. This
152
results in eleven (11) queries total: one for the list of Categories and one
153
for each Category returned to load each related list of Products (N+1 or in
154
this case 10+1=11). To make this situation worse, we're dealing with
155
potentially large lists of data.</p>
156
 
157
<com:TTextHighlighter Language="xml" CssClass="source">
158
<resultMap id="select-category-result" class="Category">
159
    <result property="Id" column="CAT_ID"/>
160
    <result property="Description" column="CAT_DESCRIPTION"/>
161
    <result property="ProductList" column="CAT_ID" select="selectProductsByCatId"/>
162
</resultMap>
163
 
164
<resultMap id="select-product-result" class="Product">
165
    <result property="Id" column="PRD_ID"/>
166
    <result property="Description" column="PRD_DESCRIPTION"/>
167
</resultMap>
168
 
169
<!-- This statement executes 1 time -->
170
<statement id="selectCategory" parameterClass="int"
171
            resultMap="select-category-result">
172
    select * from CATEGORY where CAT_ID = #value#
173
</statement>
174
 
175
<!-- This statement executes N times (once for each category returned above)
176
     and returns a list of Products (1:M) -->
177
<statement id="selectProductsByCatId" parameterClass="int"
178
            resultMap="select-product-result">
179
    select * from PRODUCT where PRD_CAT_ID = #value#
180
</statement>
181
</com:TTextHighlighter>
182
 
183
<h2>1:N and M:N Solution?</h2>
184
<p>
185
One way to avoid multiple SQL queries is to use Joins in the query
186
together with the <tt>groupBy</tt> attribute in <tt>&lt;resultMap&gt;</tt> and
187
the <tt>resultMapping</tt> attribute of <tt>&lt;result&gt;</tt>.
188
The following example selects all the categories and the corresponding
189
products using 1 query.
190
</p>
191
 
192
<com:TTextHighlighter Language="xml" CssClass="source">
193
<resultMap id="select-category-result" class="Category" groupBy="CAT_ID">
194
    <result property="Id" column="CAT_ID"/>
195
    <result property="Description" column="CAT_DESCRIPTION"/>
196
    <result property="ProductList" resultMapping="select-product-result"/>
197
</resultMap>
198
 
199
<resultMap id="select-product-result" class="Product">
200
    <result property="Id" column="PRD_ID"/>
201
    <result property="Description" column="PRD_DESCRIPTION"/>
202
</resultMap>
203
 
204
<!-- This statement executes 1 time -->
205
<statement id="selectCategory" parameterClass="int"
206
            resultMap="select-category-result">
207
    select
208
            CATEGORY.CAT_ID as CAT_ID,
209
            CATEGORY.CAT_DESCRIPTION as CAT_DESCRIPTION,
210
            PRODUCT.PRD_ID as PRD_ID,
211
            PRODUCT.PRD_DESCRIPTION as PRD_DESCRIPTION
212
     from CATEGORY
213
        left join PRODUCT on
214
            PRODUCT.PRD_CAT_ID = CATEGORY.CAT_ID
215
</statement>
216
</com:TTextHighlighter>
217
 
218
<p>In the above example, the <tt>groupBy</tt> attribute is set
219
to the column of that specifies the Category ID. All the rows
220
with the same <tt>CAT_ID</tt> will be considered as a collection
221
for the <tt>ProductList</tt> property.</p>
222
 
223
<div class="info"><b>Lazy Loading vs. Joins (1:M and M:N):</b>
224
<p>As with the 1:1 situation described previously, it's important to note that
225
using a join is not always better. This is even more true for collection
226
properties than it was for individual value properties due to the greater
227
amount of data. If you are in a situation where it is rare to access the
228
related object (e.g. the ProductList property of the Category class) then it
229
might actually be faster to avoid the join and the unnecessary loading of the
230
list of products. This is especially true for database designs that involve
231
outer joins or nullable and/or non-indexed columns. In these situations it
232
might be better to use the sub-select solution with the lazy loading. The
233
general rule of thumb is: use the join if you're more likely going to access
234
the associated properties than not. Otherwise, only use it if lazy loading is
235
not an option.</p>
236
 
237
<p>As mentioned earlier, if you're having trouble deciding which way to go, don't
238
worry. No matter which way you go, you can always change it without impacting
239
your PHP code. The two examples above would result in exactly the same object
240
graph and are loaded using the exact same method call. The only consideration
241
is that if you were to enable caching, then the using the separate select (not
242
the join) solution could result in a cached instance being returned. But more
243
often than not, that won't cause a problem (your application should not be
244
dependent on instance level equality i.e. "<tt>===</tt>").</p>
245
</div>
246
 
247
</com:TContent>