Blame | Letzte Änderung | Log anzeigen | RSS feed
<com:TContent ID="body"><h1>Complex Properties</h1><p>In a relational database, one table will often refer to another. Likewise,some of your business objects may include another object or list of objects.Types that nest other types are called "complex types". You may not want astatement to return a simple type, but a fully-formed complex type.</p><p>In the database, a related column is usually represented via a 1:1relationship, or a 1:M relationship where the class that holds the complexproperty is from the "many side" of the relationship and the property itselfis from the "one side" of the relationship. The column returned from thedatabase will not be the property we want; it is a key to be used in anotherquery.</p><p>From the framework's perspective, the problem is not so much loading a complextype, but loading each "complex property". To solve this problem, you canspecify in the Result Map a statement to run to load a given property. Inthe following example, the "category" property of the"select-product-result" element is a complex property.</p><com:TTextHighlighter Language="xml" CssClass="source"><resultMap id="select-product-result" class="product"><result property="id" column="PRD_ID"/><result property="description" column="PRD_DESCRIPTION"/><result property="category" column="PRD_CAT_ID" select="selectCategory"/></resultMap><resultMap id="select-category-result" class="category"><result property="id" column="CAT_ID"/><result property="description" column="CAT_DESCRIPTION"/></resultMap><select id="selectProduct" parameterClass="int" resultMap="select-product-result">select * from PRODUCT where PRD_ID = #value#</select><select id="selectCategory" parameterClass="int" resultMap="select-category-result">select * from CATEGORY where CAT_ID = #value#</select></com:TTextHighlighter><p>In the above example, the framework will use the "selectCategory"statement to populate the "category" property. The value of each category ispassed to the "selectCategory" statement, and the object returned is set tothe category property. When the process completes, each Product instance willhave the the appropriate category object instance set.</p><h1>Avoiding N+1 Selects (1:1)</h1><p>A problem with the above example may be that whenever you load aProduct, two statements execute: one for the Product and one for the Category.For a single Product, this issue may seem trivial. But if you load 10products, then 11 statements execute. For 100 Products, instead of onestatement product statement executing, a total of 101 statements execute. Thenumber of statements executing for the above example will always beN+1: 100+1=101.</p><p>One way to mitigate the problem is to cache the "selectCategory" statement.We might have a hundred products, but there might only be five categories.Instead of running a SQL query or stored procedure, the framework will returnthe category object from it cache. A 101 statements would still run, but theywould not be hitting the database. See <a href="?page=Manual.CacheModels">Cache Models</a>more details about caches.</p><p>Another solution is to use a standard SQL join to return the columns you needfrom the another table. A join can bring all the columns we need over from thedatabase in a single query. When you have a nested object, you can referencenested properties using a dotted notation, like "category.description".</p><p>The following example solves the same problem as the previousexample, but uses a join instead of nested properties.</p><com:TTextHighlighter Language="xml" CssClass="source"><resultMap id="select-product-result" class="product"><result property="id" column="PRD_ID"/><result property="description" column="PRD_DESCRIPTION"/><result property="category" resultMapping="Category.CategoryResult" /></resultMap><statement id="selectProduct" parameterClass="int" resultMap="select-product-result">select *from PRODUCT, CATEGORYwhere PRD_CAT_ID=CAT_IDand PRD_ID = #value#</statement></com:TTextHighlighter><div class="info"><b class="tip">Lazy Loading vs. Joins (1:1):</b><p>It's important to note that using a join is not always better. If you are in asituation where it is rare to access the related object (e.g. the categoryproperty of the Product class) then it might actually be faster to avoid thejoin and the unnecessary loading of all category properties. This isespecially true for database designs that involve outer joins or nullableand/or non-indexed columns. In these situations it might be better to use thesub-select solution with lazy loading enabled. The general rule of thumb is:use the join if you're more likely going to access the associated propertiesthan not. Otherwise, only use it if lazy loading is not an option.</p><p>If you're having trouble deciding which way to go, don't worry. No matterwhich way you go, you can always change it without impacting your applicationsource code. The two examples above result inexactly the same object graph and are loaded using the exact same method callfrom the application. The only consideration is that if you were to enablecaching, then the using the separate select (not the join) solution couldresult in a cached instance being returned. But more often than not, thatwon't cause a problem (your application shouldn't be dependent on instancelevel equality i.e. "<tt>===</tt>").</p></div><h1>Complex Collection Properties</h1><p>It is also possible to load properties that represent lists of complexobjects. In the database the data would be represented by a M:M relationship,or a 1:M relationship where the class containing the list is on the "oneside" of the relationship and the objects in the list are on the "manyside"". To load a <tt>TList</tt> of objects, there is no change to the statement(see example above). The only difference required to cause the SQLMapDataMapper framework to load the property as a <tt>TList</tt> is that the propertyon the business object must be of type <tt>TList</tt>. For example, if a Categoryhas a <tt>TList</tt> of Product instances, the mapping would look like this(assuming Category has a property called "ProductList" of <tt>TList</tt>.):</p><com:TTextHighlighter Language="xml" CssClass="source"><resultMap id="select-category-result" class="Category"><result property="Id" column="CAT_ID"/><result property="Description" column="CAT_DESCRIPTION"/><result property="ProductList" column="CAT_ID" select="selectProductsByCatId"/></resultMap><resultMap id="select-product-result" class="Product"><result property="Id" column="PRD_ID"/><result property="Description" column="PRD_DESCRIPTION"/></resultMap><statement id="selectCategory" parameterClass="int"resultMap="select-category-result">select * from CATEGORY where CAT_ID = #value#</statement><statement id="selectProductsByCatId" parameterClass="int"resultMap="select-product-result">select * from PRODUCT where PRD_CAT_ID = #value#</statement></com:TTextHighlighter><h1>Avoiding N+1 Select Lists (1:M and M:N)</h1><p>This is similar to the 1:1 situation above, but is of even greater concern dueto the potentially large amount of data involved. The problem with thesolution above is that whenever you load a Category, two SQL statements areactually being run (one for the Category and one for the list of associatedProducts). This problem seems trivial when loading a single Category, but ifyou were to run a query that loaded ten (10) Categories, a separate querywould be run for each Category to load its associated list of Products. Thisresults in eleven (11) queries total: one for the list of Categories and onefor each Category returned to load each related list of Products (N+1 or inthis case 10+1=11). To make this situation worse, we're dealing withpotentially large lists of data.</p><com:TTextHighlighter Language="xml" CssClass="source"><resultMap id="select-category-result" class="Category"><result property="Id" column="CAT_ID"/><result property="Description" column="CAT_DESCRIPTION"/><result property="ProductList" column="CAT_ID" select="selectProductsByCatId"/></resultMap><resultMap id="select-product-result" class="Product"><result property="Id" column="PRD_ID"/><result property="Description" column="PRD_DESCRIPTION"/></resultMap><!-- This statement executes 1 time --><statement id="selectCategory" parameterClass="int"resultMap="select-category-result">select * from CATEGORY where CAT_ID = #value#</statement><!-- This statement executes N times (once for each category returned above)and returns a list of Products (1:M) --><statement id="selectProductsByCatId" parameterClass="int"resultMap="select-product-result">select * from PRODUCT where PRD_CAT_ID = #value#</statement></com:TTextHighlighter><h2>1:N and M:N Solution?</h2><p>One way to avoid multiple SQL queries is to use Joins in the querytogether with the <tt>groupBy</tt> attribute in <tt><resultMap></tt> andthe <tt>resultMapping</tt> attribute of <tt><result></tt>.The following example selects all the categories and the correspondingproducts using 1 query.</p><com:TTextHighlighter Language="xml" CssClass="source"><resultMap id="select-category-result" class="Category" groupBy="CAT_ID"><result property="Id" column="CAT_ID"/><result property="Description" column="CAT_DESCRIPTION"/><result property="ProductList" resultMapping="select-product-result"/></resultMap><resultMap id="select-product-result" class="Product"><result property="Id" column="PRD_ID"/><result property="Description" column="PRD_DESCRIPTION"/></resultMap><!-- This statement executes 1 time --><statement id="selectCategory" parameterClass="int"resultMap="select-category-result">selectCATEGORY.CAT_ID as CAT_ID,CATEGORY.CAT_DESCRIPTION as CAT_DESCRIPTION,PRODUCT.PRD_ID as PRD_ID,PRODUCT.PRD_DESCRIPTION as PRD_DESCRIPTIONfrom CATEGORYleft join PRODUCT onPRODUCT.PRD_CAT_ID = CATEGORY.CAT_ID</statement></com:TTextHighlighter><p>In the above example, the <tt>groupBy</tt> attribute is setto the column of that specifies the Category ID. All the rowswith the same <tt>CAT_ID</tt> will be considered as a collectionfor the <tt>ProductList</tt> property.</p><div class="info"><b>Lazy Loading vs. Joins (1:M and M:N):</b><p>As with the 1:1 situation described previously, it's important to note thatusing a join is not always better. This is even more true for collectionproperties than it was for individual value properties due to the greateramount of data. If you are in a situation where it is rare to access therelated object (e.g. the ProductList property of the Category class) then itmight actually be faster to avoid the join and the unnecessary loading of thelist of products. This is especially true for database designs that involveouter joins or nullable and/or non-indexed columns. In these situations itmight be better to use the sub-select solution with the lazy loading. Thegeneral rule of thumb is: use the join if you're more likely going to accessthe associated properties than not. Otherwise, only use it if lazy loading isnot an option.</p><p>As mentioned earlier, if you're having trouble deciding which way to go, don'tworry. No matter which way you go, you can always change it without impactingyour PHP code. The two examples above would result in exactly the same objectgraph and are loaded using the exact same method call. The only considerationis that if you were to enable caching, then the using the separate select (notthe join) solution could result in a cached instance being returned. But moreoften than not, that won't cause a problem (your application should not bedependent on instance level equality i.e. "<tt>===</tt>").</p></div></com:TContent>