Bidirectional many-to-one/one-to-many
This is the most common type of association, I have used Product and ProductReview tables to depict how it works and how it can be mapped.
In our case each product (one) can have multiple reviews (many). On ProductReview side association can be mapped like this:
References(x => x.Product, "ProductID").FetchType.Join();
<many-to-one fetch="join" name="Product" column="ProductID" />
1: var review = session.Get<ProductReview>("1");
2: var productName = review.Product.Name;
SELECT ... FROM ProductReview pr left outer join Product p on pr.ProductID=p.ProductID WHERE ...
SELECT ... FROM ProductReview pr WHERE ...
SELECT ... FROM Product p WHERE ...
In general you have to determine in each case which FetchType is more beneficial for you.
Now, lets check Product side, this is many side of one-to-many association so Product has a collection of reviews, I have chosen to use ISet:
1: HasMany(x => x.ProductReview)
2: .KeyColumnNames.Add("ProductID")
3: .AsSet()
4: .Inverse()
5: .Cascade.All();
1: <set name="ProductReview" inverse="true" cascade="all">
2: <key column="ProductID" />
3: <one-to-many class="AdventureWorksPlayground.Domain.Production.ProductReview, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
4: </set>
- inverse="true" - it tells NHibernate that other side of this association is a parent. I know that it sounds other way round but that's how it is. ProductReview table has foreign key (and ProductID column) therefore ProductReview controls the association.
What are the implications? In above example review.Product has to be set correctly, as this the property which NHibernate will check to figure our what product is associated with the review. It will ignore collection of reviews on product! - cascade="all" - it tells NHibernate that all events (like save, update, delete) should be propagated down. Calling session.SaveOrUpdate(product) will save (or update) the product itself but also the same event will be applied to all depending objects.
1: var product = new Product
2: {
3: Name = "Bike",
4: SellStartDate = DateTime.Today
5: };
6:
7: product.ProductReview.Add(new ProductReview
8: {
9: Product = product,
10: Rating = 4,
11: ReviewerName = "Bob",
12: ReviewDate = DateTime.Today
13: });
14:
15: product.ProductReview.Add(new ProductReview
16: {
17: Product = product,
18: Rating = 2,
19: ReviewerName = "John",
20: ReviewDate = DateTime.Today
21: });
22:
23:
24: session.SaveOrUpdate(product);
Can you see a potential problem here? Each ProductReview knows about its Product, and thanks to cascade="all" everything is configured correctly but still you may end up with just one review in database ... why? I'm using ISet here, so it guarantees that I have only unique objects in the collection. Most of the people know that NHibernate classes should have Equals() and GetHashCode() methods overridden. It is useful when you want to check that two objects represent the same row in a database. People use primary id column in Equals() implementation, primary id is unique so it fits perfectly isn't it? It does if primary key is defined, and in above example, objects are saved in a last line, before that, they don't have any primary id. That is a reason for using different data to determine equality.
Bidirectional many-to-many association
For this association I have used Product, ProductProductPhoto (link) and ProductPhoto tables. Each product can have multiple photos, but each photo can also be associated with multiple products. ProductProductPhoto is just a link table and doesn't have any representation as a separate class. On both sides mapping looks very similarly.
Product side:
1: HasManyToMany(x => x.Photos)
2: .AsBag()
3: .WithTableName("Production.ProductProductPhoto")
4: .WithParentKeyColumn("ProductID")
5: .WithChildKeyColumn("ProductPhotoID")
6: .Cascade.All();
1: <bag name="Photos" cascade="all" table="Production.ProductProductPhoto">
2: <key column="ProductID" />
3: <many-to-many column="ProductPhotoID" class="AdventureWorksPlayground.Domain.Production.ProductPhoto, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
4: </bag>
1: HasManyToMany(x => x.Products)
2: .AsBag()
3: .WithTableName("Production.ProductProductPhoto")
4: .WithParentKeyColumn("ProductPhotoID")
5: .WithChildKeyColumn("ProductID")
6: .Inverse();
1: <bag name="Products" inverse="true" table="Production.ProductProductPhoto">
2: <key column="ProductPhotoID" />
3: <many-to-many column="ProductID" class="AdventureWorksPlayground.Domain.Production.Product, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
4: </bag>
This is fairly straightforward example but unfortunately not very common. In typical case link table has some additional data (like ProductDocument which has ModifiedDate column) and those additional data forces us to use different approach. Among NHibernate best practices you can find general guideline:
Good usecases for a real many-to-many associations are rare. Most of the time you need additional information stored in the "link table". In this case, it is much better to use two one-to-many associations to an intermediate link class. In fact, we think that most associations are one-to-many and many-to-one, you should be careful when using any other association style and ask yourself if it is really necessary.So, in fact, for tables Product, Document and ProductDocument, we have to create three classes and three mappings. Both Product and Document have a link to each other through ProductDocument object. Interesting part is ProductDocument which has composite primary id (two columns) which can be mapped in a following way:
1: public class ProductDocumentMap : ClassMap<ProductDocument>
2: {
3: public ProductDocumentMap()
4: {
5: UseCompositeId()
6: .WithKeyReference(x => x.Product, "ProductID")
7: .WithKeyReference(x => x.Document, "DocumentID");
8:
9: Map(x => x.ModifiedDate).Not.Nullable();
10: }
11: }
1: <class name="ProductDocument" table="Production.ProductDocument" xmlns="urn:nhibernate-mapping-2.2">
2: <composite-id>
3: <key-many-to-one class="AdventureWorksPlayground.Domain.Production.Product, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Product" column="ProductID" />
4: <key-many-to-one class="AdventureWorksPlayground.Domain.Production.Document, AdventureWorksPlayground, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" name="Document" column="DocumentID" />
5: </composite-id>
6: <property name="ModifiedDate" column="ModifiedDate" not-null="true" type="DateTime">
7: <column name="ModifiedDate" />
8: </property>
9: </class>
1: var product = CreateNewProduct();
2: var photo1 = CreateNewPhoto();
3: var photo2 = CreateNewPhoto();
4:
5: product.Photos.Add(photo1);
6: product.Photos.Add(photo2);
7:
8: // we don't have to save photos because of Cascade.SaveUpdate()
9: // INSERT INTO [Production.Product]
10: // INSERT INTO [Production.ProductPhoto]
11: // INSERT INTO [Production.ProductPhoto]
12: // INSERT INTO [Production.ProductProductPhoto]
13: // INSERT INTO [Production.ProductProductPhoto]
14: session.SaveOrUpdate(product);
No comments:
Post a Comment
Note: only a member of this blog may post a comment.