| 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><resultMap></tt> and
|
|
|
187 |
the <tt>resultMapping</tt> attribute of <tt><result></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>
|