NHibernate 3 Beginner's Guide
Rapidly retrieve data from your database into .NET objects
Dr. Gabriel Nicolas Schenker Aaron Cure
BIRMINGHAM - MUMBAI
NHibernate 3 Beginner's Guide
Copyright © 2011 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews. Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the authors, nor Packt Publishing, and its dealers and distributors will be held liable for any damages caused or alleged to be caused directly or indirectly by this book. Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
First published: May 2010 Second edition: August 2011
Production Reference: 1180811
Published by Packt Publishing Ltd. Livery Place 35 Livery Street Birmingham B3 2PB, UK ISBN 978-1-849516-02-0 www.packtpub.com
Cover Image by Vinayak Chittar (
[email protected])
Credits Authors Dr. Gabriel Nicolas Schenker
Project Coordinator Michelle Quadros
Aaron Cure Proofreaders Reviewers Fabio Maulo
Kelly Hutchinson Aaron Nash
José F. Romaniello Indexer Acquisition Editor
Tejal Daruwale
Usha Iyer Production Coordinators Development Editor Neha Mallik
Arvindkumar Gupta Melwyn D'sa Nilesh R. Mohite
Technical Editors Llewellyn F. Rozario Conrad Sardinha Copy Editors Leonard D'Silva Kriti Sharma
Cover Work Arvindkumar Gupta
About the Authors Dr. Gabriel Nicolas Schenker started his career as a physicist. Following his passion
and interest in stars and the universe, he chose to write his PhD thesis in astrophysics. Soon after this, he dedicated all his time to his second passion, writing and architecting software. Gabriel has since been working for over 12 years as an independent consultant, trainer, and mentor, mainly on the .NET platform. He is currently working as a chief software architect in a mid-size US company based in Austin TX, providing software and services to the pharmaceutical industry, as well as to many well-known hospitals and universities throughout the US and in many other countries around the world. Gabriel is passionate about software development and tries to make life for developers easier by providing guidelines and frameworks to reduce friction in the software development process. He has used NHibernate in many different commercial projects, web-based, as well as Windows-based solutions. Gabriel has written many articles and blog posts about different aspects of NHibernate. He is the author behind the well-known NHibernate FAQ blog. Gabriel has also been a reviewer for the bestselling book NHibernate 3 Cookbook. Gabriel is married and a father of four children and during his spare time likes hiking in the mountains, cooking, and reading. I would like to thank my lovely wife, Lydia, for supporting and encouraging me to write this book.
Aaron Cure is an avid developer, instructor, and innovator. During his 10 years in the military as a linguist and a satellite communications repair technician, he learned that his real love was computer programming. After various throes with PHP, Classic ASP, VB, and a brief encounter with Java/JSP, he found a real passion for the .NET framework. After searching for a "better way" to carry out database storage and retrieval, Aaron stumbled across the NHibernate framework. Unsatisfied with the options for interacting with this great framework, he founded the NHibernate Generation project (nhib-gen) on SourceForge to reduce the "barrier to entry" for most developers. Aaron and his family run a small consulting and web hosting company doing web design and custom software development for various organizations across the country. One of their more interesting projects has been software to control laser cutting machines. In his spare time, he also enjoys developing projects with his daughters, using everything from Lego NXT (using C# and Bluetooth communications) to the Microchip PIC platform (using JAL and USB). He also collects and restores classic farm tractors, engines, and farm equipment, as well as semi trucks and trailers. He and his family display them at tractor shows, parades, schools, and various other community events. This book is dedicated to my beautiful and talented wife, Sherry, and my two wonderful daughters, Kaitlyn and MacKenzie. Without their love and support, this book would have never been written. I would also like to thank my parents, Karen and Chuck, as I wouldn't be here without them. Special thanks to my editors at Packt Publishing, who had more patience with me than I think I would have had and stuck with me throughout.
About the Reviewers Fabio Maulo has lived his youth in Montecosaro, a small village in the hills of the Marche
in Italy. His first computer was a Mac128 in 1984; since then, he has always followed technology of the moment, trying to learn as much as possible. Since the end of the last century, he has been following the evolution of ORM, at first in Delphi's world and then the .NET's world. He joined NHibernate's team in 2007 and has led the project since 2008. Thanks to my wife and my daughter who bears my work.
José Fernando Romaniello is a senior developer with 10 years of experience in Microsoft technologies. He currently lives in Argentina and works for Tellago. José has a strong involvement in various open source projects in .NET world, and he actively contributes to uNhAddins, LinqSpecs, and HqlAddin. He enjoys sharing his knowledge in his blog as well as on mailing lists. In 2011, he was appointed as Microsoft Most Valuable Professional in C# by Microsoft. I want to thank my beloved wife and my daughter; I couldn't be here without their help.
www.PacktPub.com Support files, eBooks, discount offers and more You might want to visit www.PacktPub.com for support files and downloads related to your book. Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.PacktPub.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at
[email protected] for more details. At www.PacktPub.com, you can also read a collection of free technical articles, sign up for a range of free newsletters and receive exclusive discounts and offers on Packt books and eBooks.
http://PacktLib.PacktPub.com Do you need instant solutions to your IT questions? PacktLib is Packt's online digital book library. Here, you can access, read and search across Packt's entire library of books.
Why Subscribe?
Fully searchable across every book published by Packt
Copy and paste, print and bookmark content
On demand and accessible via web browser
Free Access for Packt account holders If you have an account with Packt at www.PacktPub.com, you can use this to access PacktLib today and view nine entirely free books. Simply use your login credentials for immediate access.
Table of Contents Preface 1 Chapter 1: First Look 7
What is NHibernate 7 What is new in NHibernate 3.0 8 Why would I use it 9 Where do I get it 10 Is there documentation available 11 Can I get help using NHibernate 12 Is there commercial support available 13 Who uses it 13 Summary 14
Chapter 2: A First Complete Sample
Prepare our development environment Defining a model Time for action – Creating the product inventory model Mapping our model Time for action – Creating the mapping classes Creating the database schema Time for action – Creating the database Creating a session factory Time for action – Creating a session factory Opening a session Time for action – Opening a session to the database Persisting objects to the database Time for action – Adding a new category to the database Reading from the database Time for action – Loading the list of all categories from the database Doing the same without NHibernate – using ADO.NET only Summary
15
15 20 20 22 23 25 26 30 30 32 32 33 34 36 36 38 39
Table of Contents
Chapter 3: Creating a Model
41
Chapter 4: Defining the Database Schema
63
What is a model 41 Model first versus data first 42 Elements of a model 43 Entity 43 Value object 44 Time for action – Creating a Name value object 45 Creating an entity 47 Time for action – Creating a base entity 48 Time for action – Creating a Customer entity 50 Defining relations between entities 52 Owns or contains 52 One-to-many 53 One-to-one 54 Many-to-many 55 The order entry model 56 Time for action – Implementing an order entry model 56 Summary 62 What is a database schema 63 Who owns the database 64 Time for action – Creating the OrderingSystem database 64 Laying the foundation – table layouts 67 Time for action – Creating the Categories table 68 Time for action – Defining a script to create the Products table 71 Table columns 72 Data types of table columns 72 Relations, constraints, and indices 73 Relations 74 Constraints 76 Time for action – Adding a constraint to the Product table 77 Time for action – Creating a script to add a check constraint 78 Indices 79 Time for action – Adding an index using the designer 80 Time for action – Creating a script to add an index 82 Normal form 83 Putting it all together 83 Time for action – Creating a schema for the order entry system 83 Do not use database-generated IDs 89 Views 90 [ ii ]
Table of Contents
What about stored procedures and triggers Summary
Chapter 5: Mapping the Model to the Database
90 91
93
What is mapping? 93 Types of mapping 94 XML-based mapping 94 Attribute-based mapping 97 Fluent mapping 99 Mapping by convention 102 A word about lazy loading 103 Loading data on demand 103 Proxies 103 Virtual properties and methods 104 Creating database schema creation scripts 104 Fluent mapping 105 Expression trees – please explain 106 Getting started 106 Classes 107 Entity level settings 107 ID columns 108 Properties 109 References 111 Collections 111 Mapping many-to-many relations 113 Mapping value objects 113 Time for action – Mapping our domain 114 Use mapping conventions 121 ID conventions 121 Property conventions 122 Foreign key conventions 123 No mapping; is that possible? 124 Auto-mapping with Fluent NHibernate 124 Time for action – Using auto-mapping 125 Using ConfORM 129 Time for action – Using ConfORM to map our domain 129 XML mapping 132 Getting started 133 Classes 134 Properties 134 ID columns 135 [ iii ]
Table of Contents
One-to-many relations 136 Many-to-many relations 138 Mapping value objects 139 Time for action – Mapping a simple domain using XML 139 Summary 143
Chapter 6: Sessions and Transactions
145
Chapter 7: Testing, Profiling, Monitoring, and Logging
183
What are sessions and transactions Session Transaction The session factory Creating your first session Why do we call Commit? Adding new data to the database Reading data from the database Get versus Load Updating existing data Deleting data First level cache or identity map Clearing the cache Refreshing entities in the cache No database operation without a transaction Should I use transactions when querying data? NHibernate session versus database session Time for action – Creating a session and doing some CRUD Session management Web-based applications Time for action –Implementing session management for a web application WinForm or WPF applications Windows services Unit of Work Handling exception Second level cache Cache regions Second level cache implementations Time for action – Using a second level cache Summary Why do we need tests? What should we test? What about the database?
[ iv ]
145 145 146 147 148 148 149 149 150 151 152 152 154 154 154 155 155 156 166 166 167 174 174 175 175 176 177 177 178 182 183 184 184
Table of Contents
Download SQLite Preparing our environment for testing Testing the mapping Testing the mapping with Fluent NHibernate Time for action – Creating the base for testing Time for action – Using SQLite in our tests Testing queries Logging Why do we need to log? Logging with Log4Net Time for action – Adding logging to our application Setting up logging for NHibernate Time for action – Enable logging in NHibernate Monitoring and profiling Analyzing log files Using SQL Server Profiler Monitoring and profiling with NHibernate Profiler Time for action – Adding NHibernate Profiler support Summary
185 186 189 191 191 196 198 202 202 203 203 206 207 209 209 210 210 211 215
Chapter 8: Configuration
217
Chapter 9: Writing Queries
245
Why do we need a configuration? Elements of the configuration Which database do we want to use? What byte code provider and proxy factory? Where are our mappings? Do we use second level caching? Do we want to extend NHibernate? XML configuration Time for action – Configuring NHibernate using XML Configuring NHibernate in code Time for action – Configuring NHibernate in code Fluent configuration Configuring NHibernate with Loquacious Time for action – Using Loquacious to configure NHibernate Configuring NHibernate with Fluent NHibernate Convention over configuration Summary How can we get to our data? The LINQ to NHibernate provider
217 218 218 220 221 221 221 221 222 227 227 233 233 233 242 243 243 246 247
[v]
Table of Contents
Defining the root of our query Limiting the number of records returned Filtering a set of records Mapping a set of records Sorting the resulting set Grouping records Forcing a LINQ query to execute immediately Changing from querying the database to querying in-memory objects Creating a report using LINQ to NHibernate Time for action – Preparing the system Time for action – Creating the reports Criteria queries Untyped criteria queries Strongly-typed criteria queries Time for action – Using QueryOver to retrieve data Hibernate Query Language Lazy loading properties Executing multiple queries in a batch Eager loading versus lazy loading Bulk data changes Summary
Chapter 10: Validating the Data to Persist
What is validation and why is it so important? Who owns the database? Why, what, and where do we validate? Why validate? What data? Where to validate? Validating single properties Configuring the validator Defining validation rules Fluently configure validation rules Enforcing validation Time for action – Using property validation Validating complex business rules Enforcing always valid entities Using validation classes Time for action – Validating user input What to do if we don't own the database? Summary [ vi ]
247 248 248 249 249 250 250 251 252 252 257 260 260 263 265 272 275 277 279 282 283
285
285 285 287 287 287 288 288 289 290 291 291 292 297 298 299 300 311 312
Table of Contents
Chapter 11: Common Pitfalls—Things to Avoid
313
Appendix: Pop Quiz Answers
335
Requesting the obvious 314 Wrong mapping for read-only access 315 Blindly relying on NHibernate 317 Using implicit transactions 317 Using database-generated IDs 318 Using LINQ to NHibernate the wrong way 319 The trouble with lazy loading 321 The select (n+1) problem 321 Accessing lazy loaded parts after the session is closed 324 Did I just load the whole database? 325 Using one model for read and write operations 325 CQRS 326 Phantom updates 328 Time for action – Causing a phantom update 328 Using NHibernate to persist any type of data 332 Summary 333 Chapter 2 A First Complete Sample Chapter 3
335 335 335
Creating a Model
Chapter 4 Defining the Database Schema Chapter 5 Mapping the Model to the Database Chapter 6 Sessions and Transactions Chapter 7 Testing, Profiling, Monitoring, and Logging Chapter 8 Configuration Chapter 9 Writing Queries Chapter 10 Validating the Data to Persist
Index
335
335 335 336 336 336 336 336 336 336 336 337 337 337 337
339
[ vii ]
Preface NHibernate 3 Beginner's Guide examines all of the topics required to get a functional dataaccess layer implemented while writing the least amount of code possible, presenting options along the way to handle particular edge cases or situations as they arise. The book begins with an introduction to NHibernate 3 and then moves on to creating the development environment. It teaches you how to create a model, define a database schema, and map the model to the database, and then covers sessions and transactions. This is succeeded by testing, profiling and configuration, validation of data, and writing queries. Finally, we wrap up with notes on the common pitfalls that you should avoid.
What this book covers
Chapter 1, First Look This chapter explains what NHibernate is and why we would use it in an application that needs to access data in a relational database. The chapter also briefly presents what is the new additions in NHibernate 3.x compared to the versionto version 2.x, and discusses how one can get this framework. It also links to various sources providing documentation and help are presented. Chapter 2, A First Complete Sample. This chapter walks us through a simple yet complete sample, where the core concepts of NHibernate and its usage are introduced. Chapter 3, Creating a Model. This chapter discusses what a domain model is and what building blocks constitute such a model. In an exercise, the reader creates a domain model for a simple ordering system. Chapter 4, Defining the Database Schema. This chapter explains what a database schema is, and describes in details the individual parts comprising such a schema. A schema for the ordering system is created in an exercise.
Preface
Chapter 5, Mapping the Model to the Database. This chapter teaches you how to bridge the gap between the domain model and the database schema, with the aid of some wiring. This chapter, and presents four distinct techniques on how the model can be mapped to the underlying database, or vice versa. It is also shows how we can use NHibernate to automatically create the database schema by leveraging the meta-information contained in the domain model. Chapter 6, Sessions and Transactions. This chapter teaches you how to create NHibernate sessions to communicate with the database, and how to use transactions to group multiple tasks into one consistent operation which succeeds or fails as a whole. Chapter 7, Testing, Profiling, Monitoring, and Logging. This chapter introduces how to test and profile our system during development to make sure we deliver a reliable, robust, and maintainable application. It also shows you how an application can be monitored in a productive environment and how it can log any unexpected or faulty behavior. Chapter 8, Configuration. This chapter explains how we can instruct NHibernate about which database to use, as well as provide it with the necessary credentials to get access to the stored data. Additionally, many more settings for NHibernate are presented that can be to tweaked and will optimize the database access. are explained in this chapter. Chapter 9, Writing Queries. This chapter discusses the various means on how we can easily and efficiently query data from the database to create meaningful reports on screen or on paper. Chapter 10, Validating the data to persist. This chapter discusses why data collected by an application needs to be correct, complete, and consistent. It shows how we can instrument NHibernate to achieve this goal through various validation techniques. Chapter 11, Common Pitfalls – Things to avoid. This chapter, as the last chapter of this book, presents the most common errors developers can make when using NHibernate to write or read data to and from the database. Each such pitfall is discussed in details and possible solutions to overcome the problems are given.
What you need for this book
To successfully complete the examples in this book, you will need a copy of either Visual Studio 2008 or 2010. You can use any version as long as it includes the web application projects. This could be either a Visual Web Developer Express version or any full version such as Professional. In addition to Visual Studio, you will also need a SQL database server. The examples are generated using SQL Server Express 2008 and SQL Server Management Studio (SSMS) Express. [2]
Preface
You will also need to download the NHibernate, Fluent NHibernate, SQLite, and NUnit binary files from sources presented in Chapter 2. Absolutely no knowledge of NHibernate is required to read this book.
Who this book is for
If you are a new or seasoned developer of .NET web or desktop applications who is looking for a better way to access database data, then this book is for you. It is a basic introduction to NHibernate, with enough information to get a solid foundation in using NHibernate. Some advanced concepts are presented, where appropriate, to enhance functionality, or in situations where they are commonly used.
Conventions
In this book, you will find several headings appearing frequently. To give clear instructions of how to complete a procedure or task, we use:
Time for action – heading 1.
Action 1
2.
Action 2
3.
Action 3
Instructions often need some extra explanation so that they make sense, so they are followed with:
What just happened? This heading explains the working of tasks or instructions that you have just completed. You will also find some other learning aids in the book, including:
Pop quiz – heading These are short multiple choice questions intended to help you test your own understanding.
[3]
Preface
Have a go hero – heading These set practical challenges and give you ideas for experimenting with what you have learned. You will also find a number of styles of text that distinguish between different kinds of information. Here are some examples of these styles, and an explanation of their meaning. Code words in text are shown as follows: "We can include other contexts through the use of the include directive." A block of code is set as follows: public class ProductMap : ClassMap { public ProductMap() { Id(x =>x.Id); Map(x =>x.Name).Length(50).Not.Nullable(); Map(x =>x.UnitPrice).Not.Nullable(); Map(x =>x.ReorderLevel).Not.Nullable(); Map(x =>x.Discontinued).Not.Nullable(); } }
New terms and important words are shown in bold. Words that you see on the screen, in menus or dialog boxes for example, appear in the text like this: "Run the application and click on the CreateProduct button". Warnings or important notes appear in a box like this.
Tips and tricks appear like this.
[4]
Preface
Reader feedback
Feedback from our readers is always welcome. Let us know what you think about this book—what you liked or may have disliked. Reader feedback is important for us to develop titles that you really get the most out of. To send us general feedback, simply send an e-mail to
[email protected], and mention the book title via the subject of your message. If there is a book that you need and would like to see us publish, please send us a note in the SUGGEST A TITLE form on www.packtpub.com or e-mail
[email protected]. If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, see our author guide on www.packtpub.com/authors.
Customer support
Now that you are the proud owner of a Packt book, we have a number of things to help you to get the most from your purchase.
Downloading the example code You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub.com/support and register to have the files e-mailed directly to you.
Errata Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you find a mistake in one of our books—maybe a mistake in the text or the code—we would be grateful if you would report this to us. By doing so, you can save other readers from frustration and help us improve subsequent versions of this book. If you find any errata, please report them by visiting http://www.packtpub.com/support, selecting your book, clicking on the errata submission form link, and entering the details of your errata. Once your errata are verified, your submission will be accepted and the errata will be uploaded on our website, or added to any list of existing errata, under the Errata section of that title. Any existing errata can be viewed by selecting your title from http://www.packtpub.com/support.
[5]
Preface
Piracy Piracy of copyright material on the Internet is an ongoing problem across all media. At Packt, we take the protection of our copyright and licenses very seriously. If you come across any illegal copies of our works, in any form, on the Internet, please provide us with the location address or website name immediately so that we can pursue a remedy. Please contact us at
[email protected] with a link to the suspected pirated material. We appreciate your help in protecting our authors, and our ability to bring you valuable content.
Questions You can contact us at
[email protected] if you are having a problem with any aspect of the book, and we will do our best to address it.
[6]
1
First Look It seems like every single project we begin as developers, no matter how simple, requires some sort of storage. Sometimes, this is a simple collection of values in an XML file or a key-value pair in a properties file. However, more often than not, we need to have access to larger volumes of data represented in multiple related database tables. In either case, we are generally forced to reinvent the wheel; to create new data retrieval and storage methods for each piece of data that we want to access. Enter NHibernate.
In this chapter, we will discuss:
What is NHibernate
What is new in the latest version of NHibernate
Why we should use it and who else is using it
Where can we get help or even commercial support when we get lost
What is NHibernate Put simply, NHibernate is a framework that allows us to talk to a relational database in an object-oriented way. We can store (or as we also often say, "persist") objects in a database and load those objects from the database later on. NHibernate "auto-magically" translates our object-based language to a language that the database understands. That is, NHibernate generates the necessary SQL statements for us to insert, update, delete, and load data.
First Look
If we use NHibernate, then we never have to write any code that deals with the fact that there is an impedance mismatch between the way we develop applications in .NET and how a database works. NHibernate has abstracted away this mismatch for us. Now, let me try to explain what we understand when talking about an impedance mismatch. An application written in .NET is object-oriented. We deal with objects that contain data and logic. Most often, we deal with a single object that communicates with other objects through messages or events. On the other hand, a relational database is all about (big) sets of data. Relational databases are great when it comes to manipulating those sets of data. However, in a relational database, the concept of objects makes no sense. Logic and data live apart. In a more formal fashion, we can say that NHibernate is an Object Relational Mapper (ORM) tool or framework. NHibernate is a .NET port of Java's Hibernate. It is designed to bring the interactions with the RDBMS solutions in an application code inline with the current objectoriented application design. NHibernate is not the sole ORM framework for .NET, but it is probably the most mature and feature rich of all. Some of the other frameworks that exist are: Entity Framework from Microsoft, LLBLGen Pro, Subsonic, and Genome, to name just a few. The following screenshot is the NHibernate logo:
What is new in NHibernate 3.0 NHibernate 3.0 introduces a lot of new features as compared to the previous version. The most important ones are as follows:
NHibernate is now based on .NET 3.5. It makes heavy use of the new concepts that were introduced with this version of the framework. Most notably, this is the usage of expression trees and extension methods. A good sample is the new in-code configuration of NHibernate using a fluent API. As a consequence, NHibernate 3.0 only runs on .NET 3.5 or higher.
NHibernate now contains in its core a totally rewritten LINQ provider, which supports a large part of the full spectrum of LINQ, and overcomes many of the limitations of the previous LINQ provider. The previous LINQ provider was part of the NHibernate contributions and was based on the criteria API. It only supported a limited set of LINQ queries.
[8]
Chapter 1
There is a new QueryOver API available, which combines the use of Extension Methods and Lambda Expressions to provide a statically type-safe wrapper around the ICriteria API.
A new method has been added about how we can define the mapping between our model and the database. We can now define the mappings in code and add them to the NHibernate configuration. One contribution project already leverages this new possibility. It is called ConfORM. With this API, we can define rules and exceptions to instruct NHibernate how to create our mappings. The whole mapping process is fully convention based. ConfORM is highly extensible and we can easily define our own rules and exceptions.
It is now possible to lazy load specific columns of a table. This makes sense in scenarios where a table contains BLOB or CLOB fields, which need to be loaded rarely as a result of an explicit request. Previously, only whole objects could be lazy loaded.
Detached criteria now also work with stateless sessions.
Besides these new features, there have been a lot of bug fixes and stability improvements.
Why would I use it In the past, we carefully hand-crafted our data access layers for the applications we wrote. We spent as much as 50% or more of our overall time to implement and maintain this data layer. It was not a very challenging task though, and a lot of repetitive coding was involved. For each and every single type of object we wanted to persist in the database, we had to write similar code. We had to write code to insert a new object into the database, to update an existing object in the database, and to delete an existing object from the database. Furthermore, we wrote code to read an existing single object or a list of existing objects from the database. At a minimum, we had to write six methods per object type. Somehow, we realized that writing this data layer did not really add value to our business application. It didn't make our application specific as it was only infrastructure code. For each and every application we wrote, we had to implement similar code; be it an ordering system used in an e-commerce or a facility management system for an airport, or a portfolio management, or a trading system for a bank. All types of applications needed the same kind of data access layer.
[9]
First Look
As we were so bored by always writing the same kind of code again and again, we started to think about the possibility to automate this process as much as possible. The first approach was to write code generators, which, based on certain configuration and templates, wrote the data access code for us. This worked to a certain extent. We were very happy about it, as now, finally, we had more time to concentrate on the important problems such as writing and maintaining the business logic and the user interface, which made our applications unique and added real business value to the overall solution. As we were not the only developers facing the problem of writing the data access code, some people started to write and publish a framework that would solve this problem. This was the birth of the ORM frameworks such as NHibernate. Nowadays, accessing relational data from an object-oriented application is a solved problem, period! Why would you want to reinvent the wheel? There is absolutely nothing left that is new or yet unresolved. Data access code is not specific to a certain problem domain. (This statement is even true in regard of new storage technologies such as document databases, Google's big table, and others.) Every application that needs to manipulate data reads and writes this data from or to the data store in exactly the same way. Thus, writing your own data layer is a waste of time. Somebody even used a much more pronounced phrase to pinpoint this fact, "Writing your own data access layer is like stealing money from your client".
Where do I get it The home of the NHibernate project is at http://www.nhforge.org, while the code is housed at SourceForge (http://sourceforge.net/projects/nhibernate/). NHibernate is an open source project (OSS). Thus, the code and/or the binaries are freely available and can be downloaded from SourceForge. Throughout this book, we also make use of Fluent NHibernate, which is a sister project of NHibernate and provides a way to fluently define the NHibernate mappings between the model and database. It is also an OSS project and binaries and/or source code can be downloaded from http://fluentnhibernate.org/downloads. In a later chapter, we will also use a NHibernate contribution project for the validation of the domain entities. The set of NHibernate contribution projects can be downloaded from http://sourceforge.net/projects/nhcontrib/.
[ 10 ]
Chapter 1
If you use Visual Studio 2010 Professional or higher, then there is an even easier method available as to how we can get to the necessary NHibernate (or Fluent NHibernate) binaries. We can use the NuGet extension for Visual Studio to download NHibernate and all its dependencies and automatically make those binaries a part of our projects. The NuGet project (http://nuget.codeplex.com/) is a free, open source package management system for .NET. It is led and sponsored by Microsoft. At the time of writing, the NuGet project is at version 1.3 and is still under heavy development. Nevertheless, it feels ready for professional use and can really simplify the way we add external dependencies, such as NHibernate or Fluent NHibernate, to our solutions. The following screenshot is the NuGet logo:
Is there documentation available Unlike with a commercial product, there is no manual available for NHibernate. However, there is a website that is fully dedicated to NHibernate. This website can be found at http://www.nhforge.org and contains a lot of useful information about and around NHibernate. There is also a reference document published on this website. The reference documentation is very detailed and complete, and should be one of the first sources to consult whenever dealing with a very specific problem or setting. There are also many beginners guides, tutorials, and blog posts that can be found here. The following list presents a couple of blogs that are either mostly dedicated to NHibernate or contain a lot of information around this framework:
NHibernate meta blog at http://nhforge.org/blogs/nhibernate.
NHibernate FAQs at http://nhibernate.hibernatingrhinos.com/.
Fabio Maulo's blog at http://fabiomaulo.blogspot.com. Fabio is the leader of the NHibernate project.
Ayende Rahien's blog at http://ayende.com/Blog/default.aspx. Ayende is one of the main contributors to NHibernate. Search for NHibernate.
James Gregory's blog at http://jagregory.com. James is the leader of the Fluent NHibernate project.
[ 11 ]
First Look
Quite a few books have been written about NHibernate so far. The latest one being the NHibernate 3.0 Cookbook, which is written for intermediate to advanced users of NHibernate. The detailed list of books is as follows:
"Hibernate in Action" by Christian Bauer and Gavin King, Manning Publications Co.: This is probably the first book published about Hibernate and discusses the Java version of the framework "NHibernate in Action" by Pierre Henri Kuaté, Tobin Harris, Christian Bauer, Gavin King, and Tobin Harris Manning Publications Co.
"NHibernate 2 Beginner's Guide" by Aaron Cure, Packt Publishing
"NHibernate 3.0 Cookbook" by Jason Dentler, Packt Publishing
Can I get help using NHibernate For any questions you might have regarding NHibernate, there is a Google Group called "NHibernate Users Mailing List". This mailing list is very active and questions are usually answered within hours, if not within minutes. The list can be found at http://groups. google.com/group/nhusers. At the time of writing, this list contained nearly 30,000 messages. The list can be searched by any keyword and is a good source of tips and hints about how to use NHibernate in various very specific circumstances. Mostly, we can find a solution to our problem in one of the existing message threads without even having to post our own question first. This is shown in the following screenshot:
[ 12 ]
Chapter 1
In addition to this (English) group, there exist several language specific (sub-) groups. Currently, there exist groups in Spanish, French, Italian, Portuguese, German, Dutch, and Chinese. The complete list of groups can be found at http://nhforge.org/groups. One of the more active groups is the Chinese group. The Chinese community not only has a forum, but also a community site at http://home.cnblogs.com/groups/NHibernate. They are currently working on a new community site at http://nhplex.com/. If you want to discuss the development of NHibernate or become a contributor to the project, then there is also a mailing list available for this purpose: http://groups. google.com/group/nhibernate-development.
Is there commercial support available NHibernate has a rich and vibrant community, and there are many community-based support channels which are absolutely free, as mentioned in the previous section. However, there are situations where you really want to have someone you can call to get immediate help. Or maybe your company doesn't want to use a technology without having a signed support contract at hand. At the time of writing, commercial support for NHibernate is available at least from http://nhprof.com/CommercialSupport.
Who uses it NHibernate is a mature and stable framework and has been around for many years. Many companies have been using it in many different projects. A lot of those projects are mission critical to the respective companies. Other applications that use NHibernate are exposed to high load and perform very well under these stressful circumstances. Due to political reasons, not all companies wish to disclose their usage of NHibernate. However, there is still a list of success stories available on the NHibernate website. This list can be found by using the following link: http://nhforge.org/wikis/successstories/success-stories.aspx
[ 13 ]
First Look
Summary In this chapter, we have learned what NHibernate is and why we would want to use it. We also briefly discussed the new important features that NHibernate 3.0 brings to the table. As an important aspect, we got all the information we need to find abundant documentation about NHibernate and get community-driven help or even commercial support (if needed). We also learned that NHibernate has a very broad user base and is driving many critical projects around the world in companies of all sectors. To give you some numbers: over 100 thousand downloads have been registered so far for NHibernate 2.1.2, and more than 35 thousand downloads for NHibernate 3.0 in its first three months of availability.
[ 14 ]
2
A First Complete Sample So far you have learned what NHibernate is and why it makes sense to use it when developing a new application. Now it is time to show how easy it is to use NHibernate in a real world application. We will create, with minimal effort, a fully working sample that allows us to write to and subsequently, read data from a database.
In this chapter, we shall:
Prepare our system to enable development of applications using NHibernate
Create a simple model of a problem domain
Create a database and define a mapping between the model and the database
Write and read data to and from the database
Discuss what we would have to do to achieve the same goal without NHibernate or any other ORM tool
So let's get on with it.
Prepare our development environment In this book, I am assuming that you have a computer at hand which has Windows Vista, Windows 7, Windows Server 2003 or Windows Server 2008 installed. If you are using an Apple computer, then you can install, for example, Windows 7 as a virtual machine. First, install Microsoft Visual Studio 2010 Professional, Microsoft Visual C# 2010 Express or Microsoft Visual Basic 2010 Express on your system. The Express editions of Visual Studio can be downloaded from http://www.microsoft.com/express/windows.
A First Complete Sample
Note that NHibernate 3.x can also be used with the 2008 editions of Microsoft Visual Studio, but not with any older versions. NHibernate 3.x is based on the .NET framework version 3.5, and thus only works with IDEs that support this or a higher version of the .NET framework.
Additionally, note that if you don't want to use Visual Studio, then there are at least two other free OSS options available to you:
MonoDevelop is an IDE primarily designed for C# and other .NET languages. MonoDevelop makes it easy for developers to port .NET applications created with Visual Studio to Linux and to maintain a single code base for all platforms. MonoDevelop 2.4 or higher can be downloaded from http://monodevelop.com/ download.
SharpDevelop is a free IDE for C#, VB.NET, and Boo projects on Microsoft's .NET platform. It is open source. SharpDevelop 3.2 or higher can be downloaded from http://sharpdevelop.net/OpenSource/SD/Default.aspx.
Furthermore, note that NHibernate also works on Mono: http://www.mono-project.com. Next, we need a relational database to play with. NHibernate supports all major relational databases like Oracle, MS SQL Server, MySQL, and so on. In this book, we will use MS SQL Server as our Relational Database Management System (RDBMS). We will also use SQLite in Chapter 7 when writing tests. To use SQL Server as the database server throughout this book is not a casual choice. Microsoft SQL Server is the most used RDBMS in conjunction with NHibernate and, in general, with .NET projects. The SQL Server driver for NHibernate is one of the most tested drivers in NHibernate's suite of unit tests, and when specific new features come out, it is likely that they will be first supported by this driver.
Install the free Microsoft SQL Server 2008 R2 Express on your system if you have not already done so during the install of Visual Studio. You can download the express edition of MS SQL Server from here http://www.microsoft.com/express/Database/. For our samples, it really doesn't matter which version you download: the 32-bit or the 64-bit version. Just take the one that matches best with the bitness of your operating system. Make sure that you install SQL Server with the default instance name of SQL Express. If this is not possible for you, then you will need to adjust the connection string settings in the samples we develop throughout this book.
[ 16 ]
Chapter 2
Make sure you also download and install the free SQL Server Management Studio Express (SSMS) from the following link: http://www.microsoft.com/download/en/details.aspx?id=22985.
Now, we are ready to tackle NHibernate. We can download NHibernate 3.1.0 GA from Source Forge http://sourceforge.net/projects/nhibernate/. The download consists of a single ZIP file containing the following content, as shown in the screenshot:
The binaries that are always needed when developing an NHibernate based application can be found in the Required_Bins folder. Opening this folder, we find the files as shown in the following screenshot:
[ 17 ]
A First Complete Sample
Note that if you are downloading version 3.1 or newer of NHibernate, you will no longer find the two DLLs, Antlr3.Runtime.dll and Remotion. Data.Linq.dll, in the ZIP file that were present in version 3.0. The reason is that they have been IL merged into the NHibernate.dll.
If we want to use lazy loading with NHibernate (and we surely will), then we also have to use some additional files which can be found in the Required_For_LazyLoading folder. Lazy loading is a technique that is used to load certain parts of the data only when really needed, which is when the code accesses it. Lazy loading will be discussed in detail in subsequent chapters of this book.
There are three different options at hand. We want to choose Castle. The corresponding folder contains these files, as shown in the following screenshot:
As we are also using Fluent NHibernate throughout the samples in this book, we want to download the corresponding binaries too. Go grab the binaries from the Fluent NHibernate website and copy them to the appropriate location on your system. In either case, there is no installer available or needed. We just have to copy a bunch of files to a folder we define. Please download Fluent NHibernate, which also contains the binaries for NHibernate, from here (http://fluentnhibernate.org/downloads), as shown in the following screenshot. Make sure you download the binaries for NHibernate 3.1 and not an earlier version.
[ 18 ]
Chapter 2
Save the ZIP file you just downloaded to a location where you can easily find it for later usage. The ZIP file contains the files shown in the following screenshot:
The only additional files regarding the direct NHibernate download are the FluentNHibernate.* files. On the other hand, we do not have the XSD schema files (nhibernate-configuration.xsd and nhibernate-mapping.xsd) included in this package and we'll want to copy those from the NHibernate package when implementing our sample.
[ 19 ]
A First Complete Sample
Defining a model After we have successfully downloaded the necessary NHibernate and Fluent NHibernate files, we are ready to start implementing our first application using NHibernate. Let's first model the problem domain we want to create the application for. The domain for which we want to build our application is a product inventory system. With the application, we want to be able to manage a list of products for a small grocery store. The products shall be grouped by category. A category consists of a name and a short description. The product on the other hand has a name, a short description, a category, a unit price, a reorder level, and a flag to determine whether it is discontinued or not. To uniquely identify each category and product, they each have an ID. If we draw a class diagram of the model just described, then it would look similar to the following screenshot:
Unfortunately, the class designer used to create the preceding screenshot is only available in the professional version of Visual Studio and not in the free Express editions.
Time for action – Creating the product inventory model Let's implement the model for our simple product inventory system. First, we want to define a location on our system, where we will put all our code that we create for this and the subsequent chapters.
1.
Create a folder called NH3BeginnersGuide on your file system. Inside this new folder, create another folder called lib. This is the place where we will put all the assemblies needed to develop an application using NHibernate and Fluent NHibernate.
2.
Locate the ZIP file containing the Fluent NHibernate files that you downloaded in the first section of this chapter. Extract all files to the lib folder created in the preceding step. [ 20 ]
Chapter 2
3.
Open Visual Studio and create a new project. Choose WPF Application as the project template. Call the project Chapter2. Make sure that the solution you create will be saved in the folder NH3BeginnersGuide you created in the preceding step. When using VS 2008 Pro, you can do this when creating the new project. If, on the other hand, you use the Express edition of Visual Studio, then you choose the location when you first save your project.
4.
Add a new class to the project and call it Category. To this class, add a virtual (auto-) property called Id, which is of the int type. Also, add two other virtual properties of the string type, called Name and Description. The code should look similar to the following code snippet: namespace Chapter2 { public class Category { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual string Description { get; set; } } }
Downloading the example code You can download the example code files for all Packt books you have purchased from your account at http://www.PacktPub.com. If you purchased this book elsewhere, you can visit http://www.PacktPub.com/support and register to have the files e-mailed directly to you. [ 21 ]
A First Complete Sample
5.
Add another class to the project and call it Product. To this class, add the properties, as shown in the following code snippet. The type of the respective property is given in parenthesis: Id (int), Name (string), Description (string), Category (Category), UnitPrice (decimal), ReorderLevel (int), and Discontinued (bool). The resulting code should look similar to the following code snippet: namespace Chapter2 { public class Product { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual string Description { get; set; } public virtual Category Category { get; set; } public virtual decimal UnitPrice { get; set; } public virtual int ReorderLevel { get; set; } public virtual bool Discontinued { get; set; } } }
What just happened? We have implemented the two classes Category and Product, which define our simple domain model. Each attribute of the entity is implemented as a virtual property of the class. To limit the amount of code necessary to define the entities, we use auto properties. Note that the properties are all declared as virtual. This is needed as NHibernate uses lazy loading by default. The details and implications of lazy loading will be discussed in more detail in subsequent chapters of this book.
Mapping our model Now that we have defined and implemented the model for our simple product inventory application, we need to instruct NHibernate how to map our model classes to the underlying tables in the database. The mapping is responsible to define which class maps to which table, and which property of a class maps to which column of a table. Having defined this information, NHibernate can successfully operate and create the necessary SQL statements that allow us to add, update, and delete data in the database, as well as query existing data. Various methods exist to show how we can define the mappings. Probably, the most common methods are either to define the mappings in the form of XML documents or to define them in code. In this chapter, we will choose the latter method. [ 22 ]
Chapter 2
Time for action – Creating the mapping classes To make our life very simple, we will use Fluent NHibernate to map our model to the underlying database. Fluent NHibernate allows us to define the mapping in code, in a typesafe way.
1.
Add a reference to FluentNHibernate.dll and to NHibernate.dll, which you can locate in the lib folder, as shown in the following screenshot:
2.
Add a new class to the project and call it CategoryMap. Add using FluentNHibernate.Mapping; to the file.
3.
Make the class inherit from ClassMap.
4.
Add a default constructor to the class and define the mapping for the Id.
[ 23 ]
A First Complete Sample
5.
In the constructor, add Id(x =>x.Id);, that is, we use the Id method defined in the base class to define the mapping of Id. The Id method expects a lambda expression which identifies the property of the class Category, which shall be mapped as Id.
6.
Furthermore, in the constructor, add Map(x =>x.Name); to define the mapping for the Name property.
7.
Do the same thing for the Description property. The resulting code should look similar to the following code snippet: using FluentNHibernate.Mapping; namespace Chapter2 { public class CategoryMap : ClassMap { public CategoryMap() { Id(x => x.Id); Map(x => x.Name); Map(x => x.Description); } } }
8.
Add another class called ProductMap to the project and add the same using statement to the file as in the CategoryMap file. Make the class inherit from ClassMap and also add a default constructor.
9.
In the default constructor, add code to map the property Id as the primary key.
10. Furthermore, add code to map the properties: Name, Description, UnitPrice, ReorderLevel, and Discontinued.
11. Finally, use References(x =>x.Category); to map the Category property. When done, your code should look similar to the following code snippet:
using FluentNHibernate.Mapping; namespace Chapter2 { public class ProductMap : ClassMap { public ProductMap() { [ 24 ]
Chapter 2 Id(x => x.Id); Map(x => x.Name); Map(x => x.Description); Map(x => x.UnitPrice); Map(x => x.ReorderLevel); Map(x => x.Discontinued); References(x => x.Category); } } }
Make sure all your classes (that is, Category, Product, CategoryMap, and ProductMap) are defined as public. If these classes are not defined as public, NHibernate will not be able to discover and use them.
What just happened? We have defined two classes which provide NHibernate with detailed information about how the model classes Category and Product can be mapped to an underlying database containing two compatible tables for each Category and Product class, respectively. Looking at the code, it seems to be very little information that we provide to NHibernate to do its job, but the nice thing is that Fluent NHibernate uses a lot of predefined and meaningful defaults if not defined otherwise.
Creating the database schema We have defined a model for our problem domain and have defined the necessary information that NHibernate needs to map the model to an underlying database. Now we need to create this database. However, we do not want to handcraft the database schema, but rather let NHibernate create the database schema for us. The only thing we need to do manually is create a new empty database. NHibernate will use the meta information we provided via the definition of the mapping classes to create an appropriate database schema.
[ 25 ]
A First Complete Sample
Time for action – Creating the database First, we will manually create an empty new database and then we will add code to our application, which allows us to delete and recreate the database schema.
1.
Open the Microsoft SQL Server Management Studio (SSMS), which was installed as part of the MS SQL Server Express setup. When asked for the connection details, enter .\SQLExpress as the server name and choose Windows Authentication as the authentication mode. This is shown in the following screenshot:
If you are accessing your locally installed SQL Server Express edition, then you can also use the alias (local) instead of .\SQLExpress to connect to your database.
2.
Right-click on the Databases folder and select New Database…. Enter NH3BeginnersGuide as the database name and click on OK. A new empty database will be created for you.
3.
Back in Visual Studio, open the MainWindow.xaml file and modify XAML, as shown in the following code snippet, to create a window with the title Product Inventory displaying one button with the text Create Database: [ 26 ]
Chapter 2
4.
Open the preceding code file of MainWindow (the MainWindow.xaml.cs file) and implement the code which defines a connection string to access our NH3BeginnerGuide database on SQL Server. This is shown in the following code snippet: const string connString = "server=.\\SQLExpress;" + "database=NH3BeginnersGuide;" + "Integrated Security=SSPI;";
5.
Add the following using statements at the top of the MainWindow.xaml.cs file: using using using using
6.
FluentNHibernate.Cfg; FluentNHibernate.Cfg.Db; NHibernate.Cfg; NHibernate.Tool.hbm2ddl;
Use the Fluently class of Fluent NHibernate to define a configuration which will allow us to ask NHibernate to create the database schema via the CreateSchema method passed as a parameter to the ExposeConfiguration method: Fluently.Configure() .Database(MsSqlConfiguration .MsSql2008 .ConnectionString(connString)) .Mappings(m =>m.FluentMappings.AddFromAssemblyOf()) .ExposeConfiguration(CreateSchema) .BuildConfiguration();
7.
In the CreateSchema method, use the SchemaExport class of NHibernate to delete and create the schema based on the metadata available through the configuration files, as shown in the following code snippet: private static void CreateSchema(Configuration cfg) { var schemaExport = new SchemaExport(cfg); schemaExport.Drop(false, true); schemaExport.Create(false, true); } [ 27 ]
A First Complete Sample
8.
Putting all the preceding code together, MainWindow should look similar to the following code snippet: using using using using using
System.Windows; FluentNHibernate.Cfg; FluentNHibernate.Cfg.Db; NHibernate.Cfg; NHibernate.Tool.hbm2ddl;
namespace Chapter2 { public partial class MainWindow { const string connString = "server=.\\SQLExpress;" + "database=NH3BeginnersGuide;" + "Integrated Security=SSPI;"; public MainWindow() { InitializeComponent(); } private void btnCreateDatabase_Click(object sender, RoutedEventArgs e) { Fluently.Configure() .Database(MsSqlConfiguration .MsSql2008 .ConnectionString(connString)) .Mappings(m =>m.FluentMappings .AddFromAssemblyOf()) .ExposeConfiguration(CreateSchema) .BuildConfiguration(); } private static void CreateSchema(Configuration cfg) { var schemaExport = new SchemaExport(cfg); schemaExport.Drop(false, true); schemaExport.Create(false, true); } } }
[ 28 ]
Chapter 2
9.
Run the application and click on the Create Database button. Verify within SSMS that the database NH3BeginnersGuide now contains two tables called Category and Product, respectively. Furthermore, verify that the tables contain a column for each property of the respective class, as shown in the following screenshot:
[ 29 ]
A First Complete Sample
What just happened? We have used SSMS to define an empty new SQL Server database. We then created a new WPF application with a single button on the main window. When we run the application, we can trigger code by clicking on the button which creates an NHibernate configuration object. This object is then used in conjunction with the SchemaExport class of NHibernate to create the database schema in the previously defined database.
Creating a session factory To create sessions through which we can access the database, we need a session factory. Every session object that we ever need has to be created with the aid of such a session factory. Once again, we can use the classes provided by Fluent NHibernate to make things simple. We use the Fluently helper class to define, in a fluent way, the necessary information NHibernate needs to be able to create a session factory for us. Specifically, we have to define what database product and which version of it we are going to use. We also have to define where Fluent NHibernate can find all the mapping files we have written. This leads us to the following code: private ISessionFactory CreateSessionFactory() { return Fluently.Configure() .Database(MsSqlConfiguration .MsSql2008 .ConnectionString(connString)) .Mappings(m =>m.FluentMappings .AddFromAssemblyOf()) .BuildSessionFactory(); }
The preceding method creates a session factory for an MS SQL Server version 2008. It uses the settings of connString when connecting to the database. We further instruct Fluent NHibernate to parse the assembly in which ProductMap is implemented for any mapping classes. With all this information, we instruct NHibernate to create the factory by calling BuildSessionFactory().
Time for action – Creating a session factory We now want to write the code necessary to create a session factory for our product inventory application.
1.
Add a reference NHibernate.ByteCode.Castle.dll to the project. This file can be found in the lib folder. NHibernate needs this assembly to create proxies. [ 30 ]
Chapter 2
2.
Add a using NHibernate statement to the preceding code of MainWindow (the MainWindow.xaml.cs file).
3.
Add a CreateSessionFactory method, as described above, to the preceding code.
4.
Extend the grid of MainWindow with another row with a height set to Auto.
5.
Add another button to the MainWindow and name it btnCreateSessionFactory and define its content as Create Session Factory. Set the Grid.Row to 1. Furthermore, define a click event handler for this new button. The definition of the Grid should look similar to the following code snippet:
6.
In the click event handler for this new button, enter the following code: var factory = CreateSessionFactory();
7.
Start the application and test whether you can successfully create a session factory, as shown in the following screenshot:
[ 31 ]
A First Complete Sample
What just happened? We have added a second button to the main window of our application. In the click event handler of this button, we have implemented the code that creates a new NHibernate session factory. We built the session factory with the aid of the Fluently helper class provided by Fluent NHibernate. The session factory can then be used to create session objects used to access the database created in the preceding exercise.
Opening a session Our application needs a session object to communicate with the database when writing or reading data. As mentioned above, session objects can only be obtained through a session factory. Once we have constructed a session factory, opening a session is a quick and cheap operation. After we have finished using our session object, we must not forget to close the session and dispose it. We can use a using statement to instruct the compiler to do this automatically for us.
Time for action – Opening a session to the database We will implement the code such that our product inventory application can open a session to the database.
1.
Add another row to the grid of MainWindow. In this new third row, place an additional button called btnCreateSession with content Create Session. Furthermore, define an event handler for the click event. The code for the button should look similar to the following code snippet:
2.
In the event handler of this button, add the following code: private void btnCreateSession_Click(object sender, RoutedEventArgs e) { var factory = CreateSessionFactory(); using (var session = factory.OpenSession()) { // do something with the session } }
3.
Start the application and verify that you can indeed open a session to the database. [ 32 ]
Chapter 2
What just happened? We added yet another button to the main window of the application whose click event handler contains the code to first create a session factory by using the code implemented in the preceding exercise. This session factory is then used to open a session object. We have not yet done anything meaningful with this session object. To use the session will be part of the next exercise.
Persisting objects to the database The whole ceremony of defining mappings, creating a database schema, creating a session factory, and finally creating and opening a session object has one single purpose: we want to persist the data created by our product inventory application to a durable data store. Let's first create a new category object. We can do this by using the following code snippet: var category = new Category { Name = "Beverages", Description = "Some description" };
Next, we want to save this new category object and we can do so by using the following code: var id = session.Save(category);
The value that is returned from the save method corresponds to the ID of the newly created category object. Now, let's create a product. The product has a reference to a category object. Before we can save a product, the corresponding category object must have been persisted to the database. The following code would work: var category = new Category { Name = "Beverages" }; var product = new Product { Name = "Milk", Category = category }; session.Save(category); session.Save(product);
The session object can also be used to delete an existing object from the database. The command to do so is as simple as the following code: session.Delete(category);
Here, the category object we pass as a parameter to the delete method corresponds to the one we want to remove from the database. [ 33 ]
A First Complete Sample
Time for action – Adding a new category to the database We want to add the possibility to add a new category to the main window of our application. For this we need two textboxes where the user can enter the category name and description, we also need a button to trigger the creation of the category.
1.
Add another row to the grid of the MainWindow; set the row height to *. This will be the fourth row so far. In this row, we define another grid called CategoryGrid, which will contain all the controls we need to create a new category. This new grid should look similar to the following code snippet:
2.
In the preceding code file of MainWindow, add the following code to the click event handler of the new button: private void btnAddCategory_Click(object sender, RoutedEventArgs e) { var factory = CreateSessionFactory(); using (var session = factory.OpenSession()) { var category = new Category { [ 34 ]
Chapter 2 Name = txtCategoryName.Text, Description = txtCategoryDescription.Text }; session.Save(category); } }
3.
Start the application and try to add several new categories to the database. Use SSMS to verify that the categories are indeed added to the Category table, as shown in the following screenshot:
What just happened? In this exercise, we extended our main window to be able to enter a name and description for a category, which we can then store in the database by clicking on the Add Category button, which we also added to the window. In the click event handler of the Add Category button, we implemented a code to open an NHibernate session object, create a new Category entity from the values entered by the user into the Name and Description textboxes, and then, using the session object, save the category to the database.
[ 35 ]
A First Complete Sample
Reading from the database Persisting data into a database is surely important, but we also want to be able to reuse this data and thus must have a means to access it. The NHibernate session object provides us with this possibility. We can use the session object directly to access a single object in the database, which is identified by its primary key by using the following code: var category = session.Get(1);
NHibernate will query the database for a category record in the category table having an ID of 1. NHibernate will then take this data and create a category object from it. We also say, "NHibernate rehydrates an object". If we want to read not only a single object, but a list of objects from the database, we can use the LINQ to NHibernate provider to do so. The following statement will read all records from the category table and generate a list of the category objects out of it: var categories = session.Query();
We can even go a step further and, for example, query a list of all discontinued products sorted by their name with the following statement: var products = session.Query() .Where(p =>p.Discontinued) .OrderBy(p =>p.Name);
Time for action – Loading the list of all categories from the database We want to add a list box to our application where we can display the list of all the categories that have been added to the database so far.
1.
Add another row to the CategoryGrid. In the first column of the new row, add a command button which we will use to trigger the loading of the category list. In the second column, we need to add a list box which will be used to display the list of the retrieved categories. The XAML fragment for the button and the list box should look similar to the following code snippet:
[ 36 ]
Chapter 2
2.
Add a using NHibernate.Linq and using System.Linq statement to the top of the file.
3.
Now, add a code that creates a session factory, opens a new session, and loads the ordered list of categories to the click event handler of the new button. Take the list of categories and bind it to the list box. Your code should look similar to the following code snippet: private void btnLoadCategories_Click(object sender, RoutedEventArgs e) { var factory = CreateSessionFactory(); using (var session = factory.OpenSession()) { var categories = session.Query() .OrderBy(c =>c.Name) .ToList(); lstCategories.ItemsSource = categories; lstCategories.DisplayMemberPath = "Name"; } }
4.
Run the application and try to load the list of categories. Add more categories and then reload the list. Verify that all categories are loaded and that they are sorted by name in ascending order, as shown in the following screenshot:
[ 37 ]
A First Complete Sample
What just happened? We added another button, called Load Categories, to the main window. The code we implemented in the click event handler of this latest button opens a session object and uses a LINQ query to retrieve a list of all the categories stored so far in the database. The list is ordered by the category name and displayed in a list box, which we also added to the main window.
Have a go hero Similar to what we have done to be able to add new categories to the database, add the necessary controls to the main window of the application, which will allow you to define a new product, and then store it in the database. It should be possible to define at least the Name, UnitPrice, and ReorderLevel of the product. Add an Add Product button to the window and in its click event handler, add the code to open a session, create a product with the data entered by the user, and store the product in the database by using the session object.
Doing the same without NHibernate – using ADO.NET only So far, we have seen that we can, with minimal effort, write an object-oriented application which can persist its data to a RDBMS and can also recreate objects by reading previously persisted data from the RDBMS. At no time did we have to deal with any specifics of the RDBMS we chose as our data store. If we had decided not to use an ORM framework and handcraft our data access layer, then we would have had to implement a considerable amount of code. We would also be required to have created the database schema from hand and write SQL statements for insert, update, delete, and read operations. For the C# compiler, these SQL statements are just strings and no syntax checking is possible. Using datasets and code generation might seem to diminish the problem, but this is not a real solution. On one hand, using datasets instead of domain objects makes it more difficult to write object-oriented, extendable, scalable, and robust applications. On the other hand, code generation just covers a problem, but does not solve it. We have to get rid of the repetitive code that adds no value to our business domain and just increases the maintenance overhead.
[ 38 ]
Chapter 2
Pop quiz 1. Instances of which of the following NHibernate classes are needed to automatically drop and re-create a database schema which corresponds to the domain model of our application: a. Session b. Configuration c. SchemaExport d. SessionFactory e. All of the above
Summary We learned in this chapter what the minimal steps are that are needed to create a working application which uses NHibernate as an ORM framework, and is able to write data to and read data from a database. Specifically, we covered:
What the prerequisites are and how we need to configure our system to be able to develop applications that use NHibernate 3
How we can define a model and map this model to the underlying database by using Fluent NHibernate
How to make NHibernate automatically create a database schema for us
How we create a session factory and how to use this factory to create sessions, which in turn are used to persist data to and retrieve the existing data from the database
Now that we've learned how we can use NHibernate in a real world application, we're ready to dig deeper into the details of the various steps we presented. In the next chapter, we start with the definition of the model.
[ 39 ]
3
Creating a Model So far we have learned what NHibernate is and how we can implement a simple solution based on the NHibernate ORM framework. To be able to create a solution for a business domain we might not be subject matter experts of, we need to first get a deeper insight and understanding of the domain context at hand. This is where the model comes into play.
In this chapter, we shall define:
What a model is
What the key elements of a model are
How we create a model
Now, let's start with some background.
What is a model When we get the task of writing an application for a customer, we have to write a piece of software which helps our customer to automate certain aspects of their business. To be able to do this, we need to get an understanding of the business domain our customer is working in. Each business domain is different, even though there do exist some similarities. A closer look at the supposed similarities will show us that there are always important details that clearly differentiate the two business domains even in those areas. We can only develop a solution if we have a clear understanding of the business domain. However, regularly we are laymen and have never before worked in the respective domain. By no means are we subject matter experts.
Creating a Model
To obtain this necessary understanding of the domain, we might sit together with the customer or domain experts of the customer, and develop a model of their domain. A model is an attempt to describe reality. A model is equally understandable by us developers, as by business analysts, our customers, and their subject matter experts. Thus, a model is not a UML diagram or any other very developer-centric way of describing the domain. A model consists of elements playing an important role in the domain we describe, a description of their relationships among each other, and the relationship to the world outside of our scope. Reality is complex, and thus each description is only very rough and often an over simplification of the truth. To make our lives simpler we try to limit the extent of what we try to describe. We call this limited view the scope. We only focus on certain aspects and neglect others. When we develop the application for a customer, our solution only deals with certain aspects of the business. Usually we do not have to solve all problems of the corresponding business. Again, we can limit the scope of what we are considering, and thus the definition of a model becomes simpler and more accurate. Having a model at hand allows us to focus on the essential parts of the problem domain, instead of getting lost in the details. It also allows us to use the same language when talking to business analysts and subject matter experts. Note that although this is a book about NHibernate, you will not find any reference to NHibernate in this chapter. This is a positive aspect of the ORM framework; we do not have to make any compromises regarding NHibernate in our model. Our model will consist of Plain Old CLR Objects (POCO), where CLR stands for Common Language Runtime.
Model first versus data first In the past, we have been told that the collected data is the most important asset for a company, or to put it in a more pronounced way: "the truth is in the database". Consequently, applications were built with this notion in mind. Business analysts and architects sat down, and first designed the data model. The questions they asked were: "What type of data do we have, and how are the various pieces of data related to each other?". The result of this effort was a so-called Entity Relationship Diagram (ERD). What makes an entity, such as a Customer specific to a certain company and/or sector? It is not the fact that there is an entity Customer with various attributes, such as name and address, but the real distinction is in how this entity is used. An entity Customer has a totally different meaning in the context of a bank than it has in the context of a travel agency. [ 42 ]
Chapter 3
Data sitting in a data store is of no value as long as there are no processes defined about how to use and interpret this data. However, the definition of those processes and usage scenarios are part of what we call the model of the corresponding domain. It is the domain model, which is really at the heart of an application, that adds value to a business. The data and its structure follow.
Elements of a model A model is the attempt to describe as well as possible, and as detailed as necessary, the reality of a certain domain. A model consists of various parts that we are now going to inspect in detail.
Entity An entity is an object that is uniquely identifiable by a combination of its attributes and has a well-defined lifespan. It starts to exist at a well-defined moment in time, it can also be destroyed or terminated in such a well-defined moment in time. Often, the entity contains an ID or key attribute, which is used to uniquely identify it. Two entities of the same type and having the same identifier are considered to be the same entity. In a model, we call those types of objects entity, whose identity and life cycle is of importance. Some typical entities in the line of business (LOB) applications are: customer, product, order, supplier, and so on. Taking an e-commerce application as an example, it is very important to be able to distinguish customers by a unique identifier, and to also know when a prospect becomes a customer, or when an existing customer is inactivated or removed from the list of customers. As part of a model, entities play a key role. They represent important concepts of the business domain. In real life, we are used to dealing with entities that have human readable or understandable identifiers. Such identifiers are also called natural keys. Typical examples are: the social security number (SSN) for US citizens, the product code for products, the account number for bank accounts, the order number for orders, and so on. It is important to use artificial identifiers to uniquely identify entities in an application. Such artificial identifiers are also called surrogate keys. Another definition for this identifier is the persistent object identifier (POI). In practice, we need something to uniquely identify an instance of an entity inside a data store, like the way we can identify an instance of an entity by memory through its reference.
[ 43 ]
Creating a Model
Why should we do this and why can't we just use the natural keys? We all know that, in real life, it can happen that a natural key has to change for one reason or the other. A product receives a new product code or a SSN has to be re-emitted. However, in an application, we need identifiers that remain the same during the whole life span of an entity, under all conditions. This can be guaranteed by using surrogate keys.
Value object In a model, objects may exist with a life span of no interest and which do not need to be uniquely identified by an ID or a key. These kinds of objects are called value objects. Two instances of a value object of the same type are said to be the same if all of their properties are the same. As a direct consequence of the above definition of a value object, it follows that value objects are immutable; that is, once a value object is defined, it cannot change anymore. While in a banking application, an account is an entity and needs to be uniquely identified by an ID, there exists the concept of money, which is an object that is a combination of a value and a currency symbol. Such a money object is a good sample of a value object. Two such money objects that have the same numeric value and the same currency symbol are the same. There is no distinction between them. One object can be replaced by the other without introducing any side effects. Other examples of value objects are:
Name of, for example, a person entity. The Name value object consists of the surname, given name, and middle name of a person object.
Geographical coordinate in a GIS application. Such value objects consist of a value pair for latitude and longitude.
Color in a colorimetry application. A color object consists of values for red, green, blue, and alpha channels.
Address in a customer relationship management (CRM) application as part of a customer entity. An address object might contain values for address line 1 and 2, zip code, and city.
Value objects never live alone. In a model, they are always part of an entity. As mentioned previously, a bank account has a balance property, which is of type money.
[ 44 ]
Chapter 3
Time for action – Creating a Name value object We want to create the implementation for a simple value object, we shall do so in the following example.
1.
In Visual Studio, create a new project. Select Class Library as the template and call this project OrderingSystem.
2.
Remove the class Class1.cs, that has been added by default, from the project.
3.
Add a folder called Domain to the project.
4.
Add a new class to the folder Domain of the project; call this class Name. As shown in the following code snippet: namespace OrderingSystem.Domain { public class Name { } }
5.
Add an auto-property LastName of type string to the class. Define the setter to be private, as shown in the following line of code: public string LastName { get; private set; }
6.
Add another auto-property FirstName of type string to the class. Also, define the setter to private. Do the same for a third and final property MiddleName. As shown in the following code snippet: public string FirstName { get; private set; } public string MiddleName { get; private set; }
7.
Add a constructor with the three parameters firstName, middleName, and lastName (all of type string). Assign the parameters to their respective properties. Do not allow null values to be passed for firstName and lastName; if a null value is passed, then throw a descriptive exception. Your code should now look similar to the following code snippet: public Name(string firstName, string middleName, string lastName) { if(string.IsNullOrWhiteSpace(firstName)) throw new ArgumentException("First name must be defined."); if(string.IsNullOrWhiteSpace(lastName)) throw new ArgumentException("Last name must be defined."); FirstName = firstName; MiddleName = middleName; LastName = lastName; } [ 45 ]
Creating a Model
8.
Override the method GetHashCode and return a value which is a combination of the hash codes of the three individual properties. Please consult the following link: http://msdn.microsoft.com/en-us/library/system.object. gethashcode.aspx, to get an detailed description of how to construct hash codes. Please note that if MiddleName is equal to null, then we take the value zero as its
hash code. Your code should look similar to the following code snippet:
public override int GetHashCode() { unchecked { var result = FirstName.GetHashCode(); result = (result*397) ^ (MiddleName != null ? MiddleName.GetHashCode() : 0); result = (result*397) ^ LastName.GetHashCode(); return result; } }
9.
To be complete we now have to override the Equals method, which accepts one parameter of type object. However, first we want to add an Equals method that accepts a parameter of type Name. In this method, we do three steps: 1. We check whether the passed parameter is null. If yes, then this entity and the one we compare to are not equal, and we return false. 2. Then, we check whether this and the other entity are the same instance. If yes, then we return true. 3. Finally, we compare each property individually. If all property values match, then we return true, else we return false. As shown in the following code snippet: public bool Equals(Name other) { if (other == null) return false; if (ReferenceEquals(this, other)) return true; return Equals(other.FirstName, FirstName) && Equals(other.MiddleName, MiddleName) && Equals(other.LastName, LastName); }
Implementing an Equals method, whose parameter is of the same type as the class itself, corresponds to the implementation of the IEquatable interface.
[ 46 ]
Chapter 3
10. Now override the method Equals and just forward the call to the previous overload of the method (do not forget to type cast the parameter):
public override bool Equals(object other) { return Equals(other as Name); }
Congratulations. You successfully implemented your first value object type. The class diagram is as shown in the following screenshot:
What just happened? In the preceding exercise, we created a value object. We have chosen the Name object as an example. The properties are the first, middle, and last name of a person. To guarantee that the Name object is truly an immutable object, its property values can only be set through the constructor. We have also implemented the Equals and GetHashCode methods of the Name object, so that the two instances of this value object can be compared.
Creating an entity We have identified various aspects that make up an entity. First of all, it is the ID which uniquely identifies and distinguishes it from another entity of the same type. Secondly, it is the attributes or properties that further describe the characteristics of an entity, and finally, it is the logic that describes the behavior of an entity. [ 47 ]
Creating a Model
Time for action – Creating a base entity First, we want to implement a base class for all types of entities. This class implements the logic around the ID which is common for all types of entities.
1.
Add a new class to the folder Domain of the project and call it Entity. Make the class abstract and generic in T. Your code should look similar to the following code snippet: using System; namespace OrderingSystem.Domain { public abstract class Entity where T : Entity { } }
2.
Add an auto-property ID of type Guid to the class. Make the setter of the property private. This will be our unique identifier for the entity. For a new entity, the ID will automatically have assigned the value of Guid.Empty, as shown in the following line of code: public Guid ID { get; private set; }
The ID property should have a private setter as we are never going to assign a value to ID through our application code. The value of ID is set by NHibernate through reflection at the moment the entity becomes persistent.
3.
Override the Equals method of the class. The code should deal with the following three cases: 1. The other entity (to which we compare this entity) is not of the same type, in which case the entities are not the same and we simply return false. 2. Both this entity and the other entity are new objects and have not yet been saved in the database. In this case, we consider the two objects to be the same entity, only if they point to the same instance in memory, or in .NET terminology, if their references are equal.
[ 48 ]
Chapter 3
3.
If the two entities we compare are of the same type and are not new, then we simply have to compare their IDs and find out whether they are equal or not. As shown in the following code snippet: public override bool Equals(object obj) { var other = obj as T; if (other == null) return false; var thisIsNew = Equals(ID, Guid.Empty); var otherIsNew = Equals(other.ID, Guid.Empty); if (thisIsNew && otherIsNew) return ReferenceEquals(this, other); return ID.Equals(other.ID); }
Entities that have never been saved to a database are called transient. Entities that have been saved to the database are called persistent.
4.
Whenever we override the Equals method, we also have to provide an implementation for the GetHashCode method. In this method, we have to just return the hash code of ID. Wait a second! There is a special case which we have to treat separately. This case results from the fact that an entity should never change its hash code as long as it is in memory. It is the case where the entity has been a new entity with an undefined ID and someone (for example, a HashSet or Dictionary, to which the entity has been added) has asked for its hash code. Later, this entity will be given an ID (most probably because it will be saved to the database and the data access layer will be assigning a value to the ID property). In this case, the entity can now not just return the hash code of the ID, but has to return the hash code that was calculated when it was still a new entity with an undefined ID. Considering this special case, our code should now look similar to the following code snippet: private int? oldHashCode; public override int GetHashCode() { // once we have a hashcode we'll never change it if (oldHashCode.HasValue) return oldHashCode.Value;
[ 49 ]
Creating a Model // when this instance is new we use the base hash code // and remember it, so an instance can NEVER change its // hash code. var thisIsNew = Equals(ID, Guid.Empty); if(thisIsNew) { oldHashCode = base.GetHashCode(); return oldHashCode.Value; } return ID.GetHashCode(); }
5.
Finally, we override the == and the != operators, such that we can compare two entities without having to use the Equals method. Internally, both methods just use the Equals method: public static bool operator ==(Entity lhs, Entity rhs) { return Equals(lhs, rhs); } public static bool operator !=(Entity lhs, Entity rhs) { return !Equals(lhs, rhs); }
What just happened? We have implemented a class that can be used as a base class for all the entities of our application. This class implements the ID property and the logic to use this ID to compare two instances of the same type of entity and determine whether they are equal or not. The code we have implemented is robust and deals with all possible edge cases.
Time for action – Creating a Customer entity Now, let's implement a real entity that inherits from the base entity. Here we can focus fully on the properties that describe the entity and on the methods that describe the entity's behavior.
1.
Add a new class to the folder Domain of the project and call this class Customer.
2.
Make the Customer entity inherit from the Entity base class. public class Customer : Entity { } [ 50 ]
Chapter 3
3.
In the class Customer, implement the following auto-properties: CustomerIdentifier of type string and CustomerName of type Name. Make the setters of the properties private, as shown in the following code snippet: public string CustomerIdentifier { get; private set; } public Name CustomerName { get; private set; }
4.
Implement a method ChangeCustomerName with the values: firstName, middleName, and lastName as parameters. The method changes the CustomerName property of the class. The code looks similar to the following code snippet: public void ChangeCustomerName(string firstName, string middleName, string lastName) { CustomerName = new Name(firstName, middleName, lastName); }
5.
In the following screenshot, we see a class diagram of the Customer entity we just implemented, together with its base class and the Name value object, which is used as a type of the CustomerIdentifier property of the Customer class.
[ 51 ]
Creating a Model
What just happened? We have implemented our first entity. The Customer entity we implemented inherits from the Entity base class we implemented in the preceding exercise. We also used the Name value object we defined earlier as the type of one of the Customer's properties.
Defining relations between entities Entities are one of the key concepts of a model. However, entities do not live in isolation; they are related to other entities. We can distinguish between different types of relations.
Owns or contains Value objects can never exist alone. They only become a meaning in conjunction with an entity. An entity can own or contain zero to many value objects. In the case of the previous Customer entity, the value object Name is owned or contained by the Customer entity. This relation is indicted by an arrow pointing from the entity to the value object, as shown in the following screenshot. Near to the arrow head, we find the name of the value object type property in the entity.
Note that there is no arrow pointing back from Name to Customer. The Name value object does not and should not know its owner. If we look at the code, then the relation is defined by implementing a property of type Name in the Customer class, as shown in the following code snippet: public Name CustomerName { get; private set; }
[ 52 ]
Chapter 3
One-to-many Let's now look at the two entities, Product and Category, we introduced in Chapter 2. How are they related to each other?
Each product belongs to exactly one category. We can thus define a property Category of type Category in the Product class. This property is said to be a reference to the category of the product. It can be used to navigate from the product to its associated category. This relation between product and category is marked with an arrow pointing from Product to Category in the following screenshot. The name (Category) of the property, which can be used to navigate from Product to Category, is marked near the arrow head. The code would look similar to the following code snippet: public Category Category { get; private set; }
Each category has many associated products. Consequently, we can define a Products property in the Category class, which is a collection or set of products. This relation is marked with a double-headed arrow pointing from Category to Product, as shown in the following screenshot. Again, the name of the property (Products) that is used to navigate from the Category to its associated products is marked near the arrow head. Here, the code might look similar to the following code snippet: private List products; public IEnumerable Products { get { return products; } }
In a real life inventory application, you will probably want to avoid putting a Products collection on the Category entity, as it is possible that a category can have hundreds, if not thousands, of associated products. To load the whole, huge collection of products for a given category would be unwise and would lead to an application having unsatisfactory response times. [ 53 ]
Creating a Model
One-to-one Sometimes, we encounter the situation where an entity acts in different roles. As an example, take a Person entity, which at a university can wear different hats. One and the same Person can work as Professor in a faculty of the university, but at the same time, can also be a Student in a different faculty. Such a relationship can be modeled as a one-to-one relationship. Another one-to-one relationship in the same domain would be the one between Professor and his possible role as HeadOfDepartment. In the following screenshot, we see a class diagram of the entities and the relationships mentioned previously.
Note that we can navigate from a Person object to its associated Professor object through the property Professor. We can also navigate from a professor object to the corresponding person object via the property Person of the professor entity. This is indicated by the two arrows in the preceding screenshot, one pointing from the Person to the Professor and the other pointing in the opposite direction. In a similar way, we can navigate from Person to Student and back, as well as from Professor to HeadOfDepartment and back.
[ 54 ]
Chapter 3
Many-to-many The last type of relation we want to discuss is a many-to-many relation. What does this mean? Let's look at a concrete example: the relation between an order and a product. A customer wants to order a product. However, the customer does not want to order only one product, but several different products. Thus, an order can contain many products. On the other hand, several different customers can place orders for one and the same product. Therefore, a product can belong to many orders. Another example would be the relation between a book and an author. An author can write many different books, while a book can have many authors. Both of these relations are examples for a many-to-many relationship, as shown in the following screenshot:
However, there are subtle differences between the two. We can leave the latter relationship as it is; it is a real many-to-many relation. Nevertheless, we need to discuss the relation between product and order a little more. Thinking a bit more about the process of placing orders, we will realize that there is some concept missing. A customer might not only want to order one unit of a product, but maybe several. Also, we might want to know the unit price of the product at the time when the order was placed and the discount applicable to the specific product. All of a sudden, a new intermediate entity (a relational entity – it relates product and order) is born. We often call this intermediate entity a line item of an order. We can change our diagram and make it look similar to the following screenshot:
[ 55 ]
Creating a Model
Evidently, we have no more many-to-many relations in this part of the model, but rather reference type and one-to-many relations. Specifically, an order has many line items and each line item references a product.
The order entry model Now that we have discussed in detail all the elements comprising a model, it is time to implement a simple yet realistic and complete example domain model. This model shall provide us with a better understanding of the domain our Customer, for which we built the application, works in.
Time for action – Implementing an order entry model The context of the model is an order entry system. The model shall be used as a base for a solution that helps to enter orders into the system. Those orders are placed by customers via phone calls.
1.
Take a piece of paper and try to identify entities that play a role in this context. Draw a box for each entity identified. Try to come up with a solution without reading ahead!
2.
Define the relations between the entities by drawing connecting lines between them. Try to define the type of relation, whether it is a one-to-many, or a reference, and so on.
3.
For each entity, define and write in the corresponding box the most important properties.
4.
Try to identify sets of properties in your entities that are good candidates for value objects.
5.
You should have come up with a model that looks similar to the following screenshot. Note: to keep things manageable, the model only shows a very simplified version of an order entry system. In reality, the domain would have many more entities and the entities themselves would have additional properties not shown here.
[ 56 ]
Chapter 3
6.
In Visual Studio, open the solution OrderingSystem.
7.
First, we create the value objects we identified in the model. a. We have already defined the class Name, which is a value object and consists of the three properties: LastName, FirstName, and MiddleName. (Both the Employee and the Customer entities have a property of type Name.) b. Add a class Address to the Domain folder of the project and add the following properties to it (all of type string): Line1, Line2, ZipCode, City, and State. Override the Equals and GetHashCode methods as we did in the Name class. The code should look similar to the following code snippet: public bool Equals(Address other) { if (other == null) return false; [ 57 ]
Creating a Model if (ReferenceEquals(this, other)) return true; return Equals(other.Line1, Line1) && Equals(other.Line2, Line2) && Equals(other.ZipCode, ZipCode) && Equals(other.City, City) && Equals(other.State, State); } public override bool Equals(object obj) { return Equals(obj as Address); } public override int GetHashCode() { unchecked { var result = Line1.GetHashCode(); result = (result * 397) ^ (Line2 != null ? Line2.GetHashCode() : 0); result = (result * 397) ^ ZipCode.GetHashCode(); result = (result * 397) ^ City.GetHashCode(); result = (result * 397) ^ State.GetHashCode(); return result; } }
8.
We have already defined the class Entity in the project and will use this class as the base class for all the other entities we are going to add to the model.
9.
Now, let's add a class for each entity to the project. Each of these classes inherits from Entity: a. To the Domain folder add a class Employee, which inherits from Entity. b. We have already added a class Customer, which inherits from the Entity earlier in this chapter; thus nothing to do here. c. To the Domain folder add a class Order, which inherits from Entity. d. To the Domain folder add a class LineItem, which inherits from Entity. e. To the Domain folder add a class Product, which inherits from Entity. [ 58 ]
Chapter 3
10. Add a property Name of type Name to the Employee class. 11. Add an additional property Address of type Address to the Customer class. 12. Also, add a read-only collection of Orders to the Customer class, as shown in the following code snippet:
private readonly List orders; public IEnumerable Orders { get { return orders; } }
13. Add properties Customer of type Customer, Reference of type Employee,
OrderDate of type DateTime, and OrderTotal of type decimal to the Order
class.
14. Add a read-only collection of LineItems to the Order class, as shown in the following code snippet:
private readonly List lineItems; public IEnumerable LineItems { get { return lineItems; } }
15. Add a constructor as shown in the following code snippet to the Order class: public Order(Customer customer) { lineItems = new List(); Customer = customer; OrderDate = DateTime.Now; }
16. Add properties Order (Order), Product (Product), Quantity (int), UnitPrice (decimal), and Discount (decimal) to the LineItem class.
17. Now, add a constructor to the LineItem class, which initializes its properties with
values passed in (specifically, note how UnitPrice and Discount are initialized):
public LineItem(Order order, int quantity, Product product) { Order = order; Quantity = quantity; Product = product; UnitPrice = product.UnitPrice; if (quantity >= 10) Discount = 0.05m; }
[ 59 ]
Creating a Model
18. Add the properties Name (string), Description (string), UnitPrice (decimal), ReorderLevel (int), and Discontinued (bool) to the Product class.
Now we have defined our domain entities, their relations among each other, as well as their properties. The result of our implementation is what we call an anemic domain model as it contains no business logic and the entities are mere data containers. Let's add some logic to the entities. We want to implement the user story "place order for existing customer". A Customer orders several different products. Of each product, they order several items. The preceding user story can result in a command that is sent from the user interface of our application to the domain model. The command contains a list of LineInfo objects that each contain the number of items and the ID of the product that the Customer ordered.
19. Add a class LineInfo to the folder Domain of the project. This class will be used as a data transfer object (DTO) and is defined as shown in the following code snippet:
public class LineInfo { public int ProductId { get; set; } public int Quantity { get; set; } }
20. Let's add a method PlaceOrder to the class Customer. Inside the method, we add code to create a new order and add a product to the order for each passed
LineInfo. Our code should look similar to the following code snippet: public void PlaceOrder(LineInfo[] lineInfos, IDictionary products) { var order = new Order(this); foreach (var lineInfo in lineInfos) { var product = products[lineInfo.ProductId]; order.AddProduct(this, product, lineInfo.Quantity); } orders.Add(order); }
[ 60 ]
Chapter 3
Note that the products dictionary that is passed in as a second parameter contains all products, which are referenced by one of the LineInfo items through its ProductID. The origin of this dictionary is not of interest in this chapter and will be discussed in a later chapter.
21. Now, we have to define the AddProduct method of the Order class. This method internally creates a new LineItem object and adds it to the order's line item collection, as shown in the following snippet of code: public void AddProduct(Customer customer, Product product, int quantity) { Customer = customer; var line = new LineItem(this, quantity, product); lineItems.Add(line); }
What just happened? Hurray! We have successfully defined the model for a simple order entry system. Our model contains all key elements of a typical business context model. Of course, to make our model fully functional, we would have to implement more user stories similar to the "place order" story.
Pop quiz – Identifying entities 1. An entity always has an identifier. This identifier is used to: a. Uniquely identify an entity. b. Compare two entities of the same type for equality. c. Both of the above.
Have a go hero – Extending the model Try to extend the order entry model and add an EmployeePhoto entity, which has a oneto-one relation with the Employee entity and contains properties to deal with an image, the title of the image, and a short description of the image.
[ 61 ]
Creating a Model
Summary In this chapter, we have learned what a model is and how it helps us to better understand the domain for which we may want to implement an application. We have learned that entities and their relations among each other play a key role in a model. We discussed the notion of identity and the life cycle of an entity. Also, we have introduced the concept of value objects and pointed out its differences with the entity. At no point did we have to reference NHibernate or make any compromises just to satisfy the needs of the NHibernate ORM framework. This is a very positive factor as it helps us to keep focused on the context and not introduce unnecessary complexity, which has nothing to do with the domain context at hand. Last but not least, in this chapter we have also briefly contrasted the model first versus the data first approach when designing a solution. Now we are ready to think about how we are going to persist the data that our model produces in a database. That is where NHibernate comes to play. First, we will discuss how to map our model, such that it is compatible with a relational data store. This will be the topic of the next chapter.
[ 62 ]
4
Defining the Database Schema In this chapter, we will learn how data produced by our applications can be stored in a relational database and how this data is organized inside the database.
In this chapter, we shall:
Learn what a table is
Learn how tables are related to each other
Discuss strategies to constrain what data can be stored
Show what possibilities we have to improve the performance of the data access
Create the OrderingSystem database
So let's get on with it...
What is a database schema? Every realistic application produces and manipulates data. This data has to be stored somewhere where it can be protected from such things as power outage, or the fact that even today our computers do not have an infinite amount of memory, while the amount of data has grown to the size of terabytes or exabytes. Although many different types of data stores exist, for example, XML, document databases, Google Big Table, and so on, in this book, we want to concentrate solely on relational database management systems (RDBMS) such as Microsoft SQL Server.
Defining the Database Schema
In a relational database, the database schema is a description of the structure of the database, and as such, describes how the data is organized inside the database and is divided into database tables. The schema also contains specifications about the type of data that can be stored, the relations of the database tables among each other, and the access strategies selected when accessing the data. In a relational database, a schema defines tables, fields or columns, relationships, indexes, constraints, views, and stored procedures, among others.
Who owns the database? The content of this book is based on the assumption that the database is owned by the application. This implicates that the database should only be manipulated by one single application, your application. No other application should have direct access to your database. Access to the data in the database for other applications has to be provided through APIs controlled by your application, for example, in the form of web services. Given this boundary condition, we can now leverage this to our advantage:
We can create the database schema in a way that optimally fits our domain model and we do not have to make any compromises
We can rely on the fact that the data in the database is correct as it is only written by our application (if, on the other hand, we find that there is bad data in the database, then we know it must be due to defects in our own application)
Many people will disagree with that assumption. Why? In the past, we have been unanimously told that the truth lies in the database. There have been heroic attempts to create centralized databases that contain all data of an enterprise such as a bank. All applications that were written in the respective enterprise would access and use this central data store. Although there were very good reasons to try to implement such a central data store, the negative side effects outweighed the positive factors by far. Autonomous systems are far more flexible than centralized and strongly coupled systems.
Time for action – Creating the OrderingSystem database In this first exercise, we want to create a brand new SQL Server database.
1.
Open SQL Server Management Studio (SSMS) and log in to your SQL Server Express.
[ 64 ]
Chapter 4
2.
In the Connect to Server dialog, choose Database Engine as Server type. We are accessing the locally installed database server, thus enter either .\SQLExpress or (local)\SQLEXPRESS as Server name. Finally, select Windows Authentication as the Authentication method and click on Connect, as shown in the preceding screenshot.
3.
You will be presented with a screen similar to the following screenshot. This shows a basic summary of the SQL Server instance that you are connected to, such as the databases on the server, security information (logins, roles), and so on.
[ 65 ]
Defining the Database Schema
4.
In the Object Explorer window, right-click on the Databases node and select New Database….
5.
In the New Database dialog, enter OrderingSystem as Database name, as shown in the following screenshot. Leave all other settings as their default values and click on OK.
6.
The newly created database appears as a sub-node of the Databases folder. Expand this new node and you will see several sub-nodes, as shown in the following screenshot:
[ 66 ]
Chapter 4
7.
You will see the collapsed folders for Tables and Views, as well as Database Diagrams and Security. These are the most common objects we will use within SSMS to create database objects and manage their security permissions.
What just happened? We have just created our basic ordering system database. We will continue to use this database throughout this chapter and throughout the entire book. Now that we have our database, we can move on to creating a structure to store our data!
Laying the foundation – table layouts The first and foremost important element of a schema is the table. Data is stored inside tables. A table is comparable to a spreadsheet, as you might be familiar with from tools such as Microsoft Excel. It consists of a collection of rows and columns. While the rows of a table represent the actual data, the table contains the columns that define what type of data is stored in a table. In the following screenshot, we have a sample of a very simple table storing product category data:
The table name is Categories, which denotes what we also call the database entity we are dealing with. The column headers represent the attributes of the database entity. In this case we have CategoryId, Name, and Description properties. Each row of the table contains the data of exactly one instance of a product category. Consequently, we have as many different categories as there are rows in the table. The preceding screenshot provides the data view of a table. Also, what is interesting is the structural view of a table, as shown in the following screenshot. It shows us the metadata of the table. In this screenshot, we can see some of this metadata. We can see that:
The table has three columns with the names CategoryId, Name, and Description.
CategoryId has a data type of int, while Name is a Unicode character string with a maximum of 50 characters, and Description is also a Unicode character string but of MAX length. On SQL Server, MAX stands for a character string of up to 4 GB. [ 67 ]
Defining the Database Schema
CategoryId and Name cannot be undefined or null. Description is an optional bit of information, and thus there is a checkmark in the Allow Nulls column.
CategoryId is the Primary Key field denoted with the small key symbol on the left-hand side.
Time for action – Creating the Categories table In this second exercise, we want to create the first table in our OrderingSystem database.
1.
Open SSMS and in the Object Browser expand the Databases folder. Locate the OrderingSystem database we created in the previous exercise and expand it.
2.
Right-click on the Tables folder and select New Table…, as shown in the following screenshot:
[ 68 ]
Chapter 4
3.
SSMS will open a new tab where we can now define the details of the new table. The new table editor tab will look similar to the table in the following screenshot. In the Column Name box, enter Id, either type or select int in the Data Type field, and uncheck the Allow Nulls checkbox.
4.
In the second row, add Name as Column Name, nvarchar(50) as Data Type, and also leave Allow Nulls unchecked.
5.
Add a row for each of these: a. Column name UnitPrice, data type money, and Allow Nulls unchecked. b. Column name ReorderLevel, data type int, and Allow Nulls unchecked. c. Column name Discontinued, data type bit, and Allow Nulls unchecked. d. Column name Description, data type nvarchar(MAX), and Allow Nulls checked, as shown in the following screenshot:
[ 69 ]
Defining the Database Schema
6.
Select the first row where you have defined the Id column and right-click on this row, as shown in the following screenshot:
7.
Select Set Primary Key. A small key symbol should appear left of the column name.
8.
Finally, we need to save our table and give it a name. You can save the file by clicking on the Save (floppy disk) icon, pressing Ctrl + S, or selecting File | Save Table_1. Any of these options will bring up the Save Table dialog box, which will prompt us to choose a name for our table, as shown in the following screenshot:
9. Press CTRL + S to save the table. In the Choose Name dialog box, enter Products as the name for the new table and click on OK.
[ 70 ]
Chapter 4
What just happened? Using SSMS, we have just defined our first database table that we will use later on to store our product entities. We have used the visual designers of SSMS to define the columns of the database table and to also define which column of the table is the Primary Key.
Time for action – Defining a script to create the Products table The database table designer comes in very handy when we want to quickly define a new table as a one-time action. However, if we want to redo the same action again and again, then defining a script is better. In this exercise, we will define a SQL DDL script which generates the product table for us. DDL is the acronym for Data Definition Language or Data Description Language and is a computer language for defining data structures such as tables. Its sister DML, which stands for Data Manipulation Language, is a computer language used to insert, update, and delete data in the database.
1.
In your editor of choice, (I chose Notepad++, a free Notepad clone) open a new text file.
2.
Enter SQL code to create a Products table, as shown in the following lines of code: create table Products( ) GO
3.
For each column we want to define for this table, add a line containing first the name of the column and then its data type. Do this for Id (int), Name (nvarchar(50)), UnitPrice (money), ReorderLevel (int), Discontinued (bit), and Description (nvarchar(MAX)).
4.
If a column must be defined, then add a NOT NULL after the respective column type.
5.
Add code to define the Id column to be the primary key of the table.
[ 71 ]
Defining the Database Schema
6.
Your code should look similar to the following code snippet: create table Products ( Id int not null, Name nvarchar(50) not null, UnitPrice money not null, ReorderLevel int not null, Discontinued bit not null, Description nvarchar(MAX), primary key (Id) ) GO
7.
Save the file as 0001_create_Products_table.sql.
What just happened? Using a simple text editor, we have created a SQL DDL script which when executed in the database creates the Products table for us. Creating scripts to manipulate the database schema is the preferred way over manipulating the schema with a designer such as SSMS whenever we want to repeatedly execute the same action.
Table columns The columns of the table define what kind of information we can store in a table. If we make a comparison between a table and an entity type (a C# class), then we can say that the table column is equivalent to a property of the entity. To define a property on a class, we have to give it a name and a data type. Similarly, a table column has a name and a data type. We can define even more characteristics on a column such as a restriction whether the column is nullable or not. By the way, null is a database term used to define an undefined value.
Data types of table columns You might have already noticed that the data types used for table columns differ quite a bit from those you generally use when defining properties of a C# class. The only column data type that looks familiar so far is int. This is a fact we have to live with. It is even worse as every database product has its own specific types. Oracle column types are different from those of Microsoft SQL Server, which in turn are different from the column types used by MySQL, and so on. Luckily, NHibernate does a good job in automatically converting the data types for us from .NET types to database types and vice versa. However, it is still important to have a good understanding of those differences.
[ 72 ]
Chapter 4
The following table lists the most common .NET types and its counterparts in MS SQL Server and in Oracle: .NET
SQL Server
Oracle
Comment
int
int
NUMBER(p,0)
p is the precision and specifies the maximal
decimal
money or
NUMBER(p,s)
number of decimal digits that can be stored.
s is the scale and specifies the maximal number of decimal digits that can be stored to the right of the decimal point.
decimal(p,s)
For example, NUMBER (7,2) has 5 digits before and 2 digits after the decimal point. string
nvarchar(50)
VARCHAR2(50)
For short strings of up to 4000 characters. Stores Unicode characters.
string
nvarchar(MAX)
NCLOB
For strings of a length of up to 4 GB.
bool
bit
CHAR(1)
Oracle does not have a Boolean type. Often it is simulated by a char(1) column containing either Y or N.
DateTime
datetime
TIMESTAMP
Date and time.
DateTime
date
DATE
Date only.
byte[]
varbinary(n)
RAW
Variable length binary strings of up to 2000 bytes. n is the max length of the array.
byte[]
varbinary(MAX)
BLOB
Stores unstructured binary large objects of up to 4 GB.
Stores Unicode characters.
In contradiction to the common type system of .NET, a unified type system for databases does not exist. Each database vendor or provider has its own specific type system.
Relations, constraints, and indices So far, we have seen that a relational database allows us to organize our data into different tables. To bring even more structure to how the data is stored and what data is stored, relational databases use constraints and relations. Furthermore, we can define indexes to optimize the access of the data.
[ 73 ]
Defining the Database Schema
Relations Relations are used to associate data in different tables. Only through these relations, data stored in a database has some business-relevant meaning. Otherwise, the data would just be a bunch of unrelated sets of facts. Relational databases are called relational due to this fact that the data is related among different tables. As an example, let's look at the connection between the data of a customer and the data of orders placed by this customer. How is this relation defined in a relational database system? For example, let's take a single customer record. This record can be considered the parent of some order records. We have a so called parent–child relation, where the order records are the children of the customer record. The customer record is uniquely identified by its Primary Key. Each child record (that is, each order record) now stores the value of its parent's Primary Key in a special column. This column is then called the foreign key. As a result, an order record is related to its parent via this foreign key. We can visualize this relation as follows:
The line between the Customers and the Orders tables has a key symbol at the end pointing to the Customers table and an infinity symbol (∞) at the other end towards the Orders table. This means that the parent record which is referenced via its Primary Key is in the Customers table, while the child records are in the Orders table. The Orders table contains a field CustomerId which is used as the foreign key. The infinity symbol also indicates that there can be many order records associated with a single customer record. There exist three types of relations in a relational database. It is the same kind of relations that we discussed in the previous chapter when we discussed the relations between entities in a model:
One-to-many relations: One parent record can have zero to many associated child records. A good example for this type of relation is the Customers to Orders relation just discussed. [ 74 ]
Chapter 4
One-to-one relations: This is a degenerated one-to-many relation where a parent record can have exactly one associated child record. Often the records having such a relation share the same Primary Key. No special foreign key field is then needed in the child table. A good example of such a relation is the relation between a product record and its associated product photo record. This can be visualized as shown in the following screenshot:
Note the key symbol on both sides of the connecting line between Products and ProductPhotos.
Many-to-many relations: A parent record can have many associated child records. Each child record in turn can have many associated parent records. The sample from the ordering system domain is the relation between orders and products. A single order can contain many products, and each product can be present in many different orders. Another familiar relation of this type is the relation between a book and an author. A book can be written by many authors and an author can write more than one book. To establish such a relationship, we need a helper table that mitigates between the two ends. This table is called a relational table and has no other purpose than to relate two database entities or tables. In the case of the order to product relation, it would be the LineItems table. This can be visualized as shown in the following screenshot:
[ 75 ]
Defining the Database Schema
In the preceding screenshot, the ends of the lines with the key symbol are pointing towards the Orders and Products tables, indicating that those tables contain the Primary Keys. On the other hand, the LineItems table contains two foreign key fields: one for the Orders (OrderId) and one for the Products table (ProductId). Often, relational tables are only used to connect two other tables in a many-to-many relationship. In this case, those relational tables only consist of foreign keys pointing to the respective records in the tables which they connect. However, sometimes it makes sense to store some additional information in these relational tables, and thus add characteristics to a relation. One such example is LineItems which connects Orders and Products. Typically, we store information like Quantity, Discount, and so on in the LineItems table.
Constraints Constraints are used to limit the data that can be saved in a table. Constraints are defined for a table column. Various constraints exist:
Not null constraints: When storing data in a table, the value of the column that is defined as NOT NULL cannot be undefined. This constraint is useful in all situations where the existence of a value is mandatory, for example, the name of a product must be defined.
The data type: The data type of a column is also a kind of constraint as it limits the possible values that the column can contain to a well-defined subset. As an example, the ReorderLevel of the product must be an integral number.
Check constraints: In most modern relational databases, we can define formulas to further limit the values that can be stored in a database column. As an example, we would not want the ReorderLevel to be a negative number and can add a check constraint to avoid this.
Foreign key constraints: This constraint ensures that there cannot be a foreign key of a child table pointing to a non-existing parent record in the parent table. For example, a product cannot have a foreign key CategoryId with a value of 1 if no category with Id equal to 1 exists.
[ 76 ]
Chapter 4
Time for action – Adding a constraint to the Product table In this exercise, we will use the table designer of SSMS to define a check constraint to the column ReorderLevel of the Products table.
1.
In SSMS, locate the table Products, right-click on it and select Design. The table designer for the Products table opens in a new tab.
2.
Inside the table designer, right-click in any column and select Check Constraints…. The Check Constraints dialog window opens.
3.
Click on Add to add a new check constraint.
4.
Change the (Name) to CK_Product_ReorderLevel.
5.
In the Expression field, add ReorderLevel>=0, as shown in the following screenshot:
6.
Click on Close to dismiss the dialog window and return to the table designer.
7. Press CTRL + S in the table designer to save the changes.
[ 77 ]
Defining the Database Schema
8.
Verify your changes by right-clicking on the Constraints folder of the Products table in the Object Explorer and selecting Refresh. A new child node CK_Products_ ReorderLevel should appear, as shown in the following screenshot:
What just happened? In the preceding exercise, we have used SSMS to create a check constraint for a column of the Products table. The check constraint assures that we cannot write a negative value into the column ReorderLevel.
Time for action – Creating a script to add a check constraint Now we want to define the script which allows us to add a check constraint to the Products table.
1.
Open the file 0001_create_Products_table.sql we created during the preceding exercises.
2.
At the very end of the file, add a statement to delete the check constraint to be defined, if it already exists. The code should look similar to the following code snippet: IF EXISTS (SELECT * FROM sys.check_constraints WHERE object_id = OBJECT_ID('CK_Products_ReorderLevel') AND parent_object_id = OBJECT_ID('Products')) ALTER TABLE Product DROP CONSTRAINT CK_Products_ReorderLevel GO [ 78 ]
Chapter 4
3.
Add code to add a check constraint to the column ReorderLevel of the Products table which ensures that no value less than zero can be entered. You should come up with something like the following code snippet: ALTER TABLE Products ADD CONSTRAINT CK_Products_ReorderLevel CHECK (ReorderLevel>=0) GO
4.
Save the file.
What just happened? Using a simple editor, such as Notepad, we have defined a script which allows us to create a check constraint for the ReorderLevel column of the Products table. This script can now be used and reused again many many times. While, if we use the designer, we would have to repeat the same steps over and over again.
Indices A database uses indices to (sometimes) dramatically improve the data access speed. Indices can be created using one or more columns of a database table. If there was no such thing as indices in a database, and if the database contained a table and this table grows in size over time and might contain millions of records, then the access speed would be extremely disappointing. Why? Well, given a certain query, the database would have to traverse the whole table to locate the requested data. This is also called a full table scan. By using indices, the database can optimize the query and avoid traversing the whole table. When defining an index on a table field or column, the database system creates a structure which contains the values of the sorted indexed field together with the corresponding Primary Key. As the indexed values are sorted, the database can now find the requested values much faster by using a B-tree algorithm. Once the values are located, then the database uses the associated Primary Keys to access the full records in the table.
[ 79 ]
Defining the Database Schema
Time for action – Adding an index using the designer In this exercise, we want to add an index to the Products table. In our ordering system, we want to have the possibility to search for products by name. To make this search fast, we define an index for the Name column.
1.
In SSMS, locate the Products table in the Object Explorer.
2.
Right-click on the Products table and select Design. The designer for the Products table will open in a new tab.
3.
Right-click on any of the columns in the Products designer and select Indexes/Keys…, as shown in the following screenshot. The Indexes/Keys... dialog window will open.
4.
Click on Add to add a new index and set the properties as follows: a. Change the (Name) to IX_Products_Name. b. Choose Name (ASC) as the column name to be indexed. c. Set Type to Index. d. Set Is Unique to Yes.
[ 80 ]
Chapter 4
5.
Leave all the other settings as the default values, as shown in the following screenshot:
6.
Click on Close to close the Indexes/Keys... dialog window.
7.
Back in the Products table designer, press CTRL + S to save the newly created index.
8.
Verify that the index has been created by right-clicking on the Indexes node of the Products table in the Object Explorer and choosing Refresh. The new index IX_ Products_Name should appear as a new child of the Indexes folder, as shown in the following screenshot:
[ 81 ]
Defining the Database Schema
What just happened? Using the visual designer of SSMS, we have added an index to the Products table. The index we defined allows us to efficiently query and filter the name of the product records stored in the database table. Our queries will remain efficient, even if the content of the table grows to many millions of entries.
Time for action – Creating a script to add an index In this exercise, we want to create a SQL DDL script to add the same index to the Products table as defined in the preceding exercise using the SSMS table designer.
1.
Open the 0001_create_Products_table.sql file we created during the preceding exercises.
2.
At the very end of the file, add a statement to delete the IX_Products_Name index if it already exists. The code is as shown in the following code snippet: IF
EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('Product') AND name = 'IX_Products_Name') DROP INDEX IX_Products_Name ON Product GO
3.
Add an alternate table statement to add the index to the Products table. Your code should look similar to the following code snippet: CREATE UNIQUE INDEX IX_Products_Name ON Product (Name ASC) GO
4.
Save the file.
What just happened? Once again, we have used a simple editor to define a script which adds an index to the Name column of the Products table. Also pointed out are the advantages of defining scripts over doing the same work by using the various visual designers of SSMS is the repeatability of the work. Scripts can be applied repeatedly without any further work, whereas visual designers always require the work to be redone over and over again.
[ 82 ]
Chapter 4
Normal form To optimize data storage and retrieval, and to avoid errors introduced by redundant data, relational databases normalize data. The process of organizing data to minimize redundancy is called normalization. Edgar F. Codd, the inventor of the relational model, which is the base of relational databases, introduced the concept of normalization. He defined what we now know as the First Normal Form (1NF) in 1970. A year later he introduced and defined the Second and Third Normal Form (2NF and 3NF).
1NF: A table is free of repeating groups.
2NF: Non-key attributes must depend on the whole key.
3NF: Non-key attributes are dependent on "nothing but the key".
To be 1NF compliant, we need to eliminate duplicate columns from the same table, create separate tables for each group of related data, and identify each row with a unique column or set of columns (the Primary Key). In other words, we don't want to store duplicate data, we want to store it once and relate to it. Essentially, a 3NF database will store data in multiple tables to normalize the data and reduce duplication as we talked about earlier, and additionally:
Functional dependencies on non-key fields are eliminated by putting them in a separate table. At this level, all non-key fields are dependent on the Primary Key.
A row is in 3NF if, and only if, it is in 2NF and if attributes that do not contribute to a description of the Primary Key are moved into a separate table.
Putting it all together Now that we have all the concepts of database tables and relationships sorted out, let's add some tables to our OrderingSystem database.
Time for action – Creating a schema for the order entry system The goal of this exercise is to create a schema that matches the needs of our model to store data. From the preceding exercises, we already have the table Products defined in the OrderingSystem database. So let's add the remaining tables:
1.
Open SSMS and locate the OrderingSystem database in the Object Explorer. Expand the OrderingSystem node such that the Tables folder is visible.
2.
We want to define the Employees table. Right-click on the Tables folder and select New Table…. A new tab opens with the table designer for the new table. [ 83 ]
Defining the Database Schema
3.
Add a column Id of type int. Right-click on the column and select Set Primary Key to make it the Primary Key of the table. Note that the Allow Nulls is automatically set to false as a Primary Key can never be null.
4.
Add columns LastName, MiddleName, and FirstName, all of type nvarchar(50). LastName and FirstName have Allow Nulls unchecked, as shown in the following screenshot:
5. Press CTRL + S to save the table and when asked, name it Employees. 6.
Now we define the Customers table. Right-click on the Tables folder in the Object Explorer and select New Table….
7.
Add a Primary Key column Id of type int.
8.
Add a column CustomerIdentifier of type nvarchar(50) and set Allow Nulls to false.
9.
Add columns LastName, MiddleName, and FirstName all of the type nvarchar(50). Set Allow Nulls for LastName and FirstName to false.
10. Add columns Line1 and Line2 of type nvarchar(100) and for Line1 set Allow Nulls to false.
11. Add a column ZipCode of type nvarchar(10) and set Allow Nulls to false. 12. Add a column City of type nvarchar(50) and set Allow Nulls to false. 13. Add a column State of type nchar(2) and set Allow Nulls to false. 14. Press CTRL + S to save the table and when asked name it Customers. 15. Next, we define the Orders table. Select New Table… to add a new table. 16. Add a Primary Key column Id of type int. 17. Add a field CustomerId of type int. Set Allow Nulls to false. This is the foreign key to the table Customers.
[ 84 ]
Chapter 4
18. Add a field EmployeeId of type int. Set Allow Nulls to false. This is the foreign key to the table Employees.
19. Add a field OrderDate of type datetime. Set Allow Nulls to false. 20. Add a field OrderTotal of type money. Set Allow Nulls to false, as shown in the following screenshot:
21. Press CTRL + S to save the table and give it the name Orders. 22. Now, select New Table… again to define the LineItems table. 23. Add a Primary Key column Id of type int. 24. Add a field OrderId of type int. Set Allow Nulls to false. This is the foreign key to the table Orders.
25. Add a field ProductId of type int. Set Allow Nulls to false. This is the foreign key to the table Products.
26. Add a field UnitPrice of type money. Set Allow Nulls to false. 27. Add a field Discount of type money. Set Allow Nulls to false. 28. Add a field Quantity of type int. Set Allow Nulls to false. 29. Press CTRL + S to save the table and give it the name LineItems. We have defined all tables that make up our ordering system. It is now time to define the relations among those tables.
30. Open the table designer for the LineItems table. 31. Right-click inside the designer and select Relationships…. The Foreign Key Relationships dialog window will open.
32. Click on Add to add a new relationship. [ 85 ]
Defining the Database Schema
33. Change the (Name) to FK_LineItems_Products as we want to define a relationship between the LineItems and the Products table, as shown in the following screenshot:
34. Click on the … button next to the Tables and Columns Specification. The Tables and Columns dialog window will open.
35. Select Products as Primary key table and Id as the column in the Primary Key table.
36. Select the column ProductId of the table LineItems as the foreign key column, as shown in the following screenshot:
[ 86 ]
Chapter 4
37. Click on OK to accept the settings. Back in the Foreign Key Relationships dialog window, click on Close to dismiss the dialog and return to the table designer.
38. In the table designer, press CTRL + S to save the changes. A window pops up with a
warning that the two tables Products and LineItems will be saved to the database. Click on Yes to confirm and continue, as shown in the following screenshot:
[ 87 ]
Defining the Database Schema
39. Repeat steps 31 through 38 to add a foreign key relationship between the
LineItems and the Orders tables. Call the relation FK_LineItems_Orders. Link the OrderId field of the LineItems table to the Id field of the Orders table.
40. Open the table designer for the Orders table. 41. Repeat steps 31 through 38 to add a foreign key relationship between the Orders and the Customers tables. Call the relation FK_Orders_Customers. Link the CustomerId field of the Orders table to the Id field of the Customers table.
42. Repeat steps 31 through 38 to add a foreign key relationship between the Orders and the Employees tables. Call the relation FK_Orders_Employees. Link the EmployeeId field of the Orders table to the Id field of the Employees table.
We have completed the definition of our OrderingSystem database. The database schema can be visualized, as shown in the following screenshot:
[ 88 ]
Chapter 4
What just happened? In the preceding exercise, we have used SSMS to create the database schema for our ordering system. We have created all tables as well as their respective Primary Key and the foreign key relationships between the tables.
Have a go hero – Adding an additional table to the schema Earlier in this chapter, when discussing one-to-one relationships, we mentioned a ProductPhotos table. Try to add such a table to the schema and also define the relationship between this new table and the Products table.
Have a go hero – Authoring a DDL script to generate the schema Try to write a script that creates the whole ordering system schema in one go, including all tables, primary key definitions, and foreign key relations.
Do not use database-generated IDs Most modern database servers, such as MS SQL Server or Oracle, offer the possibility to use database-generated IDs for Primary Keys. In SQL Server, we can auto generate IDs by defining the column to be an identity column. In Oracle, we would use sequences to generate new IDs. When using NHibernate or any other ORM framework, you will want to avoid this feature of the database as it has many drawbacks. One of the criteria for a good Primary Key is that it is assigned by an NHibernate Persistent Object ID (POID) generator. Automatic assignment lets NHibernate manage the assignment of Primary Keys. NHibernate has the "smarts" baked right in to create those IDs for us and put them back into our object. Whenever a record is inserted into the database, it is assigned a number, either the next number in the sequence (that is, high/low), or a randomly assigned GUID (Globally Unique Identifier), depending on which POID you are using. We will talk more about POID generators and why to use them instead of database-generated IDs in the next chapter.
[ 89 ]
Defining the Database Schema
Views Views are database objects that are often used to provide read-only access to data stored in the various tables of the database. Views can provide a partial view of a single table or a denormalized view of several joined tables. In this regard, views are very useful for query operations and less useful for data manipulation operations such as creating and updating data. Views can be regarded as virtual tables. No data is stored in a view. Whenever we query a view, the data is retrieved on the fly from the underlying tables. Note that MS SQL Server and Oracle allow the definition of materialized views. A materialized view is a database object that contains the result of a query. Or put in other words: the result of the query is precalculated and readily available when needed. In this context, when talking about views, we are not referring to this special type of views.
As an example, let's assume that we want to show in our application a list of the customers with orders that have order totals higher than, let's say, 1000 US$. We can easily get to this data by using a view which combines for us the necessary tables combining the requested values. The following code snippet generates a view which joins tables Customers and Orders, and filters the data for records whose OrderTotal is greater than or equal to 1000 US$. This view can then be used by our application to retrieve the desired data: create view CustomerOrderTotals as select c.Id, c.FirstName, c.MiddleName, c.LastName, o.OrderDate, o.OrderTotal from Customers c join Orders o on c.Id = o.CustomerId where o.OrderTotal >= 1000;
What about stored procedures and triggers? Most modern RDBMSs offer the possibility to define logic inside the database. This can be done in the form of either stored procedures and/or triggers. A database trigger is procedural code that is automatically executed in response to certain events on a particular table or view in a database. Such events can be, for example, the insertion or deletion of a record in a table. The trigger is mostly used to maintain the integrity of the information in the database or to create audit logs.
[ 90 ]
Chapter 4
A stored procedure is procedural code that is available to applications accessing the database. The code is stored and executed inside the database. Typical uses for stored procedures include access control, data validation, or data aggregation among many others. In a modern application that is designed with a model-first approach, the usage of stored procedures (and triggers) is at least controversial. Personally, I tend to avoid triggers completely and stored procedures in most of situations, and only use the latter in very specific scenarios which are difficult or impossible to solve by other means.
Pop quiz 1. Which of the following are elements of a database schema? a. Classes b. Views c. Properties d. Events e. Stored procedures f. Foreign key constraints g. Tables
Summary In this chapter, we have learned what a relational database is and how we can construct such a database which fits our needs to store data generated by our system. We specifically covered what a database schema is and which elements it comprises. We then learned how we can create database tables to logically partition our data. In addition, we discussed what type of constraints we can use to further structure our data and relate our tables in the schema. Last but not least, we examined the generation of Primary Keys, as well as the usage of triggers and stored procedures in the context of NHibernate. Now that we have discussed how to model our data into tables, we are ready to bridge the gap between the domain model and the database model or schema. We do this through mapping, which will be discussed in the next chapter.
[ 91 ]
5
Mapping the Model to the Database In this chapter, we will discuss the various methods that exist to map a domain model to an underlying database. We will discover that, on one hand, the more we follow conventions, the less we have to code or define, and on the other hand, the more flexibility we want, the more effort we have to invest in the mapping.
In this chapter, we shall:
Get a clear understanding about what mapping is and what it is needed for
Discuss in detail four of the most commonly used mapping techniques
Define and implement conventions to reduce the coding effort
Automatically create scripts to create a database schema from our mappings
Define the mapping for our ordering system domain model
Ok then, let's dive into the details.
What is mapping? In Chapter 3, we introduced the model as one of the core elements of our application. In Chapter 4, we discussed the database schema. Looking back, we realize that there is a huge difference between how the two are defined. The model is object-oriented, whereas the database schema is relational. The former works with individual objects, whereas the latter works with sets of data. There is a conceptual mismatch between the two approaches. We also call this the impedance mismatch.
Mapping the Model to the Database
We have to find a means to bridge the gap between the two and that's where mapping comes into play. Mapping defines how the data that lives in the model inside the objects and their properties finds its way into database tables and their fields, defined in a relational database schema. Mapping has a lot to do with wiring up two sides that might look completely different. When starting a new project from grounds up, a so-called green field project, we want to define the model first and make the database follow out of the model. We do not want to define the database schema first and then try to put our domain model on top of it. If we make the database schema follow the requirements of the model, then the mapping becomes easy and straight forward. We can use sensible conventions for most parts of the mappings and, as a consequence, we do not have to explicitly define a lot of details. In extreme cases, we can even let the framework automatically create the whole mapping for us.
Types of mapping When using NHibernate as our ORM framework, we can identify four main types of mappings: 1. 2. 3. 4.
XML-based mapping. Attribute-based mapping. Fluent mapping. Convention-based mapping, sometimes also called auto-mapping.
The following section will explain the main concepts behind these different ways of mapping the domain to an underlying database. There also exist some OSS and commercial tools that provide the possibility to visually design the mapping between the domain and the underlying database schema. However, this approach is outside the scope of this book.
XML-based mapping Each type of the model is mapped with an XML document to a table in the database schema. The structure of this XML document is defined in the nhibernate-mapping.xsd file, which is part of the NHibernate download. This way of mapping the model to the database schema is the original and most flexible way to define a mapping. Historically, NHibernate offered only this form of mapping. This flexibility comes at a price. To define an XML document for each and every entity and value object is a lot of work, and XML is not known to be very readable on one hand and very wrist-friendly on the other. [ 94 ]
Chapter 5
To define a mapping, we add an XML file to the project and give it the extension *.hbm.xml. The XML file has to be an embedded resource of the project and its content must follow the structure defined in the .xsd file mentioned above. To define an XML file as an embedded resource of the project is the most common way to add a mapping to the application. It is also possible to keep the XML file as an external document. In the latter case, the configuration of NHibernate has to be adjusted accordingly.
When defining the mapping for the Product entity, we would name the mapping file Product.hbm.xml. To start with the definition of the mapping for the entity Product, we add code similar to the following code snippet to the XML file:
The preceding XML snippet declares that this shall be a mapping for a class with name Product, which is located in an assembly called Sample1, and the class resides in the namespace Sample1.Domain. The mapping schema definition document used is nhibernate-mapping-2.2. The various elements in an XML mapping document have to be in a predefined order. The first element we define inside the class node is the mapping of the ID. The following code snippet is used to do this:
Here, we have defined that the property Id is to be mapped as the Primary Key and that the Primary Key column's name is also Id. Furthermore, we define that NHibernate shall use the native ID generator to generate the next Primary Key value.
[ 95 ]
Mapping the Model to the Database
We can then define the mapping for the other properties of the class to be mapped. For example, to map the property Name to the Name field of the database table, we would use the following code snippet:
Often people explicitly add the type of the property to the mapping, for example:
However, in most cases, this is not needed as NHibernate can automatically determine the right type by using reflection. My recommendation is to not add the type definition to the mapping as this is redundant information and only makes the code noisy. Only add the type definition if it is absolutely needed, as discussed later in this chapter. On the other hand, we might want to add some constraints to the mapping of the property, such as the definition that the content of the database field cannot be undefined and that the maximum length of the field is 50 characters. We can do so by adding this information via attributes to the corresponding property node:
Putting it all together, the mapping document for the Product class looks similar to the following code snippet:
The most compelling advantage of XML mapping so far is that it is the most complete form of defining mappings. If we can't define a mapping in XML, then we can't define it in any other type of mapping. At the time of writing, some very exotic types of mappings are still only possible in XML. On the other hand, XML mapping is a very verbose form of mapping and the readability of the mapping documents is limited in comparison with other types of mappings. [ 96 ]
Chapter 5
Attribute-based mapping Instead of introducing a separate document to map an entity to an underlying database table, one can also use attributes to decorate the entity and its members. These attributes with their metadata define the details of the mapping. There are several pros and cons regarding this method of mapping the model to the database. The advantages are:
As already mentioned, we do not have to introduce yet another document to define the mapping.
Definition of entity and mapping are in one place, therefore it is easier to understand how the entity is mapped.
The mapping is type-safe and defined in C#. There is no need to use yet another language (such as XML) to define the mappings.
There are also, of course, disadvantages to consider when using this method:
When adding attributes to our domain entities, we couple the entities tightly to concern to persistence. Our entities are no longer POCO (Plain old CLR objects).
The introduction of attributes clutters our entities and can makes it harder for the developer to follow what business logic the entity contains.
Attributes are very limited in their usage. A lot of more advanced mapping scenarios cannot be achieved with attributes, or it is only possible to achieve the desired result in a very convoluted way.
There are two major frameworks available that offer attribute-based mapping. The first one is in an NHibernate contribution project, the second one is part of the larger framework Castle Active Records. Attribute-based mapping has lost a bit of its appeal with the introduction of fluent or convention-based mapping. Let's look at a sample. How would we map the Product entity when using the NHibernate contribution project? When mapping an entity, we have to define details on the class itself and on the properties and/or fields of the class. Methods never have to be mapped. On the class level, we define such things as:
Which table do we want to map the class to; and optionally:
[ 97 ]
Mapping the Model to the Database
What is the name of the schema of the table in the database? [Class] public class Product { }
The preceding code snippet would map our entity Product to a database table with the same name. If we want to use a different name for the database table, then we can do so by adding this information to the Class attribute: [Class(TableName="Products")] public class Product { }
To map a property to a table field, we decorate it with the Property attribute. The following code snippet would map the Name property to the field Name of the table Products: [Class(TableName="Products")] public class Product { [Property] public string Name { get; set; } }
As with the Class attribute, the Property attribute can also be used by specifying more data to refine our mapping of the corresponding property. Such refinements could be the selection of a column name that is different from the property name, or the definition of a maximal length of the database field, or the definition that the field must contain a value that is, it cannot be null, and so on. [Property(Column="ProductName", Length=50, NotNull=true)] public string Name { get; set; }
The preceding code snippet maps the property Name to the table field ProductName, defines the column to be not null, and defines its length to be 50. Last but not least, let's see how we can define the mapping for the ID of the entity to the Primary Key column of the corresponding table, as shown in the following code snippet: [Id(Column="ProductID")] [Generator()] public int Id { get; set; }
[ 98 ]
Chapter 5
Here, we defined that the property Id of the Product entity is the ID (also known as Primary Key) of the entity and shall be mapped to the database table field ProductID. Furthermore, we define that NHibernate shall use the native generator to create new IDs. In a similar way, we can also define relations and class hierarchies by using attributes, but this exceeds the scope of this book. Putting it all together, we have the following code snippet: [Class(Name = "Products")] public class Product { [Id(Column = "ProductID")] [Generator(Class = "native")] public int Id { get; set; } [Property(Column = "ProductName", Length = 50, NotNull = true)] public string Name { get; set; } [Property(NotNull = true)] public decimal UnitPrice { get; set; } [Property(NotNull = true)] public int ReorderLevel { get; set; } [Property(NotNull = true)] public bool Discontinued { get; set; } }
Very simple to set up indeed, but, on the other hand, look how noisy our class definition has already got, and this does not include using other attributes for the definition of additional concerns, such as validation or indexing to enable full text search.
Fluent mapping In this case, the mapping of domain entities to the underlying database tables happens in a type-safe way using a fluent API, which makes the code very readable and concise. The mapping is defined in a separate class and thus does not pollute the entity. The framework, which provides us with the possibility to fluently define the mapping, uses expression trees and static reflection to provide us the type-safety, where we don't have to use "magic strings". The framework Fluent NHibernate (see http://www.fluentnhibernate.org) provides us with the possibility to fluently define our mappings.
[ 99 ]
Mapping the Model to the Database
Let's look at how we would map the Product entity when using fluent mapping. To start with, we add a class to our project that inherits from ClassMap, where T will be the class we want to map. It makes sense to call all mapping classes the same way as the class we want to map with a postfix Map. In our case, the definition would be: public class ProductMap : ClassMap {…}
In the constructor of the class, we then define the details of the mapping. Basic properties are mapped using the Map method of the base class and passing as a parameter, a lambda expression, defining the property of the class we want to map. For the Name property of the Product class, we would add the following code: public ProductMap() { Map(x =>x.Name); }
The preceding code snippet would map the property Name to the field Name of the database table Product. This is based on the fact that Fluent NHibernate uses sensible conventions if we do not specify things explicitly. If we want to be more specific, then we can do so. To define that the field Name cannot be undefined and that its maximum length is 50 characters, we would use the following code: Map(x =>x.Name) .Not.Nullable() .Length(50);
To map the ID of an entity, we use the method Id of the base class and again pass a lambda expression, which defines the property which is the Id of the entity: Id(x =>x.Id);
The preceding code would map the property Id as the Primary Key to the field Id of the database table Product. Furthermore, the native ID generator of NHibernate would be used to generate new Primary Key values. If we want to be more specific, then we can do so and, for example, map the property Id to a database field with a different name, ProductID, and define that the GuidComb algorithm shall be used for ID generation with the following code: Id(x =>x.Id, "ProductID") .GeneratedBy.GuidComb();
It is very important that we only define those details that deviate from the defaults determined by the base conventions of the framework. Any unnecessary details are only noise in our code and obfuscate our view to the essential elements. [ 100 ]
Chapter 5
Putting it all together, we would have the following code to map the Product entity to the database table Product, having fields with the same names as the properties of the entity: public class ProductMap : ClassMap { public ProductMap() { Id(x =>x.Id); Map(x =>x.Name).Length(50).Not.Nullable(); Map(x =>x.UnitPrice).Not.Nullable(); Map(x =>x.ReorderLevel).Not.Nullable(); Map(x =>x.Discontinued).Not.Nullable(); } }
The advantages of mapping fluently are apparent, when looking at the preceding code:
The code used to define the mapping is very readable and concise.
The mapping is kept separate from the entity, and thus does not pollute the entity.
The mapping is type-safe. We use no magic strings.
Possible selections or settings for each element are very discoverable as the fluent API is fully supported by IntelliSense.
As usual, there is no such thing as a free lunch and we also have to name some disadvantages of the fluent mapping:
We have to define a mapping class for each entity and value object.
Some admittedly exotic mapping scenarios are not supported by fluent mapping, though the situation improves with every new release of Fluent NHibernate.
Currently, defining the mappings fluently introduces a slight performance penalty when starting the application and initializing NHibernate as the fluent mappings have to be translated into XML mappings, which will then be parsed and used by NHibernate. This additional step is not necessary when defining the mappings directly in XML.
When the name of the database table differs from the entity name or the database field name differs from the property name of the entity, then we have to use magic strings. Unfortunately, there is no way around it.
[ 101 ]
Mapping the Model to the Database
Mapping by convention Ideally, we would want the mapping to happen auto-magically. We ask ourselves, "Why can the system not be clever enough to wire itself up?". We are lucky! It is indeed possible to let the system do nearly all of the wiring. How is this possible? The base for this is the definition and usage of sensible conventions. A framework can analyze our domain and use reflection to obtain a lot of metadata about the entities, such as their names and what properties they contain, as well as the type of properties. We can exchange any of the base conventions with our own implementation, and thus influence in detail how the auto-mapping is executed. As with the other types of mappings, we want to look at some of the advantages as well as some of the disadvantages of auto-mapping. The most compelling advantages are:
It requires a lot less code as no explicit mapping between domain and database schema is needed.
The database schema follows the domain model as close as possible, as the domain model is the main source of metadata from which the database schema is created by the system.
As developers, we can concentrate on the areas where in our application there are exceptions from the default mapping behavior and address those exceptions explicitly.
And, here are some of the disadvantages:
As everything is convention-driven, we have much less fine-grained control over the way specific entities are mapped.
Convention-based mapping is only useful in applications that do not have to deal with already existing legacy databases.
Note that the first of the two "disadvantages" can also be seen as an advantage. It really depends on the context.
[ 102 ]
Chapter 5
A word about lazy loading In Chapter 3, we defined the domain of our ordering system. Now, we are going to map our domain to an underlying database. Our domain is defined such that it is possible to navigate from one entity to another via the relations we have defined. As an example, we can navigate from our order entity to the line item entities that are associated with the specific order. Furthermore, from a line item we can navigate to the product entity, which this given line item represents inside the order. Theoretically, we can start at one point in the domain and pick one entity, and then navigate throughout the whole domain. As long as we are in the domain and our domain is not connected to any database, this is not a problem, but it can have unwanted side effects as soon as we start to hydrate our domain with data from a database.
Loading data on demand Which data should NHibernate load for us when we instruct it to load, for example, a customer? Should NHibernate load only the customer data or should it also load all its orders? In addition, if it loads all orders, should it then also load all details of the ordered products? Now, having the products, should it also get the categories the products are associated with? We can continue asking these questions until NHibernate has loaded the whole database. As we like to have the ability to navigate through the domain via the relations we defined, but, on the other hand, want to avoid that NHibernate loads too much data (in extreme cases, we can cause NHibernate to load the whole database), then we need to introduce a mechanism which allows us to always have just the data loaded we really need. Here comes lazy loading to the rescue! NHibernate has, since its very beginning, supported the idea that data is only to be loaded when it is really needed. This behavior is called lazy loading as NHibernate sits around lazy and only does what it is told to do.
Proxies Now, how can NHibernate make lazy loading happen? NHibernate needs some way to hook into our entities and intercept operations we execute on certain properties of these entities. For this purpose, NHibernate is using proxies. A proxy is a class that is "wrapped" around another class and acts on its behalf. When NHibernate loads data from the database and creates an entity, say a product, it does not return an instance of type product to us, but rather a proxy to the product. To us, this proxy seems to be a product as it behaves like a product. This is possible due to the fact that the proxy is inherited from the Product class and just overrides all its virtual members.
[ 103 ]
Mapping the Model to the Database
Virtual properties and methods The preceding section has some important implications. To be able to leverage lazy loading of NHibernate, our domain entities must define all their public properties and their public methods as virtual. Only in this case can NHibernate successfully proxy these entities for us.
Creating database schema creation scripts The nice thing about the model first approach is that we can instruct NHibernate to automatically generate the creation scripts for the database schema for us. That is, we do not have to manually create the database schema, just let NHibernate figure out how the tables and their relations are going to look. To do its job, NHibernate uses the information we provide when defining the mappings for our domain model. For this job, NHibernate provides us with a class called SchemaExport, which expects a Configuration object as the parameter of its constructor. var configuration = ... var exporter = new SchemaExport(configuration);
We can then call the Execute method, which has several overloads. These overloads can be used to either only create SQL scripts, or to directly create the schema in the database, among other possibilities. Note that there has been no mention of how the configuration object is defined. This will be discussed in detail in Chapter 8, which talks about configuring NHibernate. To generate the SQL scripts and write them to the console, we can use the following code: var configuration = ... var sb = new StringBuilder(); var writer = new StringWriter(sb); var exporter = new SchemaExport(configuration); exporter.Execute(true, false,false, null, writer); Console.WriteLine(sb);
As an alternative way to produce the same result, we can use the Create method of the SchemaExport class and provide the action s=>sb.AppendLine(s) as the first parameter to the method. The action adds a string s to a predefined StringBuilder object sb: var configuration = ... var sb = new StringBuilder(); var exporter = new SchemaExport(configuration); exporter.Create(s =>sb.AppendLine(s), false); Console.WriteLine(sb);
[ 104 ]
Chapter 5
Still, we have not defined where the configuration object comes from. We can use the Loquacious configuration of NHibernate 3 to create such a configuration object for us. In the following code snippet, we have assumed that the mappings have been defined as embedded XML documents and that we are accessing a MS SQL Server 2008 database server: var configuration = new Configuration(); configuration.DataBaseIntegration(db => { db.Dialect(); db.Driver(); }); configuration.AddAssembly(typeof(Product).Assembly);
We can also use Fluent NHibernate to create such a configuration object for us: var connString = "server=.\\SQLEXPRESS;database=OrderingSystem;" + "integrated security=SSPI;"; var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(connString) ) .Mappings(m =>m.FluentMappings .AddFromAssemblyOf()) .BuildConfiguration();
In the preceding case, we assume that we are going to target a MS SQL Server 2008 installed on our local machine having an (empty) database OrderingSystem defined. We specify further that when accessing SQL Server, integrated security should be used. Finally, we assume that we have defined our mappings fluently and that all mapping files are defined in the same assembly as the Product entity.
Fluent mapping As already briefly discussed, fluent mapping is a technique where we use a framework that exposes a fluent API to define the mappings for our domain model. The fluent API makes a mapping very readable as there is no unnecessary code noise involved, such as angle brackets. Fluent mappings are defined in C# and the framework makes heavy use of expression trees to allow us to define all details in a type-safe way without using magic strings. This makes tasks, such as refactoring entities, a breeze as tools such as Visual Studio or ReSharper can automatically refactor the affected mapping files too. [ 105 ]
Mapping the Model to the Database
Expression trees – please explain When Microsoft introduced LINQ for .NET 3.5, it had to first create the necessary infrastructure. LINQ heavily relies on lambda expressions and expression trees. Now, what is an expression tree? Let's try to give a very simple answer: when the compiler parses code, it converts this code into tree-like structures, which can be better analyzed and optimized. Usually, after having created and optimized an expression tree, the compiler compiles it to executable code. However, sometimes it would be nice if developers had the opportunity to work with those trees rather than with the compiled code. Why? It is because an expression tree contains a whole lot of information about the original code and its structure that we can leverage. Analyzing the data stored in the tree and the structure of the tree, we can gain similar and even more information as when using reflection. This analysis of the expression tree is also called static reflection, whereas the standard reflection using the Type type of .NET is called dynamic reflection. Now, let's see how we can use this and show some code. We want to be able to define the mapping for a property of an entity without using magic strings; that is, we want to avoid having code similar to the following line: Map("Some Property");
The preceding code snippet uses a string to define which property to map. We'd rather have the following, type-safe code in its place: Map(x => x.SomeProperty);
The preceding code uses a lambda expression to define which property we want to map. Note that there are no magic strings involved this time! Now, the Map function is provided by the Fluent NHibernate API and its signature looks similar to the following code snippet: public Property Part Map(Expression expression) {…}
The important part is the type of the parameter Expression, which represents an expression tree. In other words, the method Map receives an expression tree which it can use to analyze and find out which property we are talking about. Nice!
Getting started To be able to fluently map our model to the underlying database, we add a reference to Fluent NHibernate and to NHibernate, as shown in the following screenshot. These are, at the moment, the only additional assemblies we have to reference. The two assemblies can be found in the lib folder we prepared in Chapter 2. [ 106 ]
Chapter 5
Classes To map an entity to a database table, we have to add a class to our project that inherits from ClassMap, which is defined in the FluentNHibernate.dll assembly. This base class contains all the necessary plumbing code that makes the mapping an easy and very straightforward task. To map the Product entity, we would start with the following code snippet: public class ProductMap : ClassMap { public ProductMap() { // here we define the mapping details } }
The mapping has to be defined in the default constructor of the mapping class. Note that ClassMap expects the class to be mapped as a generic parameter. In our case, this is the Product class. All details of the mapping have to be defined in the default constructor of the class.
Entity level settings If we want our database to follow the domain model, then we need to specify the least details. However, for additional flexibility, we have the ability to explicitly specify a lot of details. As an example, we can define the name of the table we want to map the entity to, if it is different from the class name of the entity. We can do so using the following code snippet: Table("tbl_Product"); [ 107 ]
Mapping the Model to the Database
If a specific table resides in another schema than all other tables, then we can also specify the corresponding schema by using the following code snippet: Schema("OrderingSystem");
By default, Fluent NHibernate configures our entities to be lazy loaded. If we do not like this, then we can change the behavior by using the following code snippet: Not.LazyLoad();
There are many more details we can specify, which are outside the scope of this introduction.
ID columns The first thing we usually map is the ID of the entity. This is done by using the Id method. This method has a lot of options but, in its simplest form, it looks similar to the following code: Id(x =>x.Id) .GeneratedBy.HiLo("1000");
Note that the parameter for the HiLo method is a string, although internally it is really used as an integer. This is a defect of the current version of Fluent NHibernate.
With the preceding code snippet, we are instructing NHibernate that the property Id of the Product entity shall be mapped as the Primary Key and that new IDs are to be automatically generated by the HiLo generator of NHibernate. All ID generators defined by NHibernate can be used in the mapping. These are the native generator, the Guid and GuidComb generators, and the HiLo generator, as shown previously, among others. If you want to use GUIDs as your IDs, then you can use the GuidComb generator and not the Guid generator, as it is optimized for database use. Some sources claim that the performance differences for larger tables can be quite significant when using the GuidComb versus Guid generator; although this has not yet been proven to be true for all types of database engines and is at least questioned in regards to MS SQL Server 2008. As long as our field is named Id in the database, we are good to go. However, what if it was named ID or PRODUCT_ID? This simply wouldn't handle it. In that case, we would have to add the optional column parameter to identify it: Id(x =>x.ID, "PRODUCT_ID") .GeneratedBy.HiLo("1000");
[ 108 ]
Chapter 5
Alternatively, we could write it like the following code snippet: Id(x =>x.ID) .Column("PRODUCT_ID") .GeneratedBy.HiLo("1000")
The former version might be the preferable one, as it is more compact, but on the other hand, you might prefer the latter more explicit version. An optional attribute that is often used for the ID definition is the UnsavedValue function call. This function specifies what value should be returned in a new object before it is persisted (saved) to the database. Adding this call, the code would look similar to the following code snippet: Id(x =>x.ID, "PRODUCT_ID") .GeneratedBy.HiLo("1000") .UnsavedValue(-1);
Note that we did not explicitly define the type of the ID. Fluent NHibernate can retrieve this information from the entity by using reflection. In general, we should avoid specifying redundant information as it is not needed and only clutters your code.
Properties To map simple valued properties, we use the Map function. Simple valued properties are properties that have one of the base .NET types, such as int, float, double, decimal, bool, string, or DateTime. Such a property is mapped to a single database column. In its simplest form, the mapping of, for example, the Name property of the Product entity would look similar to the following code snippet: Map(x =>x.Name);
The preceding code snippet instructs NHibernate to map the property Name to a database column of the same name. The type of the database column would be nvarchar in the case of SQL Server and the maximum length of the database column would be 255. Additionally, the column would be nullable. We can, of course, specify other values if needed. Let's assume that the name column should have a maximum length of 50 and should not be nullable. For this we could use the following code snippet: Map(x =>x.Name) .Length(50) .Not.Nullable();
[ 109 ]
Mapping the Model to the Database
The beauty of the preceding code snippet is that we can read it as if it were plain English. There are no angle brackets or other noisy code elements that make it hard to read. The fluent API makes things concise and flow nicely. Assuming that the names of the database columns differ from the property name, we could specify that by using the following code snippet: Map(x =>x.Name, "PRODUCT_NAME") .Length(50) .Not.Nullable();
Sometimes, NHibernate cannot automatically figure out how we want to map a certain property and we have to specify our intent. As an example, we want to map a property which is an enum type. Assume the entity Product had a property ProductType of type ProductTypes, where ProductTypes is an enum like the following code snippet: public enum ProductTypes { ProductTypeA, ProductTypeB, ... }
Then we can map this property by adding a call to the CustomType function, as shown in the following code snippet: Map(x =>x.ProductType) .CustomType();
In this case, as the type behind an enum is an integral type (int by default), the database field would be of type int for an SQL Server. NHibernate will automatically translate between numbers of type int in the database and enum values in the domain model. Another example that is often used is the mapping of a property of type bool to a char(1) database column, which contains 'Y' for true or 'N' for false. NHibernate has a special mapping defined for this called YesNo. We can map, for example, the Discontinued property of the Product entity as follows: Map(x =>x.Discontinued) .CustomType("YesNo");
Once again, Fluent NHibernate is very complete and offers us a great deal of flexibility to define the details of mapping a simple valued property. In this section, we have only discussed the most important and frequently used settings. However, let's make one thing clear: in 99% of the cases, this is all you need; but it is good to know that most edge cases are covered by the fluent API. [ 110 ]
Chapter 5
References To map a property which references another entity, such as the Category property of the Product entity, we can map this relation by using code as simple as the following code snippet: References(x =>x.Category);
This type of mapping represents a many-to-one relation and is frequently used in a domain model. Most often, we want to define this reference as a mandatory one. It is very easy and straightforward to do so using the following code snippet: References(x =>x.Category) .Not.Null();
If we do not specify it explicitly, then the name of the foreign key relating the Product table to the Category table would be named Category_Id. The default convention specifies the foreign key name to be a combination of the name of the Property and ID joined by an underscore character. We can, of course, change this by specifying the name explicitly, as shown in the following code snippet: References(x =>x.Category) .Not.Null() .ForeignKey("CategoryId");
One last noteworthy setting is to specify that a reference shall be unique, as shown in the following code snippet: References(x =>x.Category) .Not.Null() .Unique();
Slowly, you should get a feeling for the concept behind the fluent API.
Collections Here, we are talking about one-to-many relations. One parent entity holds a collection of child entities. Once again, we can define such a mapping in a very clear and concise way, avoiding any disambiguaties. Let's look at the sample of an order holding a collection of line item objects. The mapping of the line item collection would be similar to the following code snippet: HasMany(x =>x.LineItems);
Usually, you want to define your HasMany mapping as Inverse. What does this mean? [ 111 ]
Mapping the Model to the Database
Between the order and a line item, there is an association. We use the inverse attribute to specify the "owner" of the association. The association can have only one owner, so one end has to be set to inverse while the other has to remain "non-inverse". In this one-to-many association between order and line items, if we don't mark the collection as the inverse end, then NHibernate will perform an additional UPDATE operation. In fact, in this case, NHibernate will first insert the entity that is contained in the collection (the line item), if necessary, insert the entity that owns the collection (the order), and afterwards update the collection entity, so that the foreign key is set and the association is made. Note that in this case, the foreign key in our DB must be nullable. When we mark the collection end as Inverse, then NHibernate will first persist the entity that owns the collection (the order), and will persist the entities that are in the collection afterwards (the line items), avoiding the additional UPDATE statement. In most cases, the former is not what we want, and thus we add Inverse to our mapping, as shown in the following code snippet: HasMany(x =>x.LineItems) .Inverse();
Another important detail we can configure is the behavior regarding whether and how NHibernate should cascade insert, update, and delete operations from a parent entity to its children. If we specify that NHibernate should cascade operations, then when we add a new line item to the LineItems collection of the order, and subsequently save the order, NHibernate will automatically also save this new line item. Without cascading enabled, we would have to explicitly save the line item ourselves. Another important scenario regarding cascading is the case where we remove an existing line item object from the LineItems collection of the order. What should happen if we save the order? With the appropriate cascade setting, NHibernate will automatically delete the line item from the database; otherwise we have to do it explicitly. Most often, you will want to use this setting, as shown in the following code snippet: HasMany(x =>x.LineItems) .Inverse() .Cascade.DeleteAllOrphans();
[ 112 ]
Chapter 5
Mapping many-to-many relations We can also map the scenario where we have, say, a book that is written by many authors and an author can write many books. In the domain model, the Book entity would have a collection of Author child objects, and at the same time, the Author entity would have a collection of Book child objects. The code to define such a mapping is shown in the following code snippet: HasMany(x =>x.Author);
If we want to specify how the intermediate table is called that links the Author and Book table in this many-to-many relation, then we can do so using the following code snippet: HasMany(x =>x.Author) .Table("BookAuthor");
Mapping value objects In Chapter 3, we learned that value objects are a very important concept of a domain model, and thus we need to know how to map a property which has a value object as type. Let's take the Name property of the Customer entity as an example. First, we might want to map the value object itself. For that, we define a class NameMap, which inherits from ComponentMap, as shown in the following code snippet: public class NameMap : ComponentMap {...}
Then, we add the mapping details to the default constructor of this class, as shown in the following code snippet: public void NameMap() { Map(x =>x.LastName).Not.Null.Length(50); Map(x =>x.MiddleName).Length(50); Map(x =>x.FirstName).Not.Null.Length(50); }
With the preceding code snippet, we define that LastName, MiddleName, and FirstName are all a maximum of 50 characters long, and that MiddleName is an optional value. Once we have mapped the value object, we can define the mapping for the Name property of the Customer entity. We add the following code to the default constructor of the CustomerMap class: Component(x =>x.Name);
[ 113 ]
Mapping the Model to the Database
Time for action – Mapping our domain It is now time to define the mapping for our domain model. We want to do this by using fluent mappings as provided by Fluent NHibernate.
1.
Start Visual Studio and open the OrderingSystem solution from Chapter 3.
2.
Add references to the assemblies, FluentNHibernate.dll and NHibernate. dll. To do this, right-click on the References node in the Solution Explorer window under the OrderingSystem project and select Add Reference…, as shown in the following screenshot. Navigate to the lib folder where all NHibernate and Fluent NHibernate assemblies are located. Select the two files just mentioned.
3.
In Chapter 3, we used an ID of type GUID for our entities. In this chapter, we want to use IDs of type int. Thus, open the Entity class and change the definition of the ID property from public Guid ID{get;set;} to: public int ID { get; set; }
4.
Also, replace any occurrence of Guid.Empty in the code of the Entity base class by 0 (zero).
5.
In the Solution Explorer window, right-click on the OrderingSystem project and from the context menu, select Add | New Folder, as shown in the following screenshot:
[ 114 ]
Chapter 5
6.
Name the new folder Mappings. We will put all the mapping classes in this folder.
7.
Right-click on the Mappings folder and select Add | New Item….
8.
In the Add New Item dialog, select Class from the template list and give it the name EmployeeMap.cs. Click on Add to add this class to the project and close the dialog.
9.
Make the EmployeeMap class inherit from ClassMap. Your code should look similar to the following code snippet: using FluentNHibernate.Mapping; using OrderingSystem.Domain; namespace OrderingSystem.Mappings { public class EmployeeMap : ClassMap { } }
[ 115 ]
Mapping the Model to the Database
10. Add an empty default constructor to the EmployeeMap class. public EmployeeMap() { }
11. Add code to map the property ID of the Employee class as Primary Key. We want
to use the HiLo generator of NHibernate as the generator for (all) our new IDs. We also want NHibernate's HiLo generator to work in intervals of 100. To do this, add the following code to the constructor: Id(x =>x.ID) .GeneratedBy.HiLo("100");
The HiLo generator used in the preceding code snippet delegates the responsibility to create sequential and unique numbers to be used as IDs for the entities to NHibernate. NHibernate maintains a database table with the current offset for the numbers to generate. We can define an interval (for example, 100 in our sample), during which NHibernate can create subsequent numbers without ever accessing the database. As soon as all numbers of the given interval are used, NHibernate updates the IDs table and takes the next interval of numbers.
12. Add code to the constructor to map the property Name of the Employee class. As the type of the Name property is Name, which is a value object, we have a component mapping and the code looks similar to the following code snippet:
Component(x =>x.Name);
13. That is all that is needed to map the Employee class. Save the EmployeeMap class. 14. Next, we want to map the Name value object. Add another class file to the Mappings folder of the OrderingSystem project (as described in step 7). Name the new class file NameMap.cs.
15. Make the class NameMap inherit from ComponentMap. Your code should look similar to the following code snippet: using FluentNHibernate.Mapping; using OrderingSystem.Domain; namespace OrderingSystem.Mappings { public class NameMap : ComponentMap { } } [ 116 ]
Chapter 5
16. Add an empty default constructor to the NameMap class. 17. Add code to the constructor to map the property LastName of the Name class. The LastName is a mandatory value, and thus cannot be null in the database. Also, the maximum length of the LastName shall be 100 characters. This leads to the following code: Map(x =>x.LastName) .Not.Nullable() .Length(100);
18. Repeat the preceding step for the FirstName property of the Name class, as shown in the following code snippet:
Map(x =>x.FirstName) .Not.Nullable() .Length(100);
19. Map the MiddleName property of the Name class, as shown in the following
code snippet. The middle name is optional, and thus the value can be null in the database. The maximum length is the same as for FirstName and LastName, as shown in the following code snippet:
Map(x =>x.MiddleName) .Length(100);
20. Save the NameMap class. With this, the mapping of the Name value object is
complete and NHibernate knows how to correctly map it when we use it in a Component type mapping, as in the EmployeeMap class.
21. Now, let's map the Customer class. Add a new class file to the Mappings folder of the project, as described in step 7, and name the file CustomerMap.cs.
22. Make the class CustomerMap inherit from ClassMap and add an empty default constructor to the class, as shown in the following code snippet:
using FluentNHibernate.Mapping; using OrderingSystem.Domain; namespace OrderingSystem.Mappings { public class CustomerMap : ClassMap { public CustomerMap() { } } } [ 117 ]
Mapping the Model to the Database
23. Add code to the constructor to map the property ID, as you did for the Employee class in step 11.
24. Add code to map the CustomerName property of the Customer class as a component, as you did for the Employee class in step 12.
25. Add code to map the CustomerIdentifier property. This property is mandatory and its maximum length should be 50 characters. The last three steps should result in the following code: Id(x =>x.ID) .GeneratedBy.HiLo("100"); Component(x =>x.CustomerName); Map(x =>x.CustomerIdentifier) .Not.Nullable() .Length(50);
26. Add code to map the Address property as a component, as shown in the following code snippet:
Component(x =>x.Address);
27. Now, we need to map the collection of orders of the customer. We want NHibernate to directly access our private field orders in the Customer class. We also instruct NHibernate to automatically cascade all insert, update, or delete operations from the customer to its orders. The code should look similar to the following code snippet: HasMany(x =>x.Orders) .Access.CamelCaseField() .Inverse() .Cascade.AllDeleteOrphan();
28. We have completed the mapping of the Customer class. Save the file. 29. Next, we will map the Address value object. Add a new class file to the Mappings folder of the project, as described in step 7, and name the file AddressMap.cs.
30. Make the class AddressMap inherit from ComponentMap and add an empty default constructor to the class.
[ 118 ]
Chapter 5
31. Map the properties Line1, Line2, ZipCode, City, and State. All values are
mandatory except Line2. The maximum length of all the properties should be 50 except ZipCode, which should be a maximum of 10 characters long. The resulting code should look similar to the following code snippet (note: use the same using statements as in all other mapping classes): public class AddressMap : ComponentMap { public AddressMap() { Map(x =>x.Line1).Not.Nullable().Length(50); Map(x =>x.Line2).Length(50); Map(x =>x.ZipCode).Not.Nullable().Length(10); Map(x =>x.City).Not.Nullable().Length(50); Map(x =>x.State).Not.Nullable().Length(50); } }
32. Now, let's map the Order class. Add a new class file to the Mappings folder of the project, as described in step 7, and name the file OrderMap.cs.
33. Make the class OrderMap inherit from ClassMap and add an empty default constructor to the class.
34. Add code to the constructor to map the property Id as discussed in step 10. 35. Map the two properties OrderDate and OrderTotal, and make them mandatory (not null), as shown in the following code snippet:
Map(x =>x.OrderDate).Not.Nullable(); Map(x =>x.OrderTotal).Not.Nullable();
36. Map the Customer property as a one-to-many reference and declare it as mandatory, as shown in the following code snippet:
References(x =>x.Customer).Not.Nullable();
37. Finally, map the OrderLines property similar to the way we mapped the Orders collection of the customer in step 27; as shown in the following code snippet: HasMany(x =>x.LineItems) .Access.CamelCaseField() .Inverse() .Cascade.AllDeleteOrphan();
38. Next, we come to the LineItem entity. Add a new class file, LineItemMap.cs, to the project.
[ 119 ]
Mapping the Model to the Database
39. Make the class inherit form ClassMap. 40. Add an empty default constructor. 41. Add code to the constructor to map the property Id as discussed in step 11. 42. Map the Order and the Product properties as a one-to-many references and declare them as mandatory; as shown in the following code snippet: References(x =>x.Order).Not.Nullable(); References(x =>x.Product).Not.Nullable();
43. Map the Quantity, UnitPrice, and Discount properties and also declare them as mandatory; as shown in the following code snippet: Map(x =>x.Quantity).Not.Nullable(); Map(x =>x.UnitPrice).Not.Nullable(); Map(x =>x.Discount).Not.Nullable();
44. Last but not least, we map the Product entity. Add a new class file, ProductMap. cs, to the project.
45. Make the class inherit from ClassMap. 46. Add an empty default constructor. 47. Add code to the constructor to map the property ID as discussed in step 11. 48. Map the properties Name, Description, UnitPrice, ReorderLevel, and
Discontinued. All except Description are mandatory values. The Name property should have a maximum length of 50, while the Description property has a
maximum length of 4,000. This results in the following code snippet:
Map(x Map(x Map(x Map(x Map(x
=>x.Name).Not.Nullable().Length(50); =>x.Description).Length(4000); =>x.UnitPrice).Not.Nullable(); =>x.ReorderLevel).Not.Nullable(); =>x.Discontinued);
What just happened? We defined the mapping for our ordering system domain. By doing this, NHibernate now understands how we want to structure the data from the domain in the database. NHibernate knows which entity is stored in which database table and how the properties are related to the database table fields. It is also clear which foreign key constraints are used at the database level to relate the database tables among each other.
[ 120 ]
Chapter 5
Use mapping conventions Fluent NHibernate uses a lot of conventions to map our domain model to a database schema. All of these conventions make sense in most of the day-to-day projects. Sometimes, it is important that we are able to add our own conventions to the mapping process, or even replace existing conventions with our own specific implementations. Fluent NHibernate is very flexible in this regard and offers us the possibility to fine-tune the system. Conventions are added to the system during initialization, that is, before the mappings are processed. In Chapter 8, which talks about configuration, we will discuss how to add conventions in detail.
ID conventions During the definition of the mappings for our ordering system domain, you might have noticed that we had to define the mapping of Id for each entity. Specifically, we had to define that we want to use the HiLo generator for ID generation over and over again. If we did not define the generator to use, then the default convention for ID generation would have been applied. The default convention uses the native generator for ID generation. We can now override the default convention by implementing our own convention. This is done by implementing the appropriate interface defined by Fluent NHibernate. Each type of convention has its own interface. In the case of the Id convention, we are talking about the interface IIdConvention. The implementation of our own convention is very simple and looks similar to the following code snippet: public class MyIdConvention : IIdConvention { public void Apply(IIdentityInstance instance) { instance.GeneratedBy.HiLo("100"); } }
Once this convention is added to the configuration, we can simplify the mapping of Id of all our entities to this: Id(x =>x.Id);
Having access to instance of the ID, we can basically manipulate each possible detail. Other meaningful manipulations would be, for example, to define the column name of the Primary Key as being a combination of the entity name and ID. The code for this would look similar to the following code snippet: instance.Column(string.Format("{0}Id", instance.EntityType.Name)); [ 121 ]
Mapping the Model to the Database
Having the preceding convention in place, Id of the Product class would map to a database field of name ProductId, whereas Id of Customer would map to a database field of name CustomerId, and so on. By now, you certainly get a feeling of the flexibility of the convention-based approach!
Property conventions We can also easily define conventions for basic property mappings. As an example, let's assume that we want to always map properties of name Name to the non-nullable database fields of maximum length 50, regardless of the entity the property is defined in. In this case, we write a class which implements the interface IPropertyConvention, as shown in the following code snippet: public class MyNameConvention : IPropertyConvention { public void Apply(IPropertyInstance instance) { if (instance.Name != "Name") return; instance.Length(50); instance.Not.Nullable(); } }
Another useful convention would be to map a property of type bool to a char(1) field containing 'Y' or 'N' for true or false, respectively. This scenario makes sense when the database is Oracle, which does not natively support a Boolean type for database fields. The code in this case would look similar to the following code snippet: public class MyBoolConvention : IPropertyConvention { public void Apply(IPropertyInstance instance) { if (instance.Type == typeof(bool)) instance.CustomType("YesNo"); } }
This convention is only applied to the mapping of properties that are of type bool. If this is the case, then the convention applies the standard custom behavior "YesNo" defined by NHibernate to the mapping.
[ 122 ]
Chapter 5
Foreign key conventions The standard convention for foreign keys names the foreign key database field by taking the name of the entity, which is referenced, and the token ID combined with an underscore. In the case of Order referencing Customer, the foreign key name in the Order table would be Customer_Id by convention. If we don't like this and prefer to use a different naming schema, say we want the name in the given sample to be CustomerId instead (without underscore), then we can do so by again implementing our own convention. For this scenario, Fluent NHibernate already defines a handy base class from which we can inherit and where we can override the method GetKeyName. The code looks similar to the following code snippet: public class MyForeignKeyConvention : ForeignKeyConvention { protected override string GetKeyName(Member property, Type type) { // property == null for many-to-many, one-to-many, join // property != null for many-to-one var refName = property == null ? type.Name :property.Name; return string.Format("{0}Id", refName); } }
With these few examples, we have only scratched the surface of what is possible to influence the behavior of the mapping process. Every single aspect of this process can be fine-tuned by adding yet another convention.
Have a go hero – Implement your own convention a. Implement a convention that converts the names of properties of an entity to an "Oracle-friendly" format. That is, convert Pascal case names into all uppercase strings with underscores separating the words. For example, convert LastName to LAST_NAME, or CustomerId to CUSTOMER_ID. Assign this name as the column name of the respective mapping. Note that, it is called "Oracle-friendly" as the Oracle database is NOT case sensitive regarding table and field names. Thus, for example, ProductNumberSuffix would become PRODUCTNUMBERSUFFIX, which is not very readable, and it makes sense to define the field name as PRODUCT_NUMBER_SUFFIX. b. Define a convention which defines all database columns as not null by default.
[ 123 ]
Mapping the Model to the Database
No mapping; is that possible? When starting a brand new project without any legacy code or database to carry over, it is strongly recommended not to define the mappings all together. We should let the framework do this work for us automatically, based on meaningful assumptions. These assumptions should be applied to the system in the form of conventions and the conventions should be easily modifiable by us. Is this possible? Yes! We even have two possibilities to doing so. The first one is offered by Fluent NHibernate and the second one by ConfORM (see http://code.google.com/p/codeconform).
Auto-mapping with Fluent NHibernate To be able to do this, first of all you would want to group the entities you want to map in a namespace, which is distinct from the namespace of all other classes defined in the project, which are not entities or value objects. This makes it easier to instruct the AutoMapper about which entities and value objects to include in the mapping. It's suggested that you create a folder Domain in your project to which you move all the entities and value objects. As we do not explicitly map entities or value objects, we define conventions or exceptions for the AutoMapper. We do this by implementing a configuration class, which inherits from the base class DefaultAutomappingConfiguration provided by Fluent NHibernate. This base class has a lot of virtual methods which can be overridden by us to adjust the system to our needs and help the AutoMapper to do its job. Assuming that we have moved all our entities and value objects to the same namespace, which contains no other classes than domain classes, we can now define our configuration class, as shown in the following code snippet: public class OrderingSystemConfiguration : DefaultAutomappingConfiguration { public override bool ShouldMap(Type type) { return type.Namespace == typeof(Employee).Namespace; } }
The preceding code instructs the AutoMapper to consider only those classes for mapping which reside in the same namespace as the Employee entity.
[ 124 ]
Chapter 5
We need to instruct the AutoMapper about one more thing before it can do its work. We have to instruct it as to which are the value objects in our system. We can do this by overriding the IsComponent method in the configuration class, as shown in the following code snippet: public override boolIsComponent(Type type) { var componentTypes = new[] {typeof (Name), typeof (Address)}; return componentTypes.Contains(type); }
Having this configuration class in place, the AutoMapper is ready to go. If we want to influence more details of the mapping process, like the selection of the ID generator, then we can do so by defining conventions in exactly the same way as we did in the preceding section about fluent mapping. To configure our system to use auto-mapping, we would use the following code snippet: var cfg = new OrderingSystemConfiguration(); var configuration = Fluently.Configure() .Database(/* database config */) .Mappings(m =>m.AutoMappings.Add( AutoMap.AssemblyOf(cfg)) .BuildConfiguration();
Note that the details of the preceding code snippets are not important at this moment and will be explained in detail in Chapter 8, where we will talk extensively about configuring our system.
Time for action – Using auto-mapping In this sample, we want to define a very basic domain model, map the model using auto-mapping as provided by Fluent NHibernate, and then let NHibernate generate the schema for us, based on this mapping:
1.
Start Visual Studio and create a new project. Choose Console Application as the project template. Name the project AutoMappingSample.
2.
Add references to the two assemblies NHibernate.dll and FluentNHibernate. dll to the project.
3.
Add a folder Domain to the project.
[ 125 ]
Mapping the Model to the Database
4.
Add a class Customer to the Domain folder of the project. The customer entity should have Id and CustomerName as properties, as shown in the following code snippet: public class Customer { public virtual int Id { get; set; } public virtual string CustomerName { get; set; } }
5.
Add a class LineItem to the Domain folder of the project. The order line entity should have the properties Id, Quantity, UnitPrice, and ProductCode, as shown in the following code snippet: public class LineItem { public virtual int Id { get; set; } public virtual int Quantity { get; set; } public virtual decimal UnitPrice { get; set; } public virtual string ProductCode { get; set; } }
6.
Add a class Order to the Domain folder of the project. The order entity should have an Id, an order date, a reference to a customer entity, and a collection of LineItem entities, as shown in the following code snippet: public class Order { public virtual int Id { get; set; } public virtual DateTime OrderDate { get; set; } public virtual Customer Customer { get; set; } public virtual IList LineItems { get; set; } }
7.
Add a class OrderingSystemConfiguration to the project.
8.
Make the class inherit from DefaultAutomappingConfiguration.
9.
Override the method ShouldMap of the base class and define which types of the project should be mapped. Be sure to override the right method as there are two overloads of ShouldMap; use the one with the parameter type of Type, as shown in the following code snippet: public class OrderingSystemConfiguration : DefaultAutomappingConfiguration { public override bool ShouldMap(Type type) [ 126 ]
Chapter 5 { return type.Namespace == typeof (Customer).Namespace; } }
10. Add the following using statements to the Program class: using using using using using using
System; AutoMappingSample.Domain; FluentNHibernate.Automapping; FluentNHibernate.Cfg; FluentNHibernate.Cfg.Db; NHibernate.Tool.hbm2ddl;
11. Add code to the method Main of the Program class to create a NHibernate
configuration. The configuration object is created with the configuration API of Fluent NHibernate and shall use auto-mapping. The scripts shall be created for MS SQL Server 2008, as shown in the following code snippet:
var cfg = new OrderingSystemConfiguration(); var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008) .Mappings(m =>m.AutoMappings.Add( AutoMap.AssemblyOf(cfg))) .BuildConfiguration();
12. Note that it is not necessary to define a connection string for this operation as we
are not going to access the database, but just to instruct NHibernate which type of database we want to target.
13. Add code to create and show the SQL scripts on the console, as shown in the following code snippet:
var exporter = new SchemaExport(configuration); exporter.Execute(true, false, false);
14. Add the following code snippet to avoid the program from exiting without waiting for a confirmation:
Console.Write("Hit enter to exit:"); Console.ReadLine();
[ 127 ]
Mapping the Model to the Database
15. Run the application. The output on the console should look like the following screenshot:
16. Note that for brevity, the drop statements of the SQL script have been omitted.
What just happened? We defined a domain model, created a NHibernate configuration object using auto-mapping, and then used the schema export class of NHibernate to create the database schema, which follows our domain model definition. We didn't have to write any code to define a mapping. Fluent NHibernate automatically generated an implicit mapping for us based on some meaningful default conventions. If we are not happy with the result, we can fine-tune any aspect of it by defining our own conventions.
[ 128 ]
Chapter 5
Using ConfORM ConfORM is part of the NHibernate contributions and can be downloaded from http:// code.google.com/p/codeconform. Make sure you download the version that is
compatible with the version of NHibernate you are using. The only additional assembly you need when working with ConfORM is ConfORM.dll. To select from and use several sets of predefined conventions, you might want to reference the assembly ConfORM.Shop.dll. It is worth having a look at the source code for any further details.
Similar to the AutoMapper of Fluent NHibernate, ConfORM uses conventions to implicitly define a mapping for the domain model. These conventions can be overridden by us or we can define our own conventions.
Time for action – Using ConfORM to map our domain In this example, again we want to create a simple domain model and use ConfORM to create an implicit mapping for us. We then want to use the NHibernate schema exporter to create the SQL script to create a database schema compatible with our domain.
1.
Start Visual Studio and create a new project. Choose Console Application as the project template. Name the project ConfORMSample.
2
Add references to the two assemblies NHibernate.dll and ConfORM.dll to the project.
3.
Create a folder Domain in the project.
4.
Add classes Customer, Order, and LineItem to the Domain folder of the project. The classes should contain the same code as defined in the preceding exercise in steps 4 through 6.
5.
To the Program class, add the following using statements: using using using using using using using using using using
System; System.Linq; ConfORMSample.Domain; ConfOrm; ConfOrm.NH; NHibernate.Cfg; NHibernate.Cfg.Loquacious; NHibernate.Dialect; NHibernate.Driver; NHibernate.Tool.hbm2ddl; [ 129 ]
Mapping the Model to the Database
6.
In the Main method of the Program class, add code to create a NHibernate configuration object using the fluent configuration API provided by NHibernate: var configuration = new Configuration(); configuration.DataBaseIntegration(db => { db.Dialect(); db.Driver(); });
7.
Note that for this exercise, we only need to specify the database type we want to create a schema creation script for.
8.
Add code to determine the types that need to be mapped, as shown in the following code snippet: var types = typeof (Customer).Assembly.GetTypes() .Where(t =>t.Namespace == typeof (Customer).Namespace);
9.
Create an instance of type ObjectRelationalMapper, which is defined in the ConfORM assembly, as shown in the following code snippet: var orm = new ObjectRelationalMapper();
10. Instruct this mapper that it should create a table for each type to be mapped, as shown in the following code snippet: orm.TablePerClass(types);
11. Add code to create the mappings and add them to the configuration object, as shown in the following code snippet:
var mapper = new Mapper(orm); var hbmMappings = mapper.CompileMappingFor(types); configuration.AddDeserializedMapping(hbmMappings, "MyDomain");
12. Add code to create the schema generation script, as shown in the following code snippet:
var exporter = new SchemaExport(configuration); exporter.Execute(true, false, false);
13. Add the following code snippet to avoid the program from exiting without waiting for confirmation:
Console.Write("Hit enter to exit:"); Console.ReadLine();
[ 130 ]
Chapter 5
14. Run the application and analyze the output. The output should look similar to the following screenshot:
What just happened? With no coding and very little configuration effort, we have been able to make the system create an implicit mapping of our domain model for us and export it as a script, which we can then use to generate our database schema. The script that we generated is based on meaningful conventions defined in ConfORM. All of these conventions can be overridden by us and we can add our own conventions to fine-tune the result to our specific needs.
[ 131 ]
Mapping the Model to the Database
XML mapping To add IntelliSense capabilities to Visual Studio for the NHibernate mapping files, we add the .xsd files, which are part of the NHibernate download, to the solution. For this, create a solution folder named Schema and add the two XML schema definition files nhibernate-configuration.xsd and nhibernate-mapping.xsd to this folder, as shown in the following screenshot:
When defining mapping for an entity, we add a new XML file to the project. The file has to have the extension .hbm.xml. As the name of the file, it is recommended to use the name of the entity you want to map, for example, Product.hbm.xml when mapping the Product entity. As shown in the following screenshot:
[ 132 ]
Chapter 5
We also set the Build Action property of the XML file to Embedded Resource, as shown in the following screenshot:
Getting started The XML mapping document begins like any XML document, with an XML declaration. No magic here, just a simple xml tag and two attributes, version and encoding.
The next tag we are going to see in our document is the hibernate-mapping tag. This tag has an attribute named xmlns, which is the XML namespace that the NHibernate mapping file should be validated against. This is directly related to a version of NHibernate, as each version has its own XML namespace to cover changes in the mapping language. We can also use this tag to define the namespace and assembly that the class we are mapping resides in. The opening and closing tags for the hibernate-mapping tag are as shown in the following code snippet:
These three properties within the hibernate-mapping tag make up the basic XML mapping document. [ 133 ]
Mapping the Model to the Database
Classes The next tag we need to define in our document is the class tag. This is a key tag, because it tells NHibernate two things—the class this mapping document is meant to represent and the table in the database that the class should map to. The class tag has two attributes we need to be concerned with—name and table.
The name attribute contains the fully-qualified POCO class that we want to map to, including the assembly name. While this can be specified in the standard fully-qualified dotted class name, a comma and then the assembly name, the preferred method is to define the namespace and assembly in the tag, as shown in the code in the previous section.
The table attribute specifies the table in the database that this mapping file represents. It can be as simple as the name of the table, Product, or as complex as needed, to adequately describe the table. If you need to include the owner of the table, such as dbo.Product, then you can add the schema attribute as follows: schema="dbo"
If we were going to map the Product class in our application to the Product table in the database, then we would use a tag as shown in the following code snippet:
As in this example, the table name is the same as our class name; we should leave out the table attribute to not unnecessarily clutter our code.
Properties We can map properties from our class to fields in the database using the id tag and the property tag. These tags are for the standard fields in the database, not the foreign key fields. We'll get to those in a minute.
[ 134 ]
Chapter 5
The id and property tags follow a standard pattern and have a number of optional parameters. They follow the basic format of defining the property on the class that they are mapping to and the data type that is used to represent that data. This will generally look similar to the following code snippet:
This is the fully verbose method of mapping the properties. Alternately, you can map the property as follows:
Both methods will provide the same mapping to NHibernate but, as stated earlier, the more verbose method gives you a lot of flexibility, while the latter method keeps your code clean and terse, free from unnecessary clutter. Generally, it is recommended to use the latter method and add more details only if needed. One of the optional attributes that people often use on the id and property tags is the type attribute. With this attribute, we can instruct NHibernate that we are using a particular data type to store that information in our class. Adding this data type, our property tag would look as shown in the following code snippet:
Again, it is recommended not to use this attribute if NHibernate can determine the property type by reflection. It is only used in scenarios where reflection does lead to an undesired result as, for example, when mapping string type properties to a database field of type CLOB.
ID columns The first property from our class that we want to map is the Id property. This tag has a number of attributes we can optionally set, but the simplest way we can map the Id property is as shown in the following code snippet:
This instructs NHibernate that we have a property in our class named Id, which maps to a field in the database called Id, and also that we use the hilo generator provided by NHibernate to automatically generate a value for this field. Simple enough! [ 135 ]
Mapping the Model to the Database
An optional attribute that is sometimes used on the id tag is the unsaved-value attribute. This attribute specifies what value should be returned in a new object before it is persisted to the database. By adding this attribute, as well as the type attribute we talked about, the code would look similar to the following code snippet:
As long as our field is named Id in the database, we are good to go. However, what if it was named id or product_id? This simply wouldn't handle it. In that case, we would have to add the optional column tag to identify it, as shown in the following code snippet:
Now we have mapped our product_id field from the database into a more standard Id property on our class. Some of the additional attributes that are commonly used on the column tag are as follows:
name: Defines the name of the column in the database.
length: The length of the field, as defined in the database.
sql-type: The database definition of the column type.
not-null: Whether or not the database column allows nulls. not-null="true"
specifies a required field.
Again, these optional attributes simply allow you to further define how your database is created. Some people don't even define the database. They just define the hbm.xml files and, for example, use NHibernate.Tool.hbm2ddl to create a SQL script to do this work!
One-to-many relations In our domain model, we have the Order entity that has a property LineItems, which is a collection of LineItem entities. Each LineItem entity, in turn, has a property Order that references the order to which the line item belongs. Let's first look at how we would map the "one" side of the relation, namely the Order property in the LineItem entity:
[ 136 ]
Chapter 5
Here, the name attribute defines the name of the property in the LineItem class. The class attribute defines which class the property references. The column attribute instructs NHibernate how the database field is called, which contains the foreign key to the Order table. Finally, the not-null attribute defines that the OrderId foreign key cannot be null. Now that we have the "one" side mapped, we need to map the "many" side. In the Order mapping file, we need to create a bag element to hold all of these line items. A bag is the NHibernate way of saying that it is an unordered collection allowing duplicated items. We have a name element to reference the class property, just like all of our other mapping elements, and a key child element to instruct NHibernate which database column this field is meant to represent.
If you look at the preceding XML code, then you will see that the one-to-many tag looks very similar to the many-to-one tag we just created for the other side. That's because this is the inverse side of the relationship. We even tell NHibernate that the inverse relationship exists by using the inverse attribute on the bag element. The class attribute on this tag is just the name of the class that represents the other side of the relationship. The cascade attribute instructs NHibernate how to handle objects when we delete them. Another attribute we can add to the bag tag is the lazy attribute. This instructs NHibernate to use lazy loading, which means that the record won't be pulled from the database or loaded into memory until you actually use it. This is a huge performance gain because you only get data when you need it, without having to do anything. When we say "get Order record with Id 14", NHibernate will get the Order record, but it won't retrieve the associated line items (LineItem records) until we reference Order.LineItems to display or act on those objects in our code. By default, lazy loading is turned on, so we only need to specify this tag if we want to turn lazy loading off by using lazy="false".
[ 137 ]
Mapping the Model to the Database
Many-to-many relations The other relationship that is used quite often is the many-to-many (MTM) relationship. In the following screenshot, the BookAuthor table is used to join the Book and Author tables. NHibernate is smart enough to manage these MTM relationships for us, and we can "optimize out" the join table from our classes and let NHibernate take care of it.
Just like the one-to-many (OTM) relationship, we represent the Authors on the Book class with a collection of Author objects as follows: public IEnumerable Authors { get; set; }
Furthermore, we represent the Books on the Author class with a collection of Book objects, as shown in the following code snippet: public IEnumerable Books { get; set; }
[ 138 ]
Chapter 5
Mapping the MTM is very similar to the OTM, just a little more complex. We still use a bag and we still have a key. We need to add the table attribute to the bag element to let NHibernate know which table we are really storing the relationship data in. Instead of a one-to-many and a many-to-one attribute, both sides use a many-to-many element. (Makes sense, it is an MTM relationship, right?). The many-to-many element structure is the same as the one-to-many element, with a class attribute and a column attribute to describe the relationship, as shown in the following code snippet:
From the Author side, it looks remarkably similar, as it's just the opposite view of the same relationship, as shown in the following code snippet:
Mapping value objects To map properties whose type is a value object, we can use the component tag. As an example, we see how we would map the property Name of the Customer entity, as shown in the following code snippet:
Time for action – Mapping a simple domain using XML In this example, we want to create a very simple domain model and map it using XML files. We will then let NHibernate create a SQL schema generation script based on these mapping files.
1.
Start Visual Studio and create a new project. Choose Console Application as the project template. Name the project XmlMappingSample.
2.
Add references to the assembly NHibernate.dll to the project.
3.
Add a new folder to the solution. Call the folder Schema. [ 139 ]
Mapping the Model to the Database
4.
Add the file nhibernate-mapping.xsd, which is located in the lib folder, to the Schema folder. (If the said file is not yet located in your lib folder, then please extract it from the ZIP file containing the NHibernate binaries—that you downloaded in Chapter 2—and copy it to the lib folder.)
5.
Create a folder Domain in the project.
6.
Add classes Customer, Order, and LineItem to the Domain folder of the project. The classes should contain the same code as defined in the exercise at the end of the section about auto-mapping (steps 4 through 6).
7.
Add an XML file to the folder Domain of the project and call the file Customer. hbm.xml. Make sure the Build Action of the file is set to Embedded Resource.
8.
Add the hibernate-mapping tag to the file and specify urn:nhibernatemapping-2.2 as the value for the xmlns attribute. Also, specify the assembly and the namespace attributes using the correct values, as shown in the following code snippet:
9.
Add code to map the Customer entity. Use the hilo generator to generate new IDs, as shown in the following code snippet:
10. Add an XML file to the folder Domain of the project and call the file LineItem. hbm.xml. Do not forget to set the Build Action to Embedded Resource.
11. Add the following XML to map the LineItem entity, as shown in the following code snippet:
[ 140 ]
Chapter 5
12. Add an XML file to the folder Domain of the project and call the file Order.hbm. xml. Set Build Action to Embedded Resource.
13. Add the following XML to map the Order entity, as shown in the following code snippet:
14. Add the following using statements to the Program class: using using using using using using using
System; NHibernate.Cfg; NHibernate.Cfg.Loquacious; NHibernate.Dialect; NHibernate.Driver; NHibernate.Tool.hbm2ddl; XmlMappingSample.Domain;
15. In the Main method of the Program class, add code to create a NHibernate
configuration object using the fluent configuration API provided by NHibernate, as shown in the following code snippet: var configuration = new Configuration(); configuration.DataBaseIntegration(db => { db.Dialect(); db.Driver(); });
[ 141 ]
Mapping the Model to the Database
16. Note that for this exercise we only need to specify the database type we want to create a schema creation script for.
17. Add code which adds all XML mapping files to the configuration, as shown in the following code snippet:
configuration.AddAssembly(typeof (Customer).Assembly);
18. Add code to create the SQL schema generation script, as shown in the following code snippet:
var exporter = new SchemaExport(configuration); exporter.Execute(true, false, false);
19. Add code to avoid the program exiting without waiting for confirmation, as shown in the following code snippet:
Console.Write("Hit enter to exit:"); Console.ReadLine();
20. Run the application and analyze the output. The output should look similar to the following screenshot:
[ 142 ]
Chapter 5
What just happened? In the preceding exercise, we defined the mapping for a simple domain using XML. We added the various XML documents as XML files to the solution and defined their Build Action to be Embedded Resource. Also, we followed the rule that the filename has to have an extension *.hbm.xml. Finally, we used the SchemaExport class of NHibernate to generate the database schema from the mapping.
Pop quiz – XML mapping 1. What are the characteristics of XML mapping? a. It is very flexible. b. It has only lately been added to NHibernate. c. It is terse and highly readable. d. It is very verbose. e. All of the above.
Summary In this chapter, we discussed various possibilities to map our domain to an underlying database. Specifically, we covered:
The different types of mappings that exist
How we map entities and their properties to database tables and table columns
The possibility to influence the mapping process by defining new, or override existing specifications of the mapping framework
How we can avoid defining explicit mappings altogether
Using NHibernate to create a SQL schema creation script
Now that we have defined our domain and mapped it to a database, we can move on to discuss the session and transaction objects, which are used to communicate to the database when storing and/or retrieving data.
[ 143 ]
6
Sessions and Transactions In this chapter, we will discuss the NHibernate session object which is used to communicate with the database.
In this chapter, we shall:
Discuss the session and the transaction objects
Present the session factory object
Implement an application that saves and retrieves data
Analyze various methods to manage sessions in the most common application types
So let's get on with it.
What are sessions and transactions Sessions and transactions are two of the most important objects that the NHibernate framework provides. Through a session object, we are able to communicate with a database and execute various operations on it. A transaction object, in turn, gives us an instrument at hand, which makes managing multiple operations as a unity possible and easy.
Session Think of a NHibernate session as an abstract or virtual conduit for the database. Gone are the days when you had to create an ADOConnection, open the Connection, pass the Connection to a Command object, create a DataReader from the Command object, and so on.
Sessions and Transactions
With NHibernate, we ask the sessionFactory for a Session object, and that's it. NHibernate handles all of the "real" sessions of the database, connections, pooling, and so on. We reap all the benefits without having to know the underlying intricacies of all of the database backend we are trying to connect to. Through a session object, we can add new data to the database, update and delete existing data in the database, and also read the existing data from the database. All these operations can be executed in an object-oriented way, without having to deal with SQL strings and the intricacies of a specific database product. A session object allows us to communicate with the data store in a database vendor agnostic way. We do not have to care about whether we are accessing an SQL Server database, a MySQL or Oracle database, and so on. NHibernate completely abstracts those details away for us.
Transaction A transaction is a concept which allows us to execute a number of tasks as a single unit of work. The outcome of this unit of work is either that all individual steps are successfully executed, or that if at least one of the tasks fails, then the system returns to its original state, as if nothing had ever happened. In this regard, we also talk of an atomic operation. In ancient history, the Greeks believed that an atom was the smallest particle that existed and that such an atom could not be broken into smaller parts. Similarly, a transaction cannot be partially executed, only as a whole. Individual tasks of a transaction cannot live on their own without causing the system to get into an inconsistent or even invalid state. A good example for a transaction is the transfer of money from one bank account to another. As a bank customer, money transfer is seen as an atomic operation, although several individual tasks have to be executed until money originating from account A is deposited into account B. For simplicity, we can assume that the money transfer consists of the following two tasks:
Task 1: Take 100 US$ out of account A
Task 2: Put 100 US$ into account B
As the money transfer is a transaction, we can have only two possible outcomes: 1. The transfer succeeds and the balance of account A is reduced by 100 US$, while the balance of account B is increased by 100 US$. 2. The transaction fails and the balance of both accounts A and B remain unchanged. What does that mean? Well, let's say Task 1 succeeds but Task 2 fails. In such a case, the system has to compensate Task 1 and undo the action. In our case, the system would have to put back the 100 US$ back into account A. [ 146 ]
Chapter 6
An operation is a transaction if and only if it fulfills the following characteristics. The operation must be:
Atomic – the operation can only be executed as a whole and not broken apart into smaller units of execution.
Consistent – the outcome of the operation has to leave the system in a consistent state.
Isolated – the operation has to run in isolation. The outside world cannot see the result of an unfinished transaction.
Durable – the outcome of the operation is permanent.
We also use the acronym ACID to describe these characteristics. An operation must be ACID to be a transaction.
The session factory NHibernate uses a factory object to create new session instances. This factory object is called a session factory. One single session factory object can create as many session objects as desired. The creation of a new session object is a very cheap operation. On the other hand, the creation of a session factory is very expensive. Depending on the complexity of the system, it can take a considerable amount of time to create a session factory instance. This is the reason why we should only create the session factory once during the whole lifetime of an application. A session factory is specific to a database. If our application only needs or communicates with a single database, then we need only one session factory. If, on the other hand, our application communicates with several different databases, then we need one session factory object per database. In more advanced scenarios, it may well be that we might want to use more than one session factory object, even in the case where we only access one single database. The reasons for this could be that we use more than one schema inside our database and we want to have a session factory per schema; or another reason might be that our application defines several subdomains (often called bound context) and we want to access the database from each subdomain through sessions created by a context-specific session factory.
The session factory is thread-safe. Code running on different threads can use the same session factory object to create new session objects. This contrasts with the fact that a session object can only be used in a single thread. In other words: a session object is not thread-safe. [ 147 ]
Sessions and Transactions
Creating your first session Creating a NHibernate session is very easy. Once we have a sessionFactory object to hand, we can just call the OpenSession method on it: var session = sessionFactory.OpenSession();
Usually, we might want to open a session in a using statement to guarantee that the session is closed and disposed, no matter what the result of the operation is. We might also want to start a transaction for better predictability of the outcome of the operation. Remember, a transaction can either fail as a whole or succeed as a whole; no other outcome is possible. This makes it easier for us to write our application: using (var session = sessionFactory.OpenSession()) { using (var transaction = session.BeginTransaction()) { // create, update, delete, or read data transaction.Commit(); } }
Why do we call Commit? You might have noticed that the above code snippet contains a transaction.Commit() statement. Why do we need such a statement? Basically, Commit closes our transaction and synchronizes the cached objects with the database. Only after this Commit method has been executed successfully are our changes made permanent in the database. Keep in mind that if we don't call Commit explicitly and a transaction ends, then all the changes we made are not flushed to the database, and thus not persisted. We'll talk more about caching in the next session. However, in its simplest configuration, NHibernate uses a first level cache (or the session cache) to store objects. When you first add an object to, or query an object from the database, it is placed into this cache. If you haven't told NHibernate to update or delete an object from the database, and it has already been cached, then it will pull this object from the cache rather than round-tripping to the database, improving performance.
[ 148 ]
Chapter 6
Adding new data to the database When creating new entities, we have to call the Save method of the NHibernate session object. Only then will NHibernate know about the existence of the respective entity and take care of persisting it to the database. The following statement takes care of this: var newProductId = (int)session.Save(newProduct);
Note that the Save method returns the ID of the newly generated record. As there are different strategies on how to create an ID (int or long versus GUID), the return type is of type object and we have to type cast the result to the desired and expected type. Instead of using the return value of the Save method and casting it to the right type, we can as well access the ID property of the just persisted entity to get the value of the newly generated ID. var newProductId = newProduct.Id;
As an alternative to the Save method mentioned above, we can also use the Persist method of the session object to make a transient entity persistent. The Persist method is void and we can directly use the ID property of the entity to retrieve the value of the newly generated entity ID. No type casting is needed in this case. It is important to note that, at the moment, we use the Save or Persist method of the session object to make a transient entity persistent. NHibernate is not going to insert anything into the database. Only once the session gets flushed, the corresponding entity is added to the database. The only exception of this is when we use the identity generator to generate the IDs of our entities. In this case, NHibernate has to make a round trip to the database at the moment we call Save or Persist in order to get the value of the new ID. At the same time, this round trip to the database breaks the concept of the unit of work. This is the main reason why it is a recommended best practice NOT to use the identity generator to generateIDs for entities but rather use, for example, the High-Low or one of the GUID generators.
Reading data from the database To read a single record from the database identified by its primary key value, we can either use the Get or the Load method of the session object. The following code snippet would load the Product entity with ID equal to 1 from the database: var product = session.Get(1);
Thus, in order to use Get or Load, we have to know the ID of the entity we want to access.
[ 149 ]
Sessions and Transactions
If we want to load the list of all the stored entities of a given type, then we can use the Query (extension) method of the session object. This method is part of the LINQ to NHibernate driver. We will discuss this driver and other means of querying in more details in Chapter 9. To load the list of all categories, we can use the following code snippet: var allCategories = session.Query().ToList();
Note the ToList() call at the end of the statement. LINQ to NHibernate returns a list of type IQueryable, which is lazy evaluated. If we want NHibernate to eagerly load all records, then we can force it to do so by calling ToList().
Get versus Load At some point, you might wonder why the NHibernate session provides a Get and a Load method. Do both do the same? If not, which one should I choose? The fact is that Get and Load act very differently if lazy loading is enabled. Both the methods are generic in the type of entity to be loaded. That is, if we want to load a Product entity, we would use the following code snippet: var product = session.Get(…);
On the other hand, if we want to load a Customer entity, then we would use the following code snippet: var customer = session.Get(…);
The method Get expects the primary key value of the desired entity to be passed as a parameter. Thus, to load a product with ID equal to 1, we would use the following code snippet: var product = session.Get(1);
The method Get physically retrieves the requested entity from the database if it exists. If the entity with the given ID does NOT exist in the database, then NULL is returned. If we use the method Load, then NHibernate does not retrieve the entity from the database, but rather creates a proxy object for us. The only property of this (proxied) entity that is populated with the data is the ID, as NHibernate knows the value because we passed it as a parameter to the method. Thus, when executing the following code: var product = session.Load(1):
[ 150 ]
Chapter 6
We get a proxied Product entity whose ID is equal to 1. The moment our code tries to access any property other than the ID, NHibernate loads the entity from the database. In fact, that is what we call lazy loading, as the database call is deferred to the last possible moment. Note that if a Product with ID equal to 1 does not exist in the database, NHibernate will still create a proxy for Product and populate its ID with the value 1.However, the moment we try to access any other property of the entity, NHibernate would try to hydrate the entity from the database and, as such an entity does not exist, NHibernate would throw an exception.
Given this fact, in which situation would we want to use Load? We want to use the Get method of the session object whenever we need to access and manipulate the entity. However, we might want to use the Load method of the session when we don't really need to change or access the details of an entity. Let's see a simple example that will make things clearer. Assume that we want to update an existing Product entity and change its category. A product belongs to a Category and thus, the Product entity has a property of type Category. However, when manipulating products, we don't want to change categories at the same time, only use them. The following code snippet would achieve the desired result: var product = session.Get(productId); product.Category = session.Load(newCategoryId);
NHibernate would generate a SELECT statement to load the product, but it would not load the category. It would just create a proxy which we can assign as the new category to the product. As the method Load does not access the database to verify that the requested category indeed exists, we have to be absolutely sure that the category really exists in the database. Otherwise, NHibernate will throw a foreign key violation exception the moment the session is flushed to the database.
Updating existing data As the NHibernate session object keeps track of any changes we make to a previously loaded object, we do not have to explicitly call update or any other method of the session object to persist the changes to the database. The moment the session object is flushed, the changes are automatically persisted by NHibernate. That is, the following code snippet would do the trick: using (var session = sessionFactory.OpenSession()) using (var tx = session.BeginTransaction()) { [ 151 ]
Sessions and Transactions var product = session.Get(1); product.UnitPrice = 10.55m; // new unit price tx.Commit(); }
Deleting data To delete an existing record in the database, we have to first load it and can then pass the object to the Delete method of the session object, as shown in the following code snippet: var productToDelete = session.Load(productId); session.Delete(productToDelete);
As the entity is only removed from the database by the above code snippet, but is not itself (yet) deleted from memory, the entity becomes transient again. Note that, in turn, to not cause unnecessary database round trips, we can use the Load method of the session object instead of the Get method. The result of the above code snippet is a single SQL delete statement that is sent to the database the moment the session is flushed. The previous optimization is not applicable in the case that the entity to be deleted contains dependent entities and/or collections of child entities that are mapped with cascading delete constraints. In such a case, NHibernate will have to load the entity into memory.
First level cache or identity map To achieve better performance, NHibernate intelligently caches data. There are different caching mechanisms in play. The most important one is the first level cache. Each session object maintains such a first level cache. The cache is created when the session object is created and is destroyed when the session object is disposed. A cache is nothing else but a hashtable. A hashtable stores values by a unique key. A value can be retrieved very efficiently by knowing its key. NHibernate deals with entities. We have learned what entities are in Chapter 3, when we talked about the model. An entity is uniquely identified by its ID. We have mentioned that two entities are equal if they are of the same type and their IDs match. NHibernate now stipulates that two objects of the same type cannot exist having the same ID. The reason is that if we would allow the system to have two instances with the same ID, then we could put the system in an inconsistent state.
[ 152 ]
Chapter 6
With this boundary condition set so that only one instance of an entity can exist for a given ID, NHibernate can do the following: An entity is loaded from the database by using its ID. NHibernate puts this entity into the first level cache. The key to access this entity is the value of its ID. When the system tries to load the same entity again from the database, then the session object first consults its cache. If the entity already exists in the cache, then NHibernate returns the cached instance. Only if the cache does not yet contain the entity with the requested ID, does the NHibernate session object load the entity from the database. The following points illustrate in detail what typically happens:
Application requests a product with ID = 1 from the session
Session asks the first level cache: "Do you contain product with ID 1?"
First level cache responds: "No"
Session loads product with ID 1 from the database
Session puts product into the first level cache, assigning it as a key for the value of its ID (= 1)
Session returns product instance to application
Application executes more operations
Application again requests a product with ID = 1 from the session
Session asks first level cache: "Do you contain product with ID 1?"
First level cache responds: "Yes"
Session loads product with ID 1 from the cache using the ID as a key and returns it to the application
As the session object uses the ID of the entity as a key to store it in the cache, the first level cache is also called an identity map. We use the following code to load an entity from the database and implicitly make the NHibernate session store it in its first level cache: var product = sesson.Get(1);
Now each subsequent Get does not cause NHibernate to query the database but rather retrieve the object from the first level cache.
[ 153 ]
Sessions and Transactions
Clearing the cache We can ask the session to remove an entity from the first level cache by using the following statement: session.Evict(product);
If we want to completely clean the cache, we can use the following code snippet: session.Clear();
The above statements should be used only in very specific situations because, if used incorrectly, they can lead to a significant performance reduction. It is advised that you only ever use these operations when writing test code for the system. We will discuss the details about this in the next chapter.
Refreshing entities in the cache If we want to refresh a single entity that is already in the first level cache from the database, then we can use the following statement: session.Refresh(product);
The preceding code snippet will reload the state of the entity product from the database. This scenario makes sense in the case where the entity has been updated in the database by some other application or process while our session is open.
No database operation without a transaction Whenever we execute operations that manipulate data in a database, we should wrap these operations inside a transaction: using( var session = sessionFactory.OpenSession()) { using (var transaction = session.BeginTransaction()) { // code that manipulates data… } }
Each relational database product automatically generates an implicit transaction for us if we do not use our own explicit transaction. This has several drawbacks, one of them being that each single task is a transaction. Multiple tasks cannot be handled as a single unit of work. Additionally, there is also a slight performance penalty to be expected if not using explicit transactions wrapping multiple tasks. [ 154 ]
Chapter 6
Should I use transactions when querying data? For optimal performance and predictability of the result, we should also wrap read operations into an explicit transaction. In fact, this is a recommended practice when working with NHibernate.
NHibernate session versus database session When we are talking of a database session in the context of ADO.NET, we are actually thinking of an open connection to a database through which we can execute various commands, such as retrieve or manipulate data. Such a session lasts as long as the corresponding ADO.NET connection is open. In code, this would look similar to the following code snippet: using (var connection = new SqlConnection("...")) { connection.Open(); //... write to or read from DB connection.Close(); }
In NHibernate, a session has a slightly different meaning. We can still think of a NHibernate session being an abstract or virtual conduit to the database. However, this time, there is no need for a physical connection to the database to be established all the time. A NHibernate session can span multiple ADO.NET connections. NHibernate automatically opens an ADO. NET connection to the database only if a write or read operation is taking place. In this regard, NHibernate alleviates us from the burden to open and close connections, define commands to manipulate data in the database, or to use data readers to query data from the database. A NHibernate session provides us with a higher abstraction and makes working with databases much easier. A NHibernate session also provides and manages the first level cache which can tremendously improve the performance of an application. A NHibernate session is a unit of work container, and as such keeps track of all the changes we make to the system.
[ 155 ]
Sessions and Transactions
Time for action – Creating a session and doing some CRUD After all this theory, let's now implement a sample. In this exercise, we will define a simple model and use auto-mapping to map the model. We will then create a session factory which provides us with session objects. We will use different session objects to write data to and read data from the database.
1.
Open SMSS and log in to your local .\SQLEXPRESS database.
2.
Define a new empty database called NHibernateSessionSample.
3.
Open Visual Studio and create a new Console Application project. Call the project NHibernateSessionSample.
4.
To the project, add references to the NHibernate, NHibernate.ByteCode. Castle, and FluentNHibernate assemblies located in the lib folder, as shown in the following screenshot:
5.
Add a folder called Domain to the project.
6.
Add a class file, Order, to the Domain folder of the project.
7.
Add the following code to the file to define the Order entity: public class Order { public virtual int Id { get; set; } public virtual Customer Customer { get; set; } public virtual DateTime OrderDate { get; set; } public virtual IList LineItems { get; set; } } [ 156 ]
Chapter 6
8.
Add a default constructor to the Order class and code to initialize the LineItems collection, as shown in the following code snippet: public Order() { LineItems = new List(); }
9.
Add a virtual method to the Order class to add a line item, as shown in the following code snippet: public virtual void AddLineItem(int quantity, string productCode) { var item = new LineItem { Order = this, Quantity = quantity, ProductCode = productCode }; LineItems.Add(item); }
10. Add a class file, LineItem to the Domain folder of the project. 11. Add the following code to the file to define the LineItem entity: public class LineItem { public virtual int Id { get; set; } public virtual Order Order { get; set; } public virtual int Quantity { get; set; } public virtual string ProductCode { get; set; } }
12. Now add a class file, Customer to the Domain folder of the project. 13. Add the following code to the file to define the Customer entity: public class Customer { public virtual int Id { get; set; } public virtual string CustomerName { get; set; } }
[ 157 ]
Sessions and Transactions
14. Add a class file, OrderingSystemConfiguration to the project. Add the following code to define which classes the auto-mapper should map:
using FluentNHibernate.Automapping; using NHibernateSessionSample.Domain; public class OrderingSystemConfiguration : DefaultAutomappingConfiguration { public override bool ShouldMap(Type type) { returntype.Namespace == typeof(Customer).Namespace; } }
15. Add code to define a static variable for the session factory in the class Program, as shown in the following code snippet:
private static ISessionFactory sessionFactory;
16. Add a static method ConfigureSystem to the Program class of the project.
This method configures NHibernate to use SQL Server as the database. Use automapping to map the domain and automatically create the schema in the database:
private static void ConfigureSystem() { }
17. Add the following code to define a connection string to the above method: const string connString = "server=.\\SQLEXPRESS;database=NHibernateSessionSample;" + "integrated security=SSPI;";
18. Add code to define an instance of the model configuration to the method. This configuration will be used by the auto-mapper, as shown in the following code snippet: var cfg = new OrderingSystemConfiguration();
19. Add the following code to create an auto-mapping model: var model = AutoMap.AssemblyOf(cfg);
20. Nevertheless, in the same method, use the fluent API to create a NHibernate configuration, as shown in the following code snippet: var configuration = Fluently.Configure() .Database(MsSqlConfiguration [ 158 ]
Chapter 6 .MsSql2008 .ConnectionString(connString) .ShowSql ) .Mappings(m =>m.AutoMappings.Add(model)) .BuildConfiguration();
21. Note the .ShowSql call in the above code snippet. This call configures NHibernate to output all SQL statements it sends to the database to the console.
22. After the preceding code snippet, add code to (re-) create the database schema. Use the SchemaExport class of NHibernate and the configuration object just created to do this job, as shown in the following code snippet: var exporter = new SchemaExport(configuration); exporter.Execute(true, true, false);
23. Finally, use the configuration object to create the session factory, as shown in the following code snippet:
sessionFactory = configuration.BuildSessionFactory();
24. Add a static method CreateCustomers to the Program class. This method creates two customer objects with some pre-canned data and stores them in the database by using the session object, as shown in the following code snippet:
private static void CreateCustomers() { var customerA = new Customer {CustomerName = "Microsoft"}; var customerB = new Customer {CustomerName = "Apple Computer"}; using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { session.Save(customerA); session.Save(customerB); transaction.Commit(); } }
25. Note that we use a transaction to wrap the saving of the two customers in
the database. The transaction is made permanent by calling Commitfrom the transaction object.
[ 159 ]
Sessions and Transactions
26. Add code to the Main method of the Program class to configure the system and
create some customer objects. Furthermore, add code so that the program asks for confirmation prior to ending, as shown in the following code snippet:
static void Main() { ConfigureSystem(); CreateCustomers(); Console.Write("\r\nHit enter to exit:"); Console.ReadLine(); }
27. Run the application and try to understand the output generated in the console window. The output should look similar to the following screenshot:
[ 160 ]
Chapter 6
28. The first couple of lines show the SQL commands to clean out the schema if it
already exists, followed by the SQL scripts to generate the new schema. Finally, at the end of the screen, we have the two insert statements which create the customer records in the database.
Next, we want to create an order with some line items and which is associated with an existing customer. We will see how NHibernate, if configured correctly, automatically cascades the insert operation to the line item children and that we do not need to explicitly save the line items.
29. Add a private static method CreateOrder to the Program class, as shown in the following code snippet:
private static int CreateOrder() { }
30. Add code to this method to create a customer object and an order with line items using precanned data, as shown in the following code snippet:
var customer = new Customer {CustomerName = "Intel"}; var order = new Order { Customer = customer, OrderDate = DateTime.Now, }; order.AddLineItem(1, "Apple"); order.AddLineItem(5, "Pear"); order.AddLineItem(3, "Banana");
31. Now add code to save the customer and the order object in the database, as shown in the following code snippet:
int orderId; using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { session.Save(customer); orderId = (int)session.Save(order); transaction.Commit(); }
32. Return the ID of the newly generated order to the caller, as shown in the following code:
return orderId;
[ 161 ]
Sessions and Transactions
33. In the Main method, just after the call to CreateCustomers, add code to call the CreateOrder function, as shown in the following code: var orderId = CreateOrder();
34. Run the application. Note that the application terminates with an exception with the following message: NHibernate.TransientObjectException"object references an unsaved transient instance - save the transient instance before flushing. Type: NHibernateSessionSample.Domain.LineItem, Entity: NHibernateSessionSample. Domain.LineItem"
35. We have to override the default configuration of the auto-mapper regarding how
it maps the HasMany part of the order entity. By default, auto-mapper configures HasMany relations as non-inverse and cascade equal to none. To configure the mapping as we want it, add the following code right after the definition of the automapper model in the ConfigureSystem method (that is after the line var model = AutoMap.AssemblyOf(cfg);): model.Override( map =>map.HasMany(x =>x.LineItems) .Inverse() .Cascade.AllDeleteOrphan() );
36. Run the application again and you should see the output as shown in the following screenshot:
37. Note that the three line items of the order are automatically added to the database without us specifying it explicitly.
[ 162 ]
Chapter 6
Now we want to load an existing order and remove one of its line items and add a new one in its place.
38. Add a new method UpdateOrder to the Program class, as shown in the following code snippet:
private static void UpdateOrder(int orderId) { }
39. Add code to create a new session and start a new transaction, as shown in the following code snippet:
using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { }
40. Inside this new transaction, add code to load the order from the database that we have previously generated, as shown in the following code snippet: var order = session.Get(orderId);
41. Add code to remove the first line item of the order: order.LineItems.RemoveAt(0);
42. Now add code to add a new line item to the order: order.AddLineItem(2, "Apricot");
43. Commit the transaction, as shown in the following code snippet: transaction.Commit();
44. To the Main method of the Program class, add a call to this new UpdateOrder method:
UpdateOrder(orderId);
45. Run the application. The end of the output in the console window should look similar to the following screenshot:
[ 163 ]
Sessions and Transactions
46. The first select statement loads the order. The second select statement loads the
line items of the order. Lastly, we see an insert statement, which adds the new line item to the database.And last, we have the delete statement, which removes the deleted line item from the database.
Last but not least, let's delete the order. We expect that all the line items of the order will also be deleted by NHibernate without us having to specify it.
1.
Add a new method DeleteOrder to the Program class: private static void DeleteOrder(int orderId) { }
2.
Add code to create a new session and start a new transaction, as shown in the following code snippet: using (var session = sessionFactory.OpenSession()) using (var transaction = session.BeginTransaction()) { }
3.
Inside this new transaction, add code to load the order from the database that we have previously generated: var order = session.Load(orderId);
4.
Add code to remove order from the system, as shown in the following code snippet: session.Delete(order);
5.
Do not forget to commit the deletion of the order, as shown in the following code snippet: transaction.Commit();
6.
Now, add code to the Main method to call this DeleteOrder method, as shown in the following code snippet: DeleteOrder(orderId);
7.
Run the application. The end of the output in the console window should look similar to the following screenshot:
[ 164 ]
Chapter 6
8.
The first select statement loads the order, the second one loads the line items of the order. All the line items of the order are deleted before the order itself is deleted.
What just happened? We have created a simple domain model and used Fluent NHibernate's auto-mapping to map the domain to the underlying database schema. With the help of the SchemaExport class of NHibernate, we then created the database schema from the mapping. We then created a session object and used it to create the Customer, Order, and LineItem entries in the respective database tables. Subsequently, we used the session
object to update and also delete existing records in the database.
Pop quiz – Creating, updating, and deleting data 1. You want to create a new session. What object do you use? a. I need no other object; I just create a new session instance by using the new keyword. b. The NHibernate configuration object. c. The session factory object. d. None of the above. 2. Which method of the session object can you use to add a new object to the database? a. session.Update(…) b. session.SaveOrUpdate(…) c. session.Save(…) d. session.New(…)
[ 165 ]
Sessions and Transactions
3. How do you remove a record from the database? a. session.Remove(…) b. session.Clear(…) c. session.Delete(…) d. transaction.Commit()
Session management When developing applications on .NET, you will probably, sooner or later, have to deal with one or several of the following three types of applications: 1. Web-based applications, including Silverlight applications. 2. WinForm or WPF applications. 3. Windows services. There are, of course, many other types of applications, but they can all probably use the same approach regarding NHibernate session management as one of the three types mentioned above.
Web-based applications Because of the stateless nature of web applications, we cannot (and should not!) try to maintain and use a single session across multiple page requests. However, as the creation of a session object is a very cheap operation, this should not be a problem per se. On the other hand, we have to remember that creating a session factory can be very expensive. Thus, the session factory should only be created once during the life cycle of the web application. It turns out that creating the session factory during the application start-up is probably the best way to do it. The session factory is thread-safe, which is very important in web-based applications,as every single web request can happen on a different thread. After creation, the session factory can, for example, be stored in the ASP.NET application object, which makes it available globally throughout the whole application. One of the most commonly used patterns for NHibernate sessions is the session per request pattern. A new session object is created at the beginning of a new web request. The session object is then flushed and disposed at the end of the web request.
[ 166 ]
Chapter 6
It is highly recommended to use an IoC container to manage and configure objects when implementing a web application. Having an IoC container at hand, we would store the session object in the container, and in this way, make it available to all other objects through dependency injection. However, for this introduction, it is assumed that we have no IoC container at hand. Thus, let's provide a solution for this case. We will use the singleton pattern to implement a session provider.
Time for action –Implementing session management for a web application In this sample, we will create a simple Silverlight application and implement a basic session management for this application. A Silverlight application is a web-based application, and thus the techniques learned here can be applied to any other type of web application (for example, ASP.NET MVC or WebForms).
1.
Open SSMS and log in to your local .\SQLEXPRESS database server.
2.
Create a new database called SilverlightSample.
3.
Open Visual Studio and create a new project.
4.
Select Silverlight Application as the project template and call the project SilverlightSample, as shown in the following screenshot:
[ 167 ]
Sessions and Transactions
5.
When asked whether to host the Silverlight application in a new website, leave all the defaults, as shown in the following screenshot, and click on OK to continue.
6.
To the SilverlightSample.Web project, add references to the three assemblies, NHibernate, NHibernate.ByteCode.Castle, and FluentNHibernate, located in the lib folder.
7.
Add the class file Product.cs to the web project (SilverlightSample.Web) and add code to define a simple Product entity, as shown in the following code snippet: public class Product { public virtual int Id { get; set; } public virtual string Name { get; set; } }
8.
Now add a class file ProductMap.cs to the web project to define the mapping of the Product entity, as shown in the following code snippet: public class ProductMap : ClassMap { public ProductMap() { Id(x =>x.Id).GeneratedBy.HiLo("1000"); Map(x =>x.Name); } } [ 168 ]
Chapter 6
9.
Add a class SessionProvider to the SilverlightSample.Web project. This class has a static property of type SessionProvider called Instance. The property has a private setter that cannot be changed from outside the class, as shown in the following code snippet: public class SessionProvider { public static SessionProvider Instance { get; private set; } }
10. Add a static instance variable of type ISessionFactory to the class, as shown in the following code snippet:
private static ISessionFactory sessionFactory;
11. Add a static constructor to the class which initializes the Instance property, as shown in the following code snippet:
static SessionProvider() { var provider = new SessionProvider(); provider.Initialize(); Instance = provider; }
12. Add an empty private constructor to the class that cannot be instantiated, as shown in the following code snippet: private SessionProvider() { }
13. Add an Initialize method to the class which contains the code to configure NHibernate, (re)create the database schema, and create a session factory:
private void Initialize() { const string connString = "server=.\\SQLEXPRESS;database=SilverlightSample;" + "user id=sa;password=sa;"; var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(connString) .ShowSql()) .Mappings(m =>m.FluentMappings .AddFromAssemblyOf()) .BuildConfiguration(); [ 169 ]
Sessions and Transactions var exporter = new SchemaExport(configuration); exporter.Execute(true, true, false); sessionFactory = configuration.BuildSessionFactory(); }
14. Note that this time, in the definition of the connection string, we don't use
integrated security but rather a user ID/password combination. Make sure that you use a combination which has the appropriate rights on your SQL Server Express database.
15. Add a public method OpenSession, which returns ISession to the class.
This method uses the session factory to get a new session object and return it to the caller: public ISession OpenSession() { return sessionFactory.OpenSession(); }
16. Add a Silverlight-enabled WCF Service to the web project. Call the service ProductService, as shown in the following screenshot:
[ 170 ]
Chapter 6
17. Delete the default method in the class and add a method CreateProduct, which has a return type of int:
[OperationContract] public int CreateProduct() { }
18. Add code to the method to create a product, open a new session, start a new
transaction, and save the product to the database. Return the ID of the newly created product. The service should look similar to the following code snippet: using System.ServiceModel; using System.ServiceModel.Activation; namespace SilverlightSample.Web { [ServiceContract(Namespace = "")] [AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Allowed)] public class ProductService { [OperationContract] public int CreateProduct() { using (var session = SessionProvider .Instance.OpenSession()) using (var transaction = session.BeginTransaction()) { var product = new Product {Name = "Apple"}; var productId = (int)session.Save(product); transaction.Commit(); return productId; } } } }
19. Note that when adding the WCF service to the web project, Visual Studio
automatically updated the Web.config file of the project to configure an endpoint for this new WCF service. You can leave these default values as they are and the application will work.
20. Compile the solution. This step is important for the next steps to work properly! [ 171 ]
Sessions and Transactions
21. In the Solution Explorer window, right-click on the SilverlightSample project and select Add Service Reference….
22. In the Add Service Reference dialog window, click on the Discover button. 23. Visual Studio should discover the ProductService.svc service on the local
machine. Expand the ProductService.svc node in the Services list, as shown in the following screenshot:
24. Note that, by default, the Namespace is defined as ServiceReference1. Change this to ProductService.
[ 172 ]
Chapter 6
25. Click on OK to continue. Visual Studio will create a proxy to the web service, which can be used by the client (the Silverlight application), as shown in the following screenshot:
26. In the SilverlightSample project, add code to the MainPage.xaml to create a button, as shown in the following code snippet:
27. Add code to the Click event handler in the preceding code snippet (the
MainPage.xaml.cs file), which uses the ProductService proxy to create a product in the database, as shown in the following code snippet: private void OnCreateProduct(object sender, RoutedEventArgs e) { var client = new ProductServiceClient(); client.CreateProductCompleted += OnProductCreated; [ 173 ]
Sessions and Transactions client.CreateProductAsync(); } private void OnProductCreated(object sender, CreateProductCompletedEventArgs e) { MessageBox.Show(string.Format( "Created product with id={0}", e.Result)); }
28. Run the application and click on the CreateProduct button. The application should respond with a message box similar to the following screenshot:
What just happened? We created a basic session provider which can be used in a web-based application such as an ASP.NET MVC, WebForms, or Silverlight application. Web applications are stateless in nature and we have used the singleton pattern to deal with this fact, and have a means to easily create sessions.
WinForm or WPF applications These types of applications are what we call state-full and it is "easier" to manage sessions in these scenarios. Usually, we would want to open a session per screen and keep the session open as long as the screen is open. If you are using a Model-View-Presenter (MVP) pattern for your application, then the presenter would be responsible for managing the session. Keep in mind though that if an exception happens during the lifetime of a screen, then the NHibernate session is in an inconsistent state and cannot be used any further. You will have to close this session and open a new one.
Windows services In a Windows service, we typically create the session factory when the service starts and we dispose the factory when the service stops. The decision of when a session has to be created depends on the nature of the database communication of the service. Most probably, we would want to create a session whenever we need to execute a (business) transaction. [ 174 ]
Chapter 6
Unit of Work When we load objects from the database and change them, it is important to keep track of what we have changed; otherwise, the changed objects won't be written to the database. Furthermore, new objects have to be added to the database and objects we delete have to be removed from the database. A Unit of Work (UoW) is used to keep track of everything that happens during a business transaction and that affects the database. It keeps track of every single step needed to update the database once the business transaction is completed. The UoW guarantees that the individual database operations needed to update the system are executed in the right order, to avoid any database constraint violations. In NHibernate, we have the Session object, which is a UoW container. The session object keeps track of all objects we load, new objects we add, existing objects we delete, and changes we make to any of these objects. Only when the session is being flushed will the database be altered. (That is, only then will the necessary create, update, and delete statements be sent to the database.)
Handling exception We can put it very short and simply: all NHibernate exceptions are non-recoverable! NHibernate use might lead to exceptions, usually, HibernateException. This exception can have a nested inner exception. To access the root cause of the exception, we can and should use the InnerException property. If an exception happens during a database operation, then the NHibernate session object is in an inconsistent state and should NOT be used any more. It is best to immediately rollback the active transaction, dispose the current session, and start over. Here is a code snippet we can use to deal with the possibility of exception happening: var session = sessionFactory.OpenSession(); var transaction = session.BeginTransaction(); try { // do some work ... transaction.Commit(); } catch (Exception e) { transaction.Rollback();
[ 175 ]
Sessions and Transactions throw; } finally { session.Close(); }
Second level cache We have seen that NHibernate provides a very efficient way to cache data. Unfortunately, this first level cache is bound to a session object. This means that each time a session is disposed, all the cached data is lost. Sometimes, we need to be a little bit more flexible. We might want to cache some data not just for the duration of a session's lifetime, but rather globally, and make it available to all session objects. For such scenarios, NHibernate introduces the concept of the second level cache. The second level cache is defined per session factory and lives as long as the session factory is not disposed. Once an entity is loaded by its unique ID and the second level cache is active, the entity is available for all other sessions (of the same session factory). Thus, once the entity is in the second level cache, NHibernate won't load the entity from the database until it is removed from the cache. To enable the second level cache, we have to define which cache provider we want to use. There exist various implementations of a second level cache. For our sample, we use a hashtable-based cache, which is included in the core NHibernate assembly. Please note that you should never use this cache provider for production level code, only for testing. Please refer to the section Second level cache implementations below, to decide which implementation best fits your needs; though you won't have to change your code if you change the cache provider. Using code similar to the following code snippet would cause NHibernate to only access the database once to retrieve the product with ID 1, although we use two different session instances: using (var session1 = sessionFactory.OpenSession()) { var product = session1.Get(1); } using (var session2 = sessionFactory.OpenSession()) { var product = session2.Get(1); }
[ 176 ]
Chapter 6
The second Get operation would take the product entity out of the second level cache. Note, however, that without having the second level cache enabled, the above code would result in two requests to the database as the first level cache of each session cannot be used by any other session. Additionally, note that to enable the second level cache, we have to configure NHibernate accordingly. The details of this configuration will be explained in Chapter 8. We also have to define the mapping of the entity that it is cacheable. If we are using fluent mappings provided by Fluent NHibernate to map our entities, then the necessary statement to add to the mapping would be: Cache.ReadWrite();
Only entities that are explicitly configured will be cached in the second level cache.
Cache regions If we don't use cache regions, then the second level cache can only be cleared as a whole. If we need to clear only part of the second level cache then we use regions. Regions are distinguished by their name. We can put any number of different queries into a named cache region. The command to clear a cache region is as follows: sessionFactory.EvictQueries("My Region");
sessionFactory is the session factory instance currently used and My Region is the name
of the cache region.
Second level cache implementations All second level cache providers are part of the NHibernate contributions project. The following list gives a short description of some of the supported providers.
SysCache: Uses System.Web.Caching.Cache as the cache provider. This means that you can rely on the ASP.NET caching feature to understand how it works.
SysCache2: Similar to NHibernate.Caches.SysCache, uses ASP.NET cache. This provider also supports SQL dependency-based expiration, meaning that it is possible to configure certain cache regions to automatically expire when the relevant data in the database changes.
Velocity: Caching provider, which is part of Microsoft Windows Server App Fabric, which in turn is a set of integrated services to build, scale, and manage IIS-based web applications.
[ 177 ]
Sessions and Transactions
Prevalence: Uses Bamboo.Prevalence as the cache provider. Bamboo. Prevalence is a .NET implementation of the object prevalence concept brought to life by Klaus Wuestefeld in Prevayler. Bamboo.Prevalence provides transparent object persistence to deterministic systems targeting the CLR. It offers persistent caching for smart client applications.
MemCache: Uses Memcached. Memcached is a high-performance, distributed memory object caching system, generic in nature, but intended for use in speeding up dynamic web applications by alleviating database load. Basically, a distributed hash table.
Time for action – Using a second level cache In this exercise, we want to implement a very simple application which uses NHibernate's second level cache. We use the hashtable cache provider in this exercise, but please make sure that you are never using this provider in the production level code!
1.
Open SSMS and log in to your local SQL Express database.
2.
Add a new database named SecondLevelCacheSample to the database server.
3.
Open Visual Studio and create a new Console Application named SecondLevelCacheSample.
4.
Add references to the NHibernate.dll, FluentNHibernate.dll, and NHibernate.ByteCode.Castle.dll assemblies located in the lib folder.
5.
Create a folder Domain in the Project.
6.
Add a class file, Product.cs, to the Domain folder of the project.
7.
Add the following code to the class to define the Product entity, as shown in the following code snippet: public class Product { public virtual int Id { get; set; } public virtual string Name { get; set; } public virtual decimal UnitPrice { get; set; } public virtual int ReorderLevel { get; set; } public virtual bool Discontinued { get; set; } }
8.
Add a class file, ProductMap.cs to the Domain folder of the project. This will be the mapping file for the Product entity:
[ 178 ]
Chapter 6
9.
The mapping file shall contain the following code: public class ProductMap : ClassMap { publicProductMap() { Cache.ReadWrite(); Id(x =>x.Id).GeneratedBy.HiLo("1000"); Map(x =>x.Name); Map(x =>x.UnitPrice); Map(x =>x.ReorderLevel); Map(x =>x.Discontinued); } }
10. Now that we have added a Cache.ReadWrite() statement to the mapping, this instructs NHibernate to use the second level cache for the Product entity.
11. Add a static field of type ISessionFactory to the Program class, as shown in the following code snippet:
private static ISessionFactory sessionFactory;
12. Add a static method ConfigureSystem to the Program class. This method will contain the code to configure NHibernate in general, and specifically the second level cache; as shown in the following code snippet:
private static void ConfigureSystem() { }
13. Add the following code to define a connection string to the database SecondLevelCacheSample we defined in step 2:
const string connString= "server=.\\SQLEXPRESS;database=SecondLevelCacheSample;" + "integrated security=SSPI;";
14. Use the fluent API of Fluent NHibernate to define a NHibernate configuration object, as shown in the following code snippet:
var configuration = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008 .ConnectionString(connString) .ShowSql) .Mappings(m =>m.FluentMappings .AddFromAssemblyOf()) .BuildConfiguration();
[ 179 ]
Sessions and Transactions
15. Add code that extends the configuration to use the second level cache, as shown in the following code snippet:
configuration.Properties["cache.provider_class"] = "NHibernate.Cache.HashtableCacheProvider"; configuration.Properties["cache.use_second_level_cache"] = "true";
16. The first value tells NHibernate which second level cache provider to use. It is a
key value pair. The value is the full path to the class which implements the second level cache provider. In our sample, we do not need to define the assembly, as the provider we chose resides in the NHibernate assembly.
17. The second value enables or disables the usage of the second level cache,
depending on the setting. Note that, technically, this second setting is not needed as the value is equal to true by default.
18. Now, add code to the ConfigureSystem method, which (re)creates the database
schema by using the SchemaExport class of NHibernate, as shown in the following code snippet: var exporter = new SchemaExport(configuration); exporter.Execute(true, true, false);
19. Finally, still to the same method, add code to generate a session factory from the configuration object, as shown in the following code:
sessionFactory = configuration.BuildSessionFactory();
20. In the Program class, create a static method TestLoadEntity, as shown in the following code snippet:
private static void TestLoadEntity() { }
21. In the preceding method, implement code to create a product instance and save it in the database, as shown in the following code snippet:
int productId; var product = new Product { Name = "Apple", UnitPrice = 1.55m, ReorderLevel = 100, Discontinued = false }; using (var session = sessionFactory.OpenSession()) [ 180 ]
Chapter 6 using (var tx = session.BeginTransaction()) { productId = (int) session.Save(product); tx.Commit(); }
22. To the same method, add code which opens two different sessions and, through each session, loads the product just stored by the preceding code fragment, as shown in the following code snippet: using (var session1 = sessionFactory.OpenSession()) { var product1 = session1.Get(productId); } using (var session2 = sessionFactory.OpenSession()) { var product2 = session2.Get(productId); }
23. Add code to the Main method in the Program class, which calls the
ConfigureSystem and the TestLoadEntity methods, and asks the user to hit Enter to exit the application: static void Main() { ConfigureSystem(); TestLoadEntity(); Console.Write("\r\nHit enter to exit:"); Console.ReadLine(); }
24. Run the application and verify that no select statement is generated by NHibernate. This shows us that the second level cache is indeed in action. The (last part of the) output on the console should look similar to the following screenshot:
25. We just have the insert statement, which creates the product in the database, but no select statement to read the product from the database. [ 181 ]
Sessions and Transactions
26. Now, change the cache.use_second_level_cache setting to false and run the
program again. This time, two select statements should be sent to the database; one for each session, as shown in the following screenshot:
27. Obviously, in addition to the insert statement, we have two select statements: one for each session object used to load the product entity.
What just happened? In the previous exercise, we have learned how to configure our application such that NHibernate uses a second level cache. We also saw how to configure the mapping of our entities such that they can be cached by NHibernate in the second level cache. We also realized that, with a single switch, we can turn second level caching on and off.
Summary We learned a lot in this chapter about how we can access the database through NHibernate and create, update, or delete objects in the database. We have also discussed the most basic methods of retrieving existing data from the database. Specifically, we covered:
What a NHibernate session is and how we use it
Why we want to use transactions
The session factory and how we can create one
Managing sessions in various types of applications
Now that we've learned so much about sessions and how to access the database through a session to manipulate data, we are ready to move on and discuss (unit) testing, which will be the main topic of the next chapter. [ 182 ]
7
Testing, Profiling, Monitoring, and Logging Up till now, we were living in a perfect world, where no errors occurred. We were assuming that everything works just fine and as expected. Unfortunately, reality can be cruel and things just don't always work out as expected.
In this chapter, we shall:
Implement a base framework to make testing database access code easy
Create tests to verify our data access code
Analyze the communication of NHibernate with the database
Configure NHibernate to log interesting information
So let's get on with it.
Why do we need tests? There are different reasons why we would want to write tests for our code. In test-driven development (TDD), we write tests to help us write better code. In this regard, tests are a means to improve the micro design of our application. However, more than that, tests also act as a safety net in case we start refactoring or changing our system.
Testing, Profiling, Monitoring, and Logging
What should we test? This is a rather philosophical question and it largely depends on the specific situation. Some development groups try to write tests for every single line of code they write. In this case, one would have 100 percent code coverage. However, this is not always possible or cost-effective. In the latter case, we have to come up with a list of areas that are worth testing. Examples on the list can be:
Complex mathematical or statistical algorithms
Involved business logic or business processes
Code that is used as the foundation or framework in an application
The mapping of the domain model to the underlying database schema
Complex database queries
This list is by no means complete and shall only give an indication of things that are worth testing.
What about the database? The database is an important part of our application and we have to include it in our tests. Although we might create the database schema from our domain model for the first release of our application, a time will come when we have to change this schema, either manually or by using database scripts. At this point in time, we have to make sure that our mappings are still working correctly. We might also want to test various strategies when writing (complex) database queries to minimize the number of database round trips, and/or to optimize the performance of a query. When running tests that include the database, then each time we run a given test the database should be in the same state, otherwise our tests will not always give the same result. There are various ways we can achieve this condition. Some solutions are more performant than the others and some, on the other hand, are easier to achieve.
In the first approach, we wipe out the database schema each time, recreate it, and fill it with the base data we need prior to each test
We write compensating code that removes the changes done by our tests in the clean-up method of our test class [ 184 ]
Chapter 7
We wrap each test inside a transaction that is rolled back at the end of the corresponding test
We use a snapshot of the database that is restored after each test run
Each method presented in the preceding text has its advantages and disadvantages. There is not really one ideal solution. It depends on the specific circumstances which approach makes the most sense.
Download SQLite Often, the first approach is selected as it offers the best repeatability of all of the approaches. Individual tests cannot have side effects on each other and the order in which the tests run do not matter. However, trying to realize this solution with a classical RDBMS will cause the test series to run with very poor performance. If the database is installed remotely, then the situation will be even worse. Thus, we have to come up with a better solution. SQLite is a lightweight open source database that can be used in the in-memory mode, which makes dropping and recreation of the database schema very fast. SQLite can be downloaded from here: http://sourceforge.net/projects/sqlitedotnet2/files/. On this page click on the SQLite for ADO.NET 2.0 link. At the time of writing, the most recent version is 1.0.66.0. Navigate to the folder 1.0.66.0. Download the file SQLite-1.0.66.0-managedonly-binaries.zip. If you have a 64-bit OS installed, then copy the file System.Data.SQlite.dll, contained in the sub-folder bin\x64 of the ZIP file, to the lib sub-folder of your samples folder. Otherwise, copy the same file from the bin sub-folder of the ZIP file to the lib folder.
Pop quiz 1. What are the characteristics of tests? a. When used in conjunction with TDD, they help us to create better code. b. They introduce some maintenance overhead into the system. c. Tests act as a safety net when refactoring code or adding new functionality. d. In certain areas, writing tests can significantly reduce the time to achieve a defect free solution to a problem. e. All of the above.
[ 185 ]
Testing, Profiling, Monitoring, and Logging
Preparing our environment for testing To be able to write unit tests, we need to prepare a small framework. First of all, let's introduce a base class, which allows us to write our unit tests in a more "natural" way that resembles English text, instead of it remaining purely technical babble. A unit test normally consists of three clearly distinguishable parts:
Arrange – we prepare or set up the boundary conditions for our test
Act – we execute the action whose outcome we want to test
Assert – finally, we verify that the outcome of the action is indeed what we expect
These three steps, arrange, act, and assert, are sometimes also denoted with the acronym AAA. In the unit tests, we are going to write these three steps in the form of their counterparts. Arrange will be the given part, act will be the when part, and assert will consist of one or more tests. We will use NUnit (http://www.nunit.org) as our unit test framework, but any other framework out there would also do the job. For example, MSTest (part of Visual Studio), MbUnit (http://www.mbunit.com/), XUnit (http://xunit.codeplex.com/), and so on. We will call our test base class SpecificationBase, it looks similar to the following code snippet: public class SpecificationBase { [TestFixtureSetUp] public virtual void TestFixtureSetUp() { BeforeAllTests(); } [SetUp] public void SetUp() { Given(); When(); } [TearDown] public void TearDown() { CleanUp(); } [ 186 ]
Chapter 7 [TestFixtureTearDown] public void TestFixtureTearDown() { AfterAllTests(); } protected protected protected protected protected
virtual virtual virtual virtual virtual
void void void void void
BeforeAllTests(){} Given(){} When(){} CleanUp(){} AfterAllTests(){}
}
NUnit has four important attributes with which we have to decorate public void methods of our class. The first attribute, TestFixtureSetUp, decorates a method that is only run once for all tests defined in the same class. This method can be used to execute tasks to prepare the system prior to run a test suite. Its counterpart, the TestFixtureTearDown attribute, decorates a method which is only called once after all tests have been executed. This method can be used to do the final cleanup after a test run. The SetUp attribute decorates a method that is run prior to each test method and the TearDown attribute decorates the method that is called after the execution of each
test method.
In our base class, all of these four methods call (empty) virtual methods, which any class that inherits from SpecificationBase can override. One of the advantages of this implementation is that the child classes do not have to deal with attributes anymore. To complete the first layer of the framework, we add a Then attribute. This attribute is defined as a child attribute of TestAttribute of NUnit. We will use the Then attribute in our assertions block to decorate the methods that contain assertions. The code is simple, as shown in the following code snippet: public class ThenAttribute : TestAttribute {}
Having this Then attribute, we can formulate our test using a given-when-then syntax. To demonstrate the usage of the preceding base class, we want to write a unit test that verifies that the properties of the Name value object, introduced in a previous chapter of the book, are correctly populated by the constructor. The code is as shown in the following snippet: [TestFixture] public class when_creating_a_name_value_object : SpecificationBase { [ 187 ]
Testing, Profiling, Monitoring, and Logging private private private private
string lastName; string firstName; string middleName; Name result;
protected override void Given() { lastName = "Schenker"; firstName = "Gabriel"; middleName = "N."; } protected override void When() { result = new Name(lastName, firstName, middleName); } [Then] public void it_should_populate_the_last_name_property() { Assert.Equals(result.LastName, lastName); } [Then] public void it_should_populate_the_first_name_property() { Assert.Equals(result.FirstName, firstName); } [Then] public void it_should_populate_the_middle_name_property() { Assert.Equals(result.MiddleName, middleName); } }
The name of the class reflects what we are going to verify. Sometimes, we call this the test case. The class name should be as close to an English phrase as possible; in fact, if we remove the underscores from when_creating_a_name_value_object, we obtain an intention revealing expression "when creating a name value object". Ideally, we use the title of the test case as our class name.
[ 188 ]
Chapter 7
Next, we prepare the environment for the test, or we can say that we set up the boundary conditions for the test. This is done in the Given method. In this case, we define some precanned values for the last, first, and middle name that we want to use in our test. Now, we can define the action in the When method. The action in our case is the instantiation of a Name value object. We store the result of the action in a result variable for further use. Please note that when instantiating the Name value object, we are using the values we (pre-) defined in the Given method. Finally, in the three methods decorated with the [Then] attribute, we assert the outcome of the action. In our case, we make sure that all three properties of the Name value object have been populated correctly. Note that the names of the three methods used for assertion of the result should again be intention revealing. The name should reflect what we expect as the result of the assertion and, again, it should be written in English prose. Looking again at the tests, we have a Given […] When […] Then […] schema throughout all tests. Business analysts can very easily be trained to formulate the business requirements in this way.
Testing the mapping To find out whether the database schema is correct and an entity is mapped correctly to the underlying database schema, we have to execute several steps. The first step is to create an entity and populate it with precanned values, as shown in the following code snippet: var product = new Product { Name = "Apple", Description = "Description for apple", UnitPrice = 1.50m, ReorderLevel = 10, Discontinued = true, };
Then we have to try to save the product, as shown in the following code snippet: session.Save(product); session.Flush();
Note session.Flush() in the preceding code snippet. It is used to enforce the writing of all changes to the database, and in our case, to create a new Product record.
[ 189 ]
Testing, Profiling, Monitoring, and Logging
If the creation of a new database record is successful, then we'll want to know whether all the property values have been written to the database as defined. For this purpose, we have to load the product just written to the database, as shown in the following code snippet: session.Evict(product); var fromDb = session.Get(product.Id);
Note the session.Evict(product) statement in the preceding code snippet. We need this statement to eliminate the previously saved entity from the first level cache, and with that make sure that NHibernate does not just load the product from its first level cache, but really makes a round trip to the database to get the values. In the test assertion part, we first make sure that the product was really stored in the database. If it hadn't been stored, then NHibernate would return null when we try to load the entity, as shown in the following code snippet: Assert.That(fromDb, Is.Not.Null);
Now that we know the entity was stored and have successfully loaded it from the database, we have to compare the newly hydrated entity property-by-property with the original product entity, as shown in the following code snippet: Assert.That(fromDb.Name, Is.EqualTo(product.Name)); Assert.That(fromDb.Description, Is.EqualTo(product.Description)); Assert.That(fromDb.UnitPrice, Is.EqualTo(product.UnitPrice)); Assert.That(fromDb.ReorderLevel, Is.EqualTo(product.ReorderLevel)); Assert.That(fromDb.Discontinued, Is.EqualTo(product.Discontinued));
If this test passes, we can be sure that 1. The database schema for the Product entity is correct. 2. The mapping for the Product entity has been defined correctly. In more advanced scenarios, we would base our persistence-related test less on the "simple" property values checking and more on the "aggregate root behavior", which includes such things as checking all cascade actions executed, either implicitly by NHibernate or explicitly by the repository operating on the aggregate root.
As we move forward in the development of our application, this test remains very valuable because it serves as a safety net when we start to extend and refactor our system. By re-running this test every time we change the application, we can be sure that we didn't break the database schema and/or the mapping of the system. Although we have certainly recognized the great value of having tests, we still might feel a bit overwhelmed by the amount of code needed to test just a single entity. [ 190 ]
Chapter 7
Testing the mapping with Fluent NHibernate Once again, Fluent NHibernate provides us with a convenient way to test the mapping of an entity and its relations to the underlying database. Compared to the preceding example, we can reduce the amount of code to write to test a single entity by at least 50 percent. The Fluent NHibernate assembly contains a PersistenceSpecification class, which we can use to quickly define a test. The code needed to test the mapping for our Product entity would look similar to the following code snippet: new PersistenceSpecification(session) .CheckProperty(x => x.Name, "Apple") .CheckProperty(x => x.Description, "Description for apple") .CheckProperty(x => x.UnitPrice, 1.50m) .CheckProperty(x => x.ReorderLevel, 10) .CheckProperty(x => x.Discontinued, true) .VerifyTheMappings();
Here we assume that we have an open NHibernate session at hand, which we provide as a parameter to the constructor of the PersistenceSpecification class. Note that we provide the entity we want to test as a generic parameter (in our case, Product). For each property we want to test, we add a call to the CheckProperty method and pass a lambda expression, which describes the property as a first parameter. We then pass a precanned value as a second parameter. This precanned value will be assigned to the respective property during the test. To execute the test, we have to add a final call to the VerifyTheMappings method.
Time for action – Creating the base for testing In this exercise, we want to implement the base framework for our mapping tests.
1.
Open Visual Studio and open the OrderingSystem solution from Chapter 5.
2.
Add a second project to the solution. Choose Class Library as the project template and call the project OrderingSystem.Tests.
3.
To the OrderingSystem.Tests project, add references to the assemblies NHibernate.dll, NHibernate.ByteCode.Castle.dll, and FluentNHibernate.dll located in the lib folder.
4.
Make sure that the OrderingSystem project references the two assemblies NHibernate.dll and FluentNHibernate.dll. If not, please add these references now.
[ 191 ]
Testing, Profiling, Monitoring, and Logging
5.
Download NUnit from http://www.nunit.org/?p=download. Only download the binaries. At the time of writing, the recommended version is NUnit-2.5.10.11092.zip. Open this ZIP file and extract the file nunit. framework.dll into the lib folder.
6.
Add a reference to the assembly nunit.framework.dll of the OrderingSystem.Tests project.
7.
Add a reference to the OrderingSystem project of the OrderingSystem.Tests project.
8.
Add a class ThenAttribute to the project OrderingSystem.Tests. Make the class inherit from the TestAttribute class of NUnit, as shown in the following code snippet: public class ThenAttribute : TestAttribute { }
9.
Add a class SpecificationBase to the project OrderingSystem.Tests. Add the following code snippet to this class: public abstract class SpecificationBase { [TestFixtureSetUp] public void TestFixtureSetUp() { BeforeAllTests(); } [SetUp] public void SetUp() { Given(); When(); } [TearDown] public void TearDown() { CleanUp(); } [TestFixtureTearDown] public void TestFixtureTearDown() { AfterAllTests(); [ 192 ]
Chapter 7 } protected protected protected protected protected
virtual virtual virtual virtual virtual
void void void void void
BeforeAllTests(){ } Given(){ } When(){ } CleanUp(){ } AfterAllTests(){ }
}
10. Add another abstract class called MappingSpecificationBase, which inherits from the SpecificationBase class to the OrderingSystem.Tests project.
11. Add the following code snippet to the class: public abstract class MappingSpecificationBase : SpecificationBase { protected Configuration configuration; private ISessionFactory sessionFactory; protected ISession session; protected override void BeforeAllTests() { configuration = Fluently.Configure() .Database(DefineDatabase) .Mappings(DefineMappings) .BuildConfiguration(); CreateSchema(configuration); sessionFactory = configuration.BuildSessionFactory(); } protected ISession OpenSession() { return sessionFactory.OpenSession(); } protected override void Given() { base.Given(); session = OpenSession(); } protected abstract IPersistenceConfigurer DefineDatabase(); protected abstract void DefineMappings(MappingConfiguration m); protected virtual void CreateSchema(Configuration cfg){} } [ 193 ]
Testing, Profiling, Monitoring, and Logging
12. This class configures a NHibernate Configuration object. The configuration
object is then used to optionally create the database schema and to create the session factory, which in turn is used to create session objects. The details of the configuration must be specified by a child class, though this class only calls abstract or empty virtual methods.
13. Add a class called entity_mapping_spec to the OrderingSystem.Tests project.
14. Make this class inherit from MappingSpecificationBase and decorate it with a [TestFixture] attribute, as shown in the following code snippet:
[TestFixture] public class entity_mapping_spec : MappingSpecificationBase { }
15. Override the DefineDatabase method and define your local SQL Server 2008 Express to be the database used for our tests, as shown in the following code snippet:
protected override IPersistenceConfigurer DefineDatabase() { return MsSqlConfiguration.MsSql2008 .ConnectionString("server=.\\SQLEXPRESS;"+ "database=NH3BeginnersGuide;"+ "integrated security=SSPI;"); }
16. Override the DefineMappings method and add the following code, which defines
that we want to use fluent mappings and that all mapping classes are to be found in the assembly where the ProductMap class is implemented:
protected override void DefineMappings(MappingConfiguration m) { m.FluentMappings.AddFromAssemblyOf(); }
17. Override the method CreateSchema and tell the system to use the SchemaExport class of NHibernate to drop and recreate the schema in the database we use for testing, as shown in the following code snippet:
protected override void CreateSchema(Configuration cfg) { new SchemaExport(cfg).Execute(false, true, false); }
[ 194 ]
Chapter 7
18. Add a method to the class that tests the correctness of the mapping for the Product entity, as shown in the following code snippet:
[Then] public void it_should_correctly_map_a_product() { new PersistenceSpecification(session) .CheckProperty(x => x.Name, "Apple") .CheckProperty(x => x.Description, "Some description") .CheckProperty(x => x.UnitPrice, 1.50m) .CheckProperty(x => x.ReorderLevel, 10) .CheckProperty(x => x.Discontinued, true) .VerifyTheMappings(); }
19. Run the test and verify that it executes successfully, as shown in the following screenshot:
What just happened? We created a base (mini) framework, which allows us to write (unit) tests with little effort that verify the correctness of the mapping of the domain model to the underlying database schema. We have also implemented a test class which leverages this framework and tests the mapping of the Product entity.
Have a go hero Add code to the entity_mapping_specs class to test the mapping of the Employee class of the ordering system domain model. Use the WIKI of the Fluent NHibernate home page (http://wiki.fluentnhibernate.org/Persistence_specification_testing) to get more details about how to use the PersistenceSpecification class when, for example, dealing with properties of type value objects.
[ 195 ]
Testing, Profiling, Monitoring, and Logging
Time for action – Using SQLite in our tests In this short exercise, we will implement the necessary steps to be able to use SQLite in our database tests.
1.
Open Visual Studio and load the OrderingSystem solution.
2.
Add a folder called UsingSqLite to the OrderingSystem.Tests project.
3.
Add a reference to the assembly System.Data.SQLite.dll of the OrderingSystem.Tests project. The said assembly should be located in the lib sub-folder of your samples folder.
4.
Add a new class called entity_mapping_spec_for_sqlite to the preceding folder.
5.
Make the class inherit from the base class MappingSpecificationBase and decorate it with the TestFixture attribute, as shown in the following code snippet: [TestFixture] public class entity_mapping_spec_for_sqlite : MappingSpecificationBase { }
6.
Override the method DefineDatabase and add code to configure SQLite for inmemory operation. You would also require NHibernate to show the generated SQL: protected override IPersistenceConfigurer DefineDatabase() { return SQLiteConfiguration.Standard .InMemory() .ShowSql(); }
7.
Additionally, override the method DefineMappings, as shown in the following code snippet: protected override void DefineMappings(MappingConfiguration m) { m.FluentMappings.AddFromAssemblyOf(); }
[ 196 ]
Chapter 7
8.
Note that SQLite in the in-memory mode behaves a little bit different than any other database in that the schema is destroyed as soon as the NHibernate session is disposed, which was used to create the schema. Thus, we cannot use the same code as used in the preceding example to generate the database schema, but rather have to override the method Given and add the following code snippet: protected override void Given() { base.Given(); new SchemaExport(configuration) .Execute(false, true, false, session.Connection, null); }
9.
Note that we use a different overload of the Execute method where we can pass an ADO.NET connection object, which will be used to create the schema. The NHibernate session object is already created by the base class and we can use it to get the necessary connection object.
10. Add code to test the mapping of the Product entity, as shown in the following code snippet. Use the same code as in the preceding example:
[Then] public void it_should_correctly_map_a_product() { new PersistenceSpecification(session) .CheckProperty(x => x.Name, "Apple") .CheckProperty(x => x.Description, "Some description") .CheckProperty(x => x.UnitPrice, 1.50m) .CheckProperty(x => x.ReorderLevel, 10) .CheckProperty(x => x.Discontinued, true) .VerifyTheMappings(); }
11. Run the test and review the output in the test runner window, as shown in the following screenshot:
[ 197 ]
Testing, Profiling, Monitoring, and Logging
What just happened? We have extended our testing framework in such a way that we can use the SQLite database in the in-memory mode when executing our mapping tests. The main advantage of this approach is the speed with which the tests are executed. As the whole database resides in memory, any database-related operation is extremely fast.
Testing queries In Chapter 9, we will discuss various methods to learn how we can use NHibernate to query data from the database. One of the methods will be using the LINQ to NHibernate driver. NHibernate defines an extension method Query() to the ISession interface. With regard to testing, there is one caveat with this approach. As the Query method is only an extension method to, and not part of, the interface ISession, this method cannot be stubbed or mocked. Any other method of the interface, such as Get or Load, can be stubbed. A stub is an object that is used on behalf of another object and returns precanned data when queried. A mock is similar to a stub and is also used on behalf of another object, but a mock also contains instrumentation code, which is used to monitor the behavior of the caller. Stubs and mocks are used in unit or integration tests to simulate real objects that are hard or impossible to use in a test environment. Samples of such stubs or mocks are objects accessing a database or the network, hardware drivers or objects accessing the file system, and so on.
To overcome this limitation, we can define a Repository class, which is a wrapper around the NHibernate Session object. The Repository class implements the interface IRepository, which only contains the most important members of the ISession interface and, most importantly, the Query method, as shown in the following code snippet: public interface IRepository { IQueryable Query(); T Get(int id); T Load(int id); // possibly other members of ISession... }
The implementation of the Repository class is trivial as every method call is directly forwarded to the contained session object, as shown in the following code snippet: public class Repository : IRepository { private readonly ISession session;
[ 198 ]
Chapter 7 public Repository(ISession session) { this.session = session; } public IQueryable Query() { return session.Query(); } public T Get(int id) { return session.Get(id); } public T Load(int id) { return session.Load(id); } }
In our database access code, we will use the IRepository instead of the ISession. Imagine that we have to implement a class which queries the database and returns a list of all products that are still active and need to be reordered; that is, products that have the value UnitsOnStock less than the value ReorderLevel. The list of products returned by the query shall be ordered by product Name. To solve this problem, we can define a class similar to the following code snippet: public class GetAllProductsToOrderQueryHandler { private readonly IRepository repository; public GetAllProductsToOrderQueryHandler( IRepository repository) { this.repository = repository; } public IEnumerable Execute() { // implement query here } }
We get a repository object through constructor injection and can use it in the Execute method to query the database. The code in the Execute method will probably look similar to the following code snippet: return repository.Query() .Where(p => !p.Discontinued && p.UnitsOnStock < p.ReorderLevel) .OrderBy(p => p.Name) .ToArray();
We want to write a unit test which verifies that our code produces the expected result. In the following section, we'll see how this goal can be achieved. [ 199 ]
Testing, Profiling, Monitoring, and Logging
We have an object of the IRepository type that is used to get a list of Product entities from the database using LINQ to NHibernate, as shown in the following code snippet: var products = repository.Query() …
We can easily stub this repository object in a test and make the stub object return a pre-canned list of products. Let's create a stub for IRepository, which returns a well known set of products, as shown in the following code snippet: public class StubbedRepository : IRepository { public IQueryable Query() { return new[] { new Product {Name = "Pineapple", UnitPrice = 1.55m, ReorderLevel = 10, UnitsOnStock = 20, Discontinued = false}, new Product {Name = "Hazelnut", UnitPrice = 0.25m, ReorderLevel = 100, UnitsOnStock = 20, Discontinued = true}, new Product {Name = "Orange", UnitPrice = 1.15m, ReorderLevel = 20, UnitsOnStock = 10, Discontinued = false}, new Product {Name = "Apple", UnitPrice = 1.15m, ReorderLevel = 20, UnitsOnStock = 50, Discontinued = false}, } .AsQueryable(); } public Product Get(int id) { throw new NotImplementedException(); } public Product Load(int id) { throw new NotImplementedException(); } }
[ 200 ]
Chapter 7
Note that in the preceding class, we only implement the method that we want to stub. We can leave all other methods unimplemented. Additionally, note that we can convert any array of objects of type T to IQueryable by using the extension method AsQueryable of LINQ. We can then define a test fixture, as shown in the following code snippet: public class when_querying_products_to_reorder : SpecificationBase { private IRepository repository; private GetAllProductsToOrderQueryHandler sut; private IEnumerable result; protected override void Given() { repository = new StubbedRepository(); sut = new GetAllProductsToOrderQueryHandler(repository); } protected override void When() { result = sut.Execute(); } }
The name of the class is once again intention revealing and tells us what scenario we are going to test. In the method Given, we create a stubbed repository and an object of type GetAllProductsToOrderQueryHandler, to which we assign a variable called sut, where sut is an acronym for system under test. We inject the stubbed repository into the sut via constructor injection. In the When method, we execute the query and store the result in an instance variable result of type IEnumerable. The variable result can then be used in the
test methods to validate the outcome of the method call.
First, we want to make sure that the code in the Execute method only returns active products, as shown in the following code snippet: [Then] public void it_should_only_return_active_products() { Assert.That(result.Any(p => p.Discontinued), Is.False); }
[ 201 ]
Testing, Profiling, Monitoring, and Logging
Next, we make sure that the property value of ReorderLevel is greater that the property value of UnitsOnStock for all returned products, as shown in the following code snippet: [Then] public void it_should_only_return_products_to_reorder() { Assert.That(result.All(p => p.ReorderLevel > p.UnitsOnStock), Is.True); }
Finally, we want to test whether the list of returned products is ordered as expected, as shown in the following code snippet: [Then] public void it_should_return_an_ordered_list() { Assert.That(result.First().Name, Is.EqualTo("Apple")); Assert.That(result.Last().Name, Is.EqualTo("Orange")); }
In the preceding code snippet, it is shown how you can create a stubbed repository, which can then be used to test the logic of the query handler class. The stub returns a list of precanned products as IQueryable. This is exactly the same type as the LINQ to NHibernate Query extension method returns.
Logging According to the Apache project (http://www.apache.org), approximately four percent of all code written is for logging. This is a pretty significant number, especially if your application is of any real size. If we are going to write all of this code, then we might as well use a framework that will make it easier for us to configure what gets logged, where we log it, and how much of it gets logged.
Why do we need to log? While developing an application, we developers are used to running unit tests and using the debugger to make sure that our code runs smoothly and produces the expected results. However, once we hand over our application to the testers for quality assurance (QA), or once our product is installed in a productive environment, we do not have the possibility to use the debugger anymore to step through the code or to run unit tests to assert expected behavior and results.
[ 202 ]
Chapter 7
However, we all know that even a very well tested application can contain bugs, and thus crash or, even worse, produce wrong results. What can we do in such situations? How can we detect a code defect or a misconfiguration of the system afterwards? What would we need to do a "post-mortem analysis"? One of the possible answers is that we need information about which method call from the application caused the error. Additionally, we would like to have the full stack trace at the moment of the failure. It would be even better if we had a log of all the steps that happened before the error occurred. Last but not least, it would be very helpful to know the data that we operated on before and during the crash or malfunction of the system. If our system could produce such a log of information while it is running in a test environment or in production, then, in case of a malfunction of the system, we could ask the system administrator to provide us with a copy of the logged information and use this information to detect and fix the defect.
Logging with Log4Net Logging is a requirement of pretty much any application. It is a standard functionality of the system and a lot of frameworks exist that provide us with this functionality. We do not and should not implement our own logging framework, but rather use one of the existing ones. One of the best known and very mature logging frameworks for .NET is Log4Net (http:// logging.apache.org/log4net/index.html). This is an open source framework and is used by NHibernate. When we download NHibernate, Log4Net is included in the binaries.
Time for action – Adding logging to our application In this little exercise, we will create a sample application and configure it to use Log4Net as a logging framework and then create some basic logging messages.
1.
Open Visual Studio and create a new project. Select Console Application as the project template and call the project LoggingSample.
2.
Add a reference to the log4net.dll assembly, located in the lib folder of the project.
3.
In the Solution Explorer, right-click on the LoggingSample project and select Properties.
[ 203 ]
Testing, Profiling, Monitoring, and Logging
4.
In the project properties window, select the Application tab and set the Target framework to .NET Framework 4.
5.
In the Solution Explorer, right-click on the LoggingSample project and select Add | New Item…. Select Application Configuration File as the file template and click on OK. A file called App.config is added to your project. Note that this step is only necessary if the preceding step has not already added an App.config file to the project.
6.
In the configuration file, we add code to configure our application to use Log4Net. Start by adding a definition for the existence of a log4net section, as shown in the following code snippet:
7.
Next, we add the log4net section to the App.config and inside this section we define an appender. An appender is basically the target of the logging information produced by Log4Net. Various targets exist, such as files, output on the console, a database table, the registry, and so on. In our example, we want to add an appender that outputs the logging information to the console, as shown in the following code snippet: [ 204 ]
Chapter 7
8.
In the preceding code snippet, the tag conversionPattern describes how the log output should be formatted.
9.
Next, we add information to the App.config file to instruct Log4Net which appender it should use (we can define more than one appender!). To do this, we add the following code snippet to the node:
10. Lastly, we define what level of debugging we want to enable. This can be done by adding the following code snippet to the node:
11. In the preceding example, we define that the debug level is DEBUG. This means that all messages of all log levels are logged as Log4Net, starting with the configured level and including all messages with higher severity. The ranking is from lowest to highest: DEBUG, INFO, WARN, ERROR, and FATAL.
12. Open the Program class and add the XmlConfigurator attribute to the class file
(make sure the attribute is defined outside the namespace declaration), as shown in the following code snippet: [assembly: log4net.Config.XmlConfigurator(Watch = true)]
13. This attribute instructs Log4Net that it should watch for configuration information in the application configuration file.
If you are using Log4Net to log application runtime information and you do not see any output produced, then you might have forgotten to add the XmlConfigurator attribute to the application.
14. Add and initialize a static class level variable of type ILog to the Program class, as shown in the following code snippet:
private static readonly ILog log = LogManager.GetLogger(typeof(Program)); [ 205 ]
Testing, Profiling, Monitoring, and Logging
15. Use the logger to produce some logging messages of various log levels. You can do this by adding the following code to the Main method of the class: log.Debug("This is a Debug message."); log.Info("This is a Info message."); log.Warn("This is a Warn message."); log.Error("This is a Error message."); log.Fatal("This is a Fatal message.");
16. Add code to make the system wait for user input before exiting, as shown in the following code snippet:
Console.Write("\r\nHit enter to exit:"); Console.ReadLine();
17. Run the application and review its output. Your console should look similar to the following screenshot:
18. In App.config, change the logging level to WARN and run the application again. What do you see this time on the console?
What just happened? We have created a simple program which is configured to use Log4Net to log messages of different severity levels. We have defined appenders or targets to which Log4Net outputs the logging messages via the configuration file of the application.
Setting up logging for NHibernate As explained earlier in this chapter, NHibernate uses Log4Net to generate logging messages. Now that we have an understanding of the most relevant parts of Log4Net and how to configure them, we can use this know-how to set up the logging for NHibernate as needed by our application requirements.
[ 206 ]
Chapter 7
If we only want NHibernate to log the queries it sends to the data source when running unit tests, then we don't have to configure Log4Net at all. It suffices to add the show_sql key to the NHibernate configuration.
Time for action – Enable logging in NHibernate 1.
Open Visual Studio and load the OrderingSystem solution.
2.
To the project OrderingSystem.Tests, add a reference to the assembly log4net.dll located in the lib folder.
3.
Add an Application Configuration (App.config) file to the OrderingSystem.Tests project and add the following code to it:
4.
Open the entity_mapping_specs class and add the following code, which initializes logging for the unit tests, as shown in the following code snippet: protected override void BeforeAllTests() { base.BeforeAllTests(); log4net.Config.XmlConfigurator.Configure(); } [ 207 ]
Testing, Profiling, Monitoring, and Logging
5.
Run the tests and review the output in the test runner output window, as shown in the following screenshot:
If you don't see any output created by NHibernate via Log4Net in the test runner output window, then make sure no other listener is configured to consume the output generated by Log4Net. A sample for such a scenario could be that you have NHibernate Profiler (described later in this chapter) configured as a listener to your tests.
6.
This is an overwhelming amount of information and we would like to reduce it to avoid getting lost! NHibernate defines two loggers called NHibernate and NHibernate.SQL. We can individually configure these two loggers. Add the following code snippet to the App.config file, just before the tag:
7.
In the preceding code snippet, we have set the threshold level for the chattier logger NHibernate to WARN, that is, only messages of type WARN, ERROR, or FATAL will be the output. The other logger NHibernate.SQL is configured to give an output of all logging messages of any level. [ 208 ]
Chapter 7
8.
After this modification of App.config, run the tests again. Now the test output should look similar to the following screenshot:
9.
This is much less information! In fact, it is the same information we get when setting ShowSql to true in the NHibernate configuration.
What just happened? In our test project developed earlier in this chapter, we configured Log4Net to output all logging information to the console. Logging information, in this case, is created by NHibernate. As NHibernate produces a huge amount of logging information, we have restricted the type of logging information that is sent to the console by adding some filter information to the configuration file of the test project.
Monitoring and profiling Once our application is implemented and running in a test or production environment, we might want, or need, to monitor it. Several possibilities exist for how we can monitor the database communication of our application.
Analyzing log files
As we have seen in the Logging section of this chapter, NHibernate can produce a massive amount of logging data if configured accordingly. This data can be very useful to analyze what's going on under the hood. Furthermore, at least one commercial tool (NHibernate Profiler) is using the output generated by NHibernate to monitor and profile the database communication and provide a valuable insight into the usage of NHibernate.
[ 209 ]
Testing, Profiling, Monitoring, and Logging
Using SQL Server Profiler If you are using any commercial version of Microsoft SQL Server as your database, then you can use SQL Server Profiler to monitor the database communication produced by the application. The disadvantage of this method is that the profiler only hooks in at the database level and is not aware of NHibernate. As a consequence, we can only get a very limited insight into the usage of NHibernate through our application. The usage of the profiler makes it possible to find errors in SQL statements and helps to detect queries or query patterns that are sub-optimal. However, once a bad query pattern is found, it is rather hard to directly use this information to optimize the usage of NHibernate in this particular area.
Monitoring and profiling with NHibernate Profiler There is one commercial tool available that is strongly recommended to everyone. This tool is called NHibernate Profiler and has been written by one of the profound experts of and main contributors to NHibernate. NHibernate Profiler is a realtime visual debugger, allowing a development team to gain valuable insight and perspective into their usage of NHibernate. This profiler offers some unique benefits that no other product can currently offer:
Specifically built to log, monitor, and analyze the communication between the application and database through NHibernate.
All SQL statements are nicely formatted and presented, and can be easily copied and pasted in, for example, SQL Server Management Studio for further testing.
Analyzing access patterns and providing feedback in the form of warnings and tips on how to avoid common pitfalls when using NHibernate, and how to improve queries and commands based on best practices.
Provides a full-stack trace of each method causing database access and allows the developer to directly jump to the relevant source code.
Various reports with useful statistics about the database communication are available.
The database communication is logged and can be saved for later analysis and a replay of the scenario.
NHibernate Profiler can be downloaded from http://www.nhprof.com. A free one month license is available to use and test the product.
[ 210 ]
Chapter 7
Your application can easily be configured to enable profiling through NHibernate Profiler. Once the application is configured, we can then monitor and debug the database communication.
The preceding screenshot shows NHibernate Profiler in action. The profiler displays its information in four panes. The first upper left pane displays session-specific data. The second lower left pane displays statistical information regarding the session factory in use. The third upper right pane contains a list of all operations executed through the session(s), while the last lower right pane shows details of a selected statement. In this case, the nicely formatted SQL of a database insert command is shown.
Time for action – Adding NHibernate Profiler support In this exercise, we want to download, install, and use NHibernate Profiler to monitor the database communication caused by our unit test.
1.
Download NHibernate Profiler from http://www.nhprof.com/Download.
2.
Unpack the ZIP file (at the time of writing NHibernate.Profiler-Build-796. zip) into a new folder (for example, C:\tools\NHProfiler).
[ 211 ]
Testing, Profiling, Monitoring, and Logging
3.
Navigate to the installation folder and run NHibernate Profiler by double-clicking on NHProf.exe. You will be presented with a dialog, as shown in the following screenshot:
4.
Request a trial license. The license will be sent to you by e-mail.
5.
Create a new XML file in the application folder (for example, MyLicense.xml) and copy the license key from the e-mail and paste it into the XML file just created.
6.
Run NHibernate Profiler again and click on Browse for Your License. Locate and select the MyLicense.xml file in the NHibernate Profiler application folder.
7.
In the NHibernate Profiler application folder, locate the file HibernatingRhinos. Profiler.Appender.dll. Copy this file to the lib sub-folder of your samples
folder.
8.
Open Visual Studio and load the Ordering System solution.
9.
As shown in the following screenshot, add a reference to the assembly HibernatingRhinos.Profiler.Appender located in the lib folder to the OrderingSystem.Tests project.
10. Furthermore, add (if you haven't already done so) a reference to the log4net
assembly also located in the lib folder of the OrderingSystem.Tests project, as shown in the following screenshot:
[ 212 ]
Chapter 7
11. Open the class MappingSpecificationBase in the OrderingSystem.Tests project we implemented earlier in this chapter.
12. Add the following code snippet as the first line to the method BeforeAllTests to enable NHibernate Profiler to monitor the communication between our test code and the database:
HibernatingRhinos.Profiler.Appender.NHibernate .NHibernateProfiler.Initialize();
If you configure NHibernate Profiler as a listener to your application, then all the output generated by Log4Net will be redirected to NHibernate Profiler.
13. Open the class entity_mapping_spec and run the tests. 14. Switch to NHibernate Profiler and review the output captured by the profiler. 15. Locate the INSERT INTO [Product]… statement and select it. In the lower part of the screen, review the output on the Details tab, as shown in the following screenshot:
[ 213 ]
Testing, Profiling, Monitoring, and Logging
16. Note the red bullets in the Alerts column in the upper right side. In the lower pane, switch to the Alerts tab and review the error message, as shown in the following screenshot:
17. The error message tells us that we have an anti-pattern in our code; we do not use
explicit transactions to wrap our database access. The read more hyperlink provides a more thorough description of the alert, as shown in the preceding screenshot. Click on this hyperlink and review the text.
18. In the upper right pane of NHibernate Profiler, locate the SELECT … FROM
[Product]… entry and select it. Note the number in the Row Count column and the times in the Duration column. In our case: 1. One record was returned as a result of the query. 2. The query took 2 ms on the database only and 48 ms overall.
What just happened? We have downloaded and installed a trial license of NHibernate Profiler. Subsequently, we have configured our application to create logging information that can be used by the profiler to analyze our database communication. After this initial setup, we have run some of our tests and reviewed the output they generated in NHibernate Profiler. NHibernate Profiler provided us with detailed information about what happened in which sequence, how long it took to execute certain queries, and how many records were returned by a given query.
[ 214 ]
Chapter 7
Summary In this chapter, we learned why it is important to write tests to verify the correct mapping of the domain model to the underlying database schema. We also implemented a framework which helps us to write mapping tests with as little effort as possible. We also discussed the value of logging and how we can configure our application to produce logging information. Lastly, we discussed how we can monitor and profile our application during runtime. We specifically put our focus on the monitoring of the database communication of our application via NHibernate. After this deep dive into testing and profiling, we are ready to tackle yet another important aspect of NHibernate, namely its configuration. This is the topic of the next chapter.
[ 215 ]
8
Configuration Any complex framework that is used by many developers in very different scenarios must be configurable. With the configuration, we can influence the runtime behavior of our application that uses the framework.
In this chapter, we will analyze in detail what a configuration is. Specifically, we will:
Discuss why we need a configuration to begin with
Be presented with a list of the elements of NHibernate we can configure
Learn four different ways of how we can configure NHibernate in our applications
This chapter contains a lot of exercises that show us one-to-one how easily NHibernate can be configured. So let's get on with it...
Why do we need a configuration? The runtime behavior of NHibernate depends on how we configure the system. NHibernate has been built with the idea of being very flexible and extendable. NHibernate can be used in all kinds of different scenarios. As an example, it is possible to use this framework with nearly any known and relevant relational database management system (RDBMS). Some of those supported databases are MS SQL Server, Oracle, IBM DB/2, and MySQL, to name just a few. It is also possible to use NHibernate in brand new projects where we can choose a modelfirst approach, as well as in scenarios where we have to write an application on top of a legacy database.
Configuration
Sometimes, we have special needs for our application. Maybe we need to add auditing capabilities to our application or maybe we want to use our own, and very specific, proxy generator. Such a flexible and extendable system can only work if it is configurable. Therefore, what NHibernate expects from us is that we provide enough information for it at startup. Luckily, most of the settings have meaningful default values such that we only need to explicitly configure a few settings.
Elements of the configuration All important aspects of the runtime behavior of NHibernate are configurable by one of the methods described in detail later on in this chapter.
Which database do we want to use? This is probably the single most important aspect of our configuration. We have to tell NHibernate what database product we are going to use, such as SQL Server, Oracle, DB/2, MySQL, and so on. As there is no such thing as a "default database", we always have to provide these details in our configuration. Besides the diversity of database products, each database product is also available in different versions, such as SQL Server 2000, 2005, or 2008, or Oracle 9i, 10i, or 11g, and so on. Furthermore, we need to authenticate ourselves when accessing a database. Therefore, we need to provide the respective security tokens in the form of a connection string. Typically, we will find these four settings in a real-world application: NHibernate.Connection.DriverConnectionProvider NHibernate.Driver.SqlClientDriver NHibernate.Dialect.MsSql2008Dialect server=.\SQLEXPRESS;database=sample;integrated security=SSPI;
The preceding code snippet is part of an XML configuration file. [ 218 ]
Chapter 8
The first line defines the connection provider NHibernate uses. Most of the time, we will want to use the default provider that NHibernate provides us with, but we can also define our own provider. In this case, we would have to declare this in exactly this configuration entry. Whenever we want to use our own implementation for a specific part of NHibernate, we have to declare this in the configuration. In this case, we have to provide the fully qualified class name combined with the assembly name in which the class is implemented. As an example, SomeNamespace.MyDriverProvider, MyAssembly would be the entry needed if we want to declare the usage of a class implemented in MyAssembly in the namespace SomeNamespace and with the class name MyDriverProvider.
The second property defines the database we want to use by specifying the database driver. In our case, this is the driver for MS SQL Server. In the third entry, we define which SQL dialect we want to use. In our example, it is any of the MS SQL Server 2008 editions. The SQL dialect specifies things such as what types the database supports or how .NET types are mapped to the corresponding database types. Furthermore, the dialect defines which the native POID generator is or how a paginated query should look. NHibernate needs these dialects as SQL is not standardized enough for all databases to be compatible among each other.
Finally, in the fourth entry, we provide the connection information in the form of a connection string to NHibernate. NHibernate will use this connection string when trying to open a session with the database. In our example, we define that we want to use a locally installed SQL Server Express Edition and we want to access a database called sample. We further declare that we want to use integrated security for authentication with the database. Note that starting from NHibernate 3.2 (which at the time of writing had just reached general availability, or GA), the configuration has been streamlined and we need to configure less elements as NHibernate is using meaningful default values.
[ 219 ]
Configuration
The following elements can thus be omitted: NHibernate now implements its own proxy generator. No additional external dependency is needed, and thus the configuration does not need to define the proxy factory factory element.
The DriverConnectionProvider class is the default connection provider, and thus no explicit configuration is needed.
Each dialect has associated a default driver, and again we can omit this declaration.
What byte code provider and proxy factory? When using lazy loading with NHibernate, we need to define which proxy generator NHibernate shall use. NHibernate currently natively supports three proxy generators. These three generators are part of the Castle (http://www.castleproject.org/), LinFu (http://code.google.com/p/linfu/), or Spring.NET (http://www.springframework. net/) framework. It doesn't really matter which provider you choose. You will probably want to use the proxy generator of the framework you already use. If you haven't used any of those frameworks so far, then selecting the Castle provider is a good choice. If you are using the Castle proxy generator, then the definition in the XML configuration file will look similar to the following code snippet: NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
The byte code provider is responsible for creating some optimizations of the code used by NHibernate. Among others, it allows us to inject external dependencies into entities that are constructed by NHibernate. These are all advanced scenarios and usually you do not want to change the default settings there. Note that starting from version 3.2, NHibernate has its own embedded proxy generator which is a modified version of the LinFu proxy generator. As a consequence, when using NHibernate 3.2 or higher, we don't need to reference any of the NHibernate.ByteCode. xxx.dll.
[ 220 ]
Chapter 8
Where are our mappings? To be able to work, NHibernate has to know how we define our mappings and where the mapping information can be found. In Chapter 5, we discussed the various ways to define a mapping between the domain model and the underlying database schema. Independent of how we define the mappings, we always have to provide these mapping definitions to NHibernate via the configuration. We can add individual mapping files or resources as well as whole assemblies which contain mappings. Using configuration in code, this declaration would look similar to the following code snippet to add a single type: configuration.AddClass(typeof (Product));
and like the following code snippet to make NHibernate parse a whole assembly of HBM files that are stored as embedded resources: configuration.AddAssembly(typeof (Product).Assembly);
Do we use second level caching? Sometimes, it makes sense to use a second layer of caching besides the first level cache (or identity map) that every NHibernate session provides. In this case, we can configure a second level cache, which is not tied to a specific session but globally available to all sessions of a given session factory. When configuring the second level cache, we have to declare among others which cache provider we want to use.
Do we want to extend NHibernate? As mentioned earlier, NHibernate is a very flexible and highly extensible framework. As such, we can, for example, define custom interceptors or listeners that add functionality to NHibernate. These custom classes and their usage have to be declared to NHibernate when configuring it at startup. Note, however, that this is an advanced topic and lies outside of the focus of this book.
XML configuration The original way of configuring NHibernate is through XML. There are two ways we can use to define the configuration. It can either be part of the application configuration file for a classical Windows application or the Web.config file for a web-based application. We can alternatively define the configuration in a separate XML file.
[ 221 ]
Configuration
Time for action – Configuring NHibernate using XML In this exercise, we are going to implement a simple application which is using NHibernate to persist data to and read data from a relational database. As a database, we will use SQLite in file mode.
1.
Open Visual Studio and create a new project. Select Console Application as the project template. Call the project XmlConfigurationSample.
2.
Due to some incompatibility issues of SQLite, we have to adjust the target framework of our application. In the Solution Explorer, right-click on the XmlConfigurationSample project and select Properties.
3.
On the Application tab, select .NET Framework 3.5 as Target framework:
4.
Switch to the Build tab and make sure that the Platform Target is set to Any CPU.
5.
Add a reference to the three assemblies NHibernate.dll, NHibernate. ByteCode.Castle.dll, and System.Data.SQLite.dll to the project, as shown in the following screenshot:
[ 222 ]
Chapter 8
6.
In the Solution Explorer, right-click on the solution, select Add | New Folder, and name the folder Schema.
7.
Right-click on the Schema solution folder and select Add | Existing Item. Add the two files nhibernate-configuration.xsd and nhibernate-mapping.xsd from the lib folder to this Schema folder.
8.
Add a new class file called Account.cs to the project. Add the following code snippet to the file to define the entity Account: public class Account { public int Id { get; set; } public string Name { get; set; } public decimal Balance { get; set; } public string CurrencyCode { get; set; } public bool IsActive { get; set; } }
9.
Add an XML file called Account.hbm.xls to the project and add the following XML code to the file to define the mapping for the Account entity:
10. Set the Build Action property of the file to Embedded Resource. 11. Add a new item of type Application Configuration File to you project. Visual Studio adds a file named App.config to the project.
[ 223 ]
Configuration
Note that, if in step 3 you have changed the target platform of the application to .NET Framework 3.5, then Visual Studio automatically adds an App. config file to your solution. If you open this file it contains a node . In this case, you don't need to execute step 10 and can just delete the content of the App.config and continue with step 11.
12. To this file, add the following XML code snippet to define a section where you will define the configuration for NHibernate:
13. After the configSections definition, add the following XML code snippet to configure NHibernate:
NHibernate.Connection.DriverConnectionProvider NHibernate.Driver.SQLite20Driver NHibernate.Dialect.SQLiteDialect NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle Sample true
[ 224 ]
Chapter 8
14. After the preceding code snippet, add a definition for the connection string to be
used by the application (which is referenced by the connection.connection_
string_name property above):
15. To the Program class, add the following using statements: using NHibernate.Cfg; using NHibernate.Tool.hbm2ddl;
16. In the Main method of the Program class, add a line to create an instance of the NHibernate Configuration class, as shown in the following code snippet: var configuration = new Configuration();
17. After the preceding statement, add code to make NHibernate scan the whole
assembly where Account is defined for mapping files, as shown in the following code snippet: configuration.AddAssembly(typeof(Account).Assembly);
18. In the Program class, implement a static method BuildSchema which uses
NHibernate's SchemaExport class to (re) create the database schema, as shown in the following code snippet: private static void BuildSchema(Configuration configuration) { new SchemaExport(configuration).Execute(true, true, false); }
19. Back in the Main method, add a line which calls this method and passes the configuration instance as a parameter: BuildSchema(configuration);
20. Add a line of code to build a session factory from the NHibernate configuration object, as shown in the following code snippet:
var factory = configuration.BuildSessionFactory();
[ 225 ]
Configuration
21. Add code to open a session. Do this in a using statement, as shown in the following code snippet:
using (var session = factory.OpenSession()) { }
22. Inside the using statement, add code to create an Account entity and use the session to save it, as shown in the following code snippet: var account = new Account { Name = "USB-10234-R1", Balance = 1545.55m, CurrencyCode = "CHF", IsActive = true }; session.Save(account);
23. Flush and clear the session, as shown in the following code snippet: session.Flush(); session.Clear();
24. Add code to reload the just stored account from the database, as shown in the following code snippet:
var fromDb = session.Get(account.Id);
25. At the end of the Main method, add code which asks the user to hit Enter to exit the application, as shown in the following code snippet:
System.Console.Write("\r\n\nHit enter to exit:"); System.Console.ReadLine();
26. Run the application and review the output on the console. Your console should look similar to the following screenshot:
[ 226 ]
Chapter 8
What just happened? We have created a simple application that uses a SQLite database to store the data. We have defined the configuration of NHibernate in XML as part of the application resource file.
Configuring NHibernate in code We do not need to provide the configuration in XML, but we can define the whole configuration in code.
Time for action – Configuring NHibernate in code In this exercise, we are going to implement a simple application using NHibernate which fully configures the ORM framework in code.
1.
Open SQL Server Management Studio (SSMS) and create a new database called CodeConfigurationSample.
2.
Open Visual Studio and create a new project. Select Console Application as the project template. Call the project CodeConfigurationSample. [ 227 ]
Configuration
3.
Add a reference to the two assemblies NHibernate.dll and NHibernate. ByteCode.Castle.dll to the project.
4.
In the Solution Explorer, right-click on the solution and select Add | New Folder and name the folder Schema.
5.
Right-click on the Schema solution folder and select Add | Existing Item. Add the two files, nhibernate-configuration.xsd and nhibernate-mapping.xsd, from the lib folder to this Schema folder, as shown in the following screenshot:
6.
Add a new class file called Product.cs to the project and add the following code snippet to define a basic Product entity: public class Product { public int Id { get; set; } public string Name { get; set; } public decimal UnitPrice { get; set; } public int ReorderLevel { get; set; } public int UnitsOnStock { get; set; } public bool Discontinued { get; set; } }
7.
Add a new XML file to the project. Call the file Product.hbm.xml.
8.
Set the Build Action property of the file to Embedded Resource.
9.
Add the following XML code snippet to the file to define the mapping for the Product entity:
10. Add the following using statements to the Program class: using NHibernate.Cfg; using NHibernate.Tool.hbm2ddl;
11. Add a static method GetConfiguration to the Program class, as shown in the following code snippet:
private static Configuration GetConfiguration() { }
12. In the method body, create an instance of the type Configuration, as shown in the following code snippet:
var cfg = new Configuration();
13. With the aid of the Add method, add a key-value pair to the Properties collection of the NHibernate configuration instance that defines which driver connection provider NHibernate should use, as shown in the following code snippet:
cfg.Properties.Add(Environment.ConnectionProvider, typeof(DriverConnectionProvider).FullName);
Instead of typing in a magic string connection.provider for the key and NHibernate.Connection.DriverConnectionProvider for the value, we can use a more type-safe way of defining the key-value pair. All possible configuration keys are available via the static NHibernate. Cfg.Environment class. For the value, we can use the typeof(T) function, where T is the class whose name we want to use as the value. The desired value can then be obtained via the FullName property.
[ 229 ]
Configuration
14. Add more key-value pairs to the Properties collection to define the connection driver and the dialect, as shown in the following code snippet: cfg.Properties.Add(Environment.ConnectionDriver, typeof(SqlClientDriver).FullName); cfg.Properties.Add(Environment.Dialect, typeof(MsSql2008Dialect).FullName);
15. We also need to define the proxy factory factory class (yes, this is not a typo;
it is really a factory of a factory). However, as the desired class does not live in the NHibernate assembly but in the NHibernate.ByteCode.Castle assembly, we cannot use the FullName this time, but have to use the property AssemblyQualifiedName, as shown in the following code snippet: cfg.Properties.Add(Environment.ProxyFactoryFactoryClass, typeof(ProxyFactoryFactory).AssemblyQualifiedName);
16. Now, we need another key-value pair to define the connection string to our database, as shown in the following code snippet:
cfg.Properties.Add(Environment.ConnectionString, @"server=.\SQLEXPRESS;database= CodeConfigurationSample;" + "integrated security=SSPI;");
17. Finally, we want NHibernate to log the SQL sent to the database, as shown in the following code snippet:
cfg.Properties.Add(Environment.ShowSql, "true");
18. We now need to declare which mappings we want to use. We can either do this by individually adding the entity types to the configuration, as shown in the following code snippet: cfg.AddClass(typeof (Product));
19. Furthermore, even better by adding the assembly which contains all XML
configuration files to the NHibernate configuration object, as shown in the following code snippet: cfg.AddAssembly(typeof (Product).Assembly);
20. Choose one or the other from the preceding two variants. 21. Return the configuration object from this function, as shown in the following code snippet:
return cfg;
[ 230 ]
Chapter 8
22. Add another static method to the Program class that (re) creates the database schema, given the configuration, as shown in the following code snippet:
private static void BuildSchema(Configuration configuration) { new SchemaExport(configuration).Execute(true, true, false); }
23. To the Main method of the Program class, add the following code snippet to
create a configuration object, (re) create the database schema, and finally build a session factory: var configuration = GetConfiguration(); BuildSchema(configuration); var factory = configuration.BuildSessionFactory();
24. Add code to open a session, as shown in the following code snippet: using(var session = factory.OpenSession()) { }
25. Inside the using statement, create a product entity and use the session to save the product to the database, as shown in the following code snippet:
var product = new Product { Name = "Apple", UnitPrice = 1.55m, ReorderLevel = 10, UnitsOnStock = 5 }; session.Save(product);
26. Additionally, inside the using statement, flush the session and clear the first level cache, as shown in the following code snippet:
session.Flush(); session.Clear();
27. Reload the product from the database using the session object and the ID of the previously saved product, as shown in the following code snippet:
var fromDb = session.Get(product.Id);
28. At the end of the Main method, add code to wait for the user to hit Enter before exiting the application, as shown in the following code snippet:
System.Console.Write("\r\n\nHit enter to exit:"); System.Console.ReadLine(); [ 231 ]
Configuration
29. Run the application and review the output in the console. It should look similar to the following screenshot:
30. There is one caveat in our code though. Usually, you would NOT want to define the connection string to the database in code, but rather in the configuration file of your application. To do so, add a new item of the type Application Configuration to you project.
31. Add the following XML code snippet to define the database connection: [ 232 ]
Chapter 8
32. In the GetConfiguration method, replace the statement which declares the connection string with the following code snippet:
cfg.Properties.Add(Environment.ConnectionStringName, "Sample");
33. Start the application again. The application should run as before, but now with the connection string defined outside the code in the configuration file.
What just happened? We have created an application using NHibernate to persist data to and query data from a database. We have configured NHibernate entirely in code with the exception of the connection string, which we defined in the configuration file of the application. In my personal career, it has been proven that the less external configuration any application needs, the easier it is to manage and support. In this regard, I personally prefer to configure as much as possible in code and only rely on external configuration where it absolutely makes sense.
Fluent configuration Personally, I prefer to configure NHibernate in code by using a fluent API. It makes the configuration very readable and self-expressing. Also, when defining the configuration, the individual settings are much more discoverable to the developer executing this task.
Configuring NHibernate with Loquacious NHibernate contains a new fluent API for configuration. This API is defined in the Loquacious namespace. All aspects of the configuration can be defined by using this API. The configuration in the preceding sample was already very readable, but the usage of the new fluent API will make it even more understandable.
Time for action – Using Loquacious to configure NHibernate As always, we do not want to talk too much about the theory, but immediately dive into an example. In this example, we use some of the concepts we discussed in earlier chapters, such as the usage of value objects and of the NHibernate Profiler tool.
1.
Open Visual Studio and create a new project of type Console Application. Name the project LoquaciousConfigurationSample.
[ 233 ]
Configuration
2.
As we again want to use SQLite in this example, we have to overcome some of its limitations and adjust our project settings accordingly. Therefore, in the Solution Explorer, right-click on the project and click on Properties.
3.
On the Application tab, select .NET Framework 3.5 as Target framework.
4.
On the Build tab, make sure that Platform target is set to Any CPU.
5.
Add a solution folder named Schema to the solution.
6.
In the Solution Explorer, right-click on the Schema folder and select Add | Existing Item…. Browse to the lib folder, select the nhibernate-mapping.xsd file, and click on OK to add this file to the Schema folder.
7.
In the Solution Explorer, right-click on the References folder of the LoquatiousConfigurationSample project and select Add Reference….
8.
Navigate to the lib folder and select the following four files: 1. NHibernate.dll 2. NHibernate.ByteCode.Castle.dll 3. HibernatingRhinos.Profiler.Appender.dll 4. System.Data.SQLite.dll
9.
Again, in the Solution Explorer, right-click on the project and select Add | New Item…. Select Application Configuration File as the template and click on Add. A file called App.config is added to your project. [ 234 ]
Chapter 8
10. Open the App.config file and add a definition for the connection string we are
going to use in this example. SQLite in file mode will be our database. Thus, the content of the App.config file should look similar to the following code snippet:
11. In this example, we want to define a Person entity which (among others) has a Name property which is a value type. Add a new class file called Person.cs
to the project.
12. Add the following code snippet to the file to define the entity: using System; namespace LoquatiousConfigurationSample { public class Person { public Guid Id { get; set; } public Name Name { get; set; } public string SSN { get; set; } public DateTime Birthdate { get; set; } } }
13. Note that this time we use an Id of type Guid—which is our primary key—and not, as in preceding examples, of type int.
14. Add another new class file, Name.cs, to the project. 15. To define the Name value type, use the code shown in the following code snippet:
namespace LoquatiousConfigurationSample { public class Name { public string FirstName { get; set; } public string LastName { get; set; } public string MiddleName { get; set; } } } [ 235 ]
Configuration
16. A value type always needs to implement equality based on the content of all its
properties, as discussed in Chapter 3. Thus, add the following code snippet to the Name class to define this equality by overriding Equals and GetHashCode: public bool Equals(Name other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Equals(other.FirstName, FirstName) && Equals(other.LastName, LastName) && Equals(other.MiddleName, MiddleName); } public override bool Equals(object obj) { if (obj.GetType() != typeof (Name)) return false; return Equals((Name) obj); } public override int GetHashCode() { unchecked { int result = (FirstName != null ? FirstName.GetHashCode() : 0); result = (result*397) ^ (LastName != null ? LastName.GetHashCode() : 0); result = (result*397) ^ (MiddleName != null ? MiddleName.GetHashCode() : 0); return result; } }
17. To define the mapping, add a new file of type XML to the project. Call the file Person.hbm.xml.
18. In the Solution Explorer, select the file and make sure that in the Properties window, the Build Action is set to Embedded Resource.
19. Add the following XML code snippet to define the mapping of the Person entity: [ 236 ]
Chapter 8
20. In the preceding XML definition, specifically note the usage of the ID generator
class guid.comb which instructs NHibernate to generate Ids of type Guid that are optimized for the usage in relational databases.
21. Furthermore, note the usage of the component tag to define the value type Name. 22. We want to use the NHibernate Profiler application to monitor the database
communication of our application through NHibernate. Thus, add code to our application to support the profiler. To the first line of the Program class in the Main method, add the following code snippet: HibernatingRhinos.Profiler.Appender.NHibernate .NHibernateProfiler.Initialize();
23. Now, finally, we will use the fluent API located in the NHibernate.Loquacious
namespace to configure NHibernate. Add a static method GetConfiguration to the Program class. The method contains the configuration code, as shown in the following code snippet: private static Configuration GetConfiguration() { var cfg = new Configuration(); cfg.SessionFactory() .Proxy .Through() .Mapping .UsingDefaultCatalog("sampleCatalog") .UsingDefaultSchema("dbo") .Integrate .LogSqlInConsole() .Using() .Connected .Through() .By() .ByAppConfing("Sample"); cfg.AddAssembly(typeof(Person).Assembly); return cfg; }
[ 237 ]
Configuration
24. In the preceding code, we first create a new instance of type NHibernate.Cfg.
Configuration. Then we use the extension method SessionFactory() to get
access to the fluent configuration API. Next, we define what proxy factory factory NHibernate shall use. Then we define what the default catalog should be and the schema that NHibernate should use. With the Integrate keyword, we start the declaration of the database driver and dialect we are going to use, as well as the connection string we have defined in the App.config file. Another maybe even more readable, and thus a preferable, way of configuring NHibernate is through lambda functions. The readability of the configuration through the Loquacious fluent API highly depends on the developer indenting the various statements correctly. So let's rewrite the preceding configuration by using lambdas: var cfg = new Configuration(); cfg.Proxy(p => { p.ProxyFactoryFactory(); }); cfg.Mappings(m => { m.DefaultCatalog = "NH3BeginnersGuide"; m.DefaultSchema = "dbo"; }); cfg.DataBaseIntegration(db => { db.ConnectionProvider(); db.Driver(); db.Dialect(); db.ConnectionStringName = "Sample2"; db.LogSqlInConsole = true; });
In the preceding code snippet, we have configured NHibernate to access the NH3BeginnersGuide database on a SQL Server 2008.
[ 238 ]
Chapter 8
25. We will now add code to use for the recreation of the database schema, as shown in the following code snippet:
private static void BuildSchema(Configuration configuration) { new SchemaExport(configuration).Execute(true, true, false); }
26. Having all the pieces together, we can now use them to recreate the database
schema and build a session factory. Add the following code snippet to the Main method in the Program class: var configuration = GetConfiguration(); BuildSchema(configuration); var factory = configuration.BuildSessionFactory();
27. Open a new session in a using statement, as shown in the following code snippet: using (var session = factory.OpenSession()) { }
28. In the body of the preceding using statement, define a person object and store it in the database:
var person = new Person { Name = new Name { LastName = "Doe", FirstName = "John", MiddleName = "A." }, Birthdate = new DateTime(1977, 1, 6), SSN = "111-22-3333" }; session.Save(person);
29. Moreover, inside the body of the using block, flush the session, clear the first
level cache, and reload the just saved person from the database, as shown in the following code snippet:
session.Flush(); session.Clear(); var fromDb = session.Get(person.Id);
[ 239 ]
Configuration
30. At the end of the Main method, add code to ask the user to press Enter to terminate the application, as shown in the following code snippet: Console.Write("\r\n\nHit enter to exit:"); Console.ReadLine();
31. Start the NHibernate Profiler. 32. Start your application and monitor its output in NHibernate Profiler. You should see something similar to the following screenshot:
33. We see that two sessions were used. The first one was used to recreate the
schema, while the second one was used by our code to save and reload the person object. Let's take a closer look at the table generation script, as shown in the following screenshot:
[ 240 ]
Chapter 8
34. We can see that in the case of the SQLite database, the default catalog and default schema were used as prefixes in the naming of the table. In any other database product, this information would be handled differently.
35. Let's have a look at the INSERT statement, as shown in the following screenshot:
36. And finally, the SELECT statement, as shown in the following screenshot:
[ 241 ]
Configuration
What just happened? In the preceding exercise, we used the fluent API provided in the Loquacious namespace to configure NHibernate. This API allows us to configure the system in a type-safe and discoverable manner. We have used two slightly different approaches to define the configuration, one that uses a fully-fluent style and the other that uses a mixture of fluent API and lambda expressions for an even improved readability. We have used XML documents to map our (simple) domain, we have also used NHibernate Profiler to monitor the output of the application. The application writes and reads entities of type product to and from the database.
Pop quiz 1. Prior to using NHibernate in our applications, we have to configure this framework. Which of the following are elements that are mandatory in our configuration? a. Second level cache. b. Connection string. c. SQL dialect. d. ADO.NET batch size. e. Default schema.
Configuring NHibernate with Fluent NHibernate Throughout the examples in this book, we have used the fluent API of Fluent NHibernate to configure NHibernate. Thus, let's keep this section rather short and there will also be no example included. Suffice to say that any aspect of NHibernate can be configured in a very user-friendly and discoverable way when using this interface. There is a sample solution in the code that accompanies this book. The sample solution is called FNHConfigurationSample.sln. Let's take a look at this sample and experiment with it.
Have a go hero Take a copy of the preceding exercise and convert it to use Fluent NHibernate to configure NHibernate and map the domain to the underlying database.
[ 242 ]
Chapter 8
Convention over configuration As we have seen, nearly every aspect of NHibernate can be configured, and thus influences the corresponding runtime behavior. With a lot of configuration possibilities and flexibility comes a lot of responsibility. We have to be aware of the fact that with the wrong configuration settings in place, we can severely impact the performance and general behavior of the system. Having to explicitly define each and every possible configuration setting of a complex framework such as NHibernate can be a tedious and error-prone task. Even if we are very careful and do not create any wrong settings, then it is at least a boring task to do. Using conventions instead gives us more freedom to concentrate on the important elements of the configuration. As for (nearly) each possible setting, there are meaningful defaults defined. Thus, we only need to explicitly define the exceptions or deviations from the defaults. It has been proven in real life that complex exception-based systems are much more manageable than systems where everything has to be configured explicitly.
Summary In this chapter, we have been diving deep into the details of why and how to configure NHibernate. Specifically, we covered:
How to configure NHibernate when using XML configuration files
Configuring every aspect of NHibernate in code
Using the new fluent API of NHibernate to configure the system
We also briefly discussed the configuration via Fluent NHibernate and the difference between configuration and convention, and why we should prefer the usage of conventions when configuring our system. Now that we've learned about configuration, we're ready to send queries to the database – which is the topic of the next chapter.
[ 243 ]
9
Writing Queries Nearly every application collects data, loads of data, and stores it in some database. In the end, users want to get some valuable information out of the data. That's why reports are always the most important part an application provides to the end user. To get those reports on-screen or on paper, we first query the data from the database. Consequently, querying the database can be considered the backbone of our solution.
In this chapter, we shall:
Learn how to use LINQ (Language Integrated Queries) to NHibernate to retrieve data from the database
Query the database using the criteria query API
Use the original object-oriented SQL dialect called Hibernate Query Language (HQL)
Discuss entities with properties that can be lazy loaded
Confront eager loading with lazy loading, as well as looking at how to batch queries
So, let's not lose any time and dive into the topic.
Writing Queries
How can we get to our data? When using NHibernate to persist and retrieve data to and from the database, we have various means available to query the data we previously stored in the database. Originally, there was only one kid on the block called HQL. HQL is similar to SQL used by most of the relational databases to query data. However, HQL is more object-oriented and, when writing HQL queries, we do not reference database tables or views. Rather, we reference the entities defined in our domain model. HQL is still a valuable way to query data and is probably the most complete of all APIs. It's reasonable to state that if you can't solve a query problem using HQL, you probably can't solve it using any of the other APIs. Later on, NHibernate introduced a second API to query data from the database. This API was called the criteria query API. Queries are created in a modular way by defining criteria and combining them. Originally, the definition of such criteria elements needed the use of strings to, for example, reference properties of an entity. With the introduction of NHibernate 3, we have a new and more type-safe variant of the criteria query API. This API is also called QueryOver. NHibernate has offered a third API for some time now. This third API was a LINQ provider for NHibernate. In NHibernate 2.x, LINQ to NHibernate was not part of the core NHibernate and was only available as a contribution project. This LINQ provider was created on top of the Criteria API. Furthermore, the LINQ provider was very incomplete; complex queries were not supported. For the release of NHibernate 3, the LINQ provider has been completely rewritten and is now part of the core NHibernate framework. This new provider is built on top of HQL. The LINQ to NHibernate provider nowadays is the preferred way to query data and can be used to write nearly all queries. Only in very specific and possibly convoluted scenarios may we want to use HQL. Finally, let's not forget, there is a fourth way to define queries in NHibernate. We can write queries by using native SQL, though this is a double-edged sword. On the positive side, you can leverage all features of your database product; on the negative side you tightly bind yourself to a specific database product and cannot easily switch to another database, say from Oracle to MS SQL Server or MySQL, and vice versa. Note that even if you write your queries in native SQL, the result of the query is an entity or a list of entities and not an ADO.NET recordset. Please also note that most of the code examples discussed in this chapter can be found in the QuerySamples solution accompanying this book.
[ 246 ]
Chapter 9
The LINQ to NHibernate provider The introduction of LINQ in .NET 3.5 has changed the way developers deal with sets of data. Where in the times prior to LINQ, the foreach loop has ruled the game when dealing with sets or lists of data, it is now LINQ that is the king of the game. LINQ to objects is centered on the interface IEnumerable and manipulates sets of objects that live in-memory. LINQ to NHibernate, on the other hand, is a LINQ provider used to access the database and retrieve data in the same way one would access and manipulate in-memory objects. The LINQ to NHibernate driver is centered on the IQueryable interface.
Defining the root of our query It all starts with a simple extension method for the NHibernate session object or, more specifically, with an extension method to the ISession interface. The extension method is generic in T, as shown in the following code snippet: var list = session.Query();
The method Query returns a "collection object" implementing the interface IQueryable. Note that the preceding query is not executed immediately, but only if we start to access and use the data, probably by enumerating the list, as shown in the following code snippet: var list = session.Query(); foreach(var product in list) { // do something with product }
This behavior is called lazy evaluation and is something we have to get used to. The preceding LINQ query would generate a SQL statement on the database, equivalent to this: SELECT [list of all mapped fields] FROM PRODUCT
The preceding query loads all mapped fields for all records of the database table PRODUCT.
[ 247 ]
Writing Queries
Limiting the number of records returned It is an anti-pattern to define queries that can possibly return an unlimited set of records from the database. Just imagine having a query like the following code snippet: var orders = session.Query();
This query returns all orders stored in the Order table. What might not be a problem during the first time your application goes into production, can become a real bottleneck over time when more and more orders are defined and added to the system. Modern e-commerce sites, such as Amazon, store millions of orders in their databases over time. Thus, we should always limit the number of records to a reasonable maximum. This can easily be done by using the Take function, as shown in the following code snippet: var orders = session.Query().Take(200);
The preceding query would return a maximum of 200 orders from the database.
Filtering a set of records Of course this, in most of the cases, is not what we want. Normally we not only want to load the first 200 or so records from a given table, but rather a well-defined subset of the records. When extracting a subset of records, we call it filtering. The keyword used for filtering is Where. LINQ defines a method Where, which takes as a parameter, a predicate of T. A predicate is a function with one parameter that returns a Boolean, as shown in the following code snippet: Func predicate
Let's say we want a list of all discontinued products. The query would then look similar to the following code snippet: var discontinuedProducts = session .Query .Where(p => p.Discontinued);
In the preceding query, we used a lambda expression p => p.Discontinued to define the predicate. This expression means: Given a product p, take the content of its property Discontinued, and return it. If the return value is true, then the corresponding product is included in the resulting filtered set. Sometimes, people also refer to the act of filtering a set of data as reducing a set of data, and the filter function is called a reduce function or operation.
[ 248 ]
Chapter 9
Mapping a set of records Returning a set of records including all fields of the source database table is not always what we want. Thus, we need a way to select the subset of fields that we really want. For this purpose, we can use the Select function, which again is an extension method for the IQueryable interface. The select method takes a mapping function as parameter. The definition of the mapping function is as shown in the following code snippet: Func mapper
Let's say, we want to load a list of discontinued products, but with only the Id and Name of the product mentioned. For this purpose, we first define a class NameID, as shown in the following code snippet: public class NameID { public int Id {get; set;} public string Name {get; set;} }
Our query will then look as shown in the following code snippet: var discontinuedProducts = session .Query .Where(p => p.Discontinued) .Select(p => new NameID{Id = p.Id, Name = p.Name});
Again, we have defined the mapper function as a lambda expression, as shown in the following code snippet: p => new NameID{Id = p.Id, Name = p.Name}
This, put in words, means: take a Product object p and return an object of type NameID, whose properties, Id and Name, correspond to the Id and Name of the product p. So, we have mapped a Product entity into a NameID object.
Sorting the resulting set Filtering and mapping is not always enough. Sometimes, we also want to sort the resulting set of data. The NHibernate LINQ provider can do this for us too. Let's assume that we want to get a list of Person objects sorted by their last name, as shown in the following code snippet: var people = session.Query() .OrderBy(p => p.LastName);
[ 249 ]
Writing Queries
If we also want it to be sorted by their first name, then the query will look similar to the following code snippet: var people = session.Query() .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName);
Note that we can sort by as many columns as we want by simply appending the ThenBy method calls. Also, note that the OrderBy and ThenBy methods have their counterparts OrderByDescending and ThenByDescending, which, of course, sort in the reverse order. var people = session.Query() .OrderByDescending(p => p.LastName) .ThenByDescending(p => p.FirstName);
Thus, the preceding code snippet returns a reversely sorted list of people.
Grouping records If we want to count how many people have their last name starting with the same letter, and this also for each letter of the alphabet, then we can do so with the help of a "clever" group function, as shown in the following code snippet: var personsPerLetter = session.Query() .GroupBy(p => p.LastName.SubString(0,1)) .Select(g => new { Letter = g.Key, Count = g.Count() });
This returns a list of anonymous types, which contain the desired result. The GroupBy method returns IEnumerable, where T represents the type of the value by which we group and V represents the type which is grouped. In the preceding sample, T would be of type string and V of type Person. The IGrouping interface offers us a list of very useful aggregate functions that allow us to count, summarize or average values, as well as get the minimum or maximum of a value, where the value is one of the properties of V.
Forcing a LINQ query to execute immediately As mentioned earlier, a LINQ query is always lazy executed, that is only when we start to iterate over the result set is the query effectively executed. Sometimes, we do not want this behavior. Instead, we want to force the LINQ driver to immediately execute the query and return the result. We have different ways of doing so. First, we can end a query with a call to the extension methods ToArray or ToList. These two methods immediately start to enumerate the result set and put the resulting objects into an array or a list of T, respectively: var products = session.Query() .Where(p => p.ReorderLevel > p.UnitsOnStock) .ToArray() [ 250 ]
Chapter 9
The preceding query loads a list of all products that need to be reordered and puts them into an array. There is another very useful method that forces a LINQ query to execute immediately. It is the ToDictionary function. This function is handy when we want to retrieve a list of entities from the database and store them in a collection using a unique key per instance. Let's give an example, as shown in the following code snippet: var personsPerLetter = session.Query() .GroupBy(p => p.LastName.SubString(0,1)) .Select(g => new { Letter = g.Key, Count = g.Count() }) .ToDictionary(x => x.Letter, x => x.Count);
The preceding query creates a dictionary with the number of people whose last name starts with a given letter. The key is the starting letter of the last name and the value is the number of persons. As you can imagine, the first lambda expression defines the value we use as the key and the second lambda expression defines the corresponding value in the collection.
Changing from querying the database to querying in-memory objects Sometimes, we want to query some data from the database, and then further manipulate this data in creative ways using functionality that is not supported by the underlying database. For these scenarios, we need a way to instruct the LINQ to NHibernate provider that from this point on all manipulations will be done in-memory and NOT on the database. The method we can use for this purpose is the AsEnumerable method. Let's have a look at an example. Let's assume that there is an Email entity which contains a lot of records. We now want to generate a filtered list of e-mails, where the filter is actually a regular expression. However, regular expressions are not supported by most database products. Thus, the filtering has to be done in-memory, as shown in the following code snippet: var statusList = session.Query() .Select(e => e.EmailAddress) .AsEnumerable() .Where(e => SomeFilterFunctionUsingRegExp(e));
In the preceding example, the projection happens in the database, but the filtering is done in-memory.
[ 251 ]
Writing Queries
Creating a report using LINQ to NHibernate To get familiar with the usage of the LINQ to NHibernate provider, we want to create a couple of different reports that we'll print on-screen. In the following example, which is divided into two parts, our domain model is part of astronomy, and more precisely, of star classification. We will use an XML configuration file and XML mapping files to map our domain model.
Time for action – Preparing the system To be able to create reports, we first define the domain model, create the mappings and the database schema, and finally, create some data. Let's do this now:
1.
Open SQL Server Management Studio and create a new empty database called LinqToNHibernateSample.
2.
Open Visual Studio and create a new Console Application type project. Name the project LinqToNHibernateSample.
3.
Set Target Framework of the project to .NET Framework 4.
4.
Add references to the NHibernate and NHibernate.ByteCode.Castle assembly projects located in the lib folder.
5.
Add a new solution folder called Schema to the solution.
6.
Add the two files nhibernate-configuration.xsd and nhibernatemapping.xsd, located in the lib folder to this Schema solution folder.
7.
Add a class file, Star.cs, to the project and add the following code snippet to the file to define the Star entity: public class Star { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual IList Planets { get; set; } public virtual StarTypes Class { get; set; } public virtual SurfaceColor Color { get; set; } public virtual double Mass { get; set; } }
[ 252 ]
Chapter 9
8.
Add a class file, Planet.cs, to the project and add the following code snippet to the file to define the Planet entity: public class Planet { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual bool IsHabitable { get; set; } public virtual Star Sun { get; set; } }
9.
Add a class file, SurfaceColor.cs, to the solution. Define enum SurfaceColor in this file, as shown in the following code snippet: public enum SurfaceColor { Blue, BueToWhite, WhiteToYellow, OrangeToRed, Red }
10. Furthermore, add a class file, StarTypes.cs, to the project. Inside this class define enum StarTypes, as shown in the following code snippet: public enum StarTypes { O, B, A, F, G, K, M }
11. Add an XML file to the project and name it Star.hbm.xml. Make sure that you set Build Action of the file to Embedded Resource.
12. In this new XML file, define the mapping for the Star entity, as shown in the following code snippet:
[ 253 ]
Writing Queries
13. Note how the two enum type properties, Color and Class, of the Star entity are mapped. We use the type attribute to specify that NHibernate shall map these properties as database columns of type int (the base type of an enum is int). Also, note how the list of planets of a star is mapped. Specifically, review the inverse and cascade attributes, as well as the type of collection we use. Furthermore, note that we are using the Guid generator, which is optimized for usage in the database.
14. Add another XML file Planet.hbm.xml to the project and also set its Build Action to Embedded Resource.
15. Add the following code snippet to the file to map the Planet entity:
16. Note how we use the column attribute in the many-to-one tag to match the key definition of bag in the Star.hbm.xml mapping file.
17. Add a new XML file to the project. Name the file hibernate.cfg.xml. This file will contain the configuration information, as shown in the following code snippet:
NHibernate.Connection.DriverConnectionProvider [ 254 ]
Chapter 9 NHibernate.Driver.SqlClientDriver NHibernate.Dialect.MsSql2008Dialect server=.\SQLEXPRESS;database=LinqToNHibernateSample; integrated security=true NHibernate.ByteCode.Castle.ProxyFactoryFactory, NHibernate.ByteCode.Castle
18. Set the Copy to Output Directory property of the preceding file to Copy always.
19. To the Main method of the Program class, add code to instantiate a NHibernate Configuration object, as shown in the following code snippet: var configuration = new Configuration();
20. Call the method Configure of the configuration object. This will search for a
file called hibernate.cfg.xml in the current directory and, if found, take all configuration values out of it, as shown in the following code snippet:
configuration.Configure();
21. Add code to tell the configuration object to parse the assembly, where the entity
Star is defined for occurrences of HBM files, and use them to map the domain, as
shown in the following code snippet:
configuration.AddAssembly(typeof(Star).Assembly);
22. Add code to (re) create the database schema, as shown in the following code snippet:
new SchemaExport(configuration).Execute(true, true, false);
23. Use the configuration object to create a session factory, as shown in the following code snippet:
var factory = configuration.BuildSessionFactory();
[ 255 ]
Writing Queries
24. Add a call to the method CreateData, which will contain the code to create some precanned data and store it in the database. Pass the session factory as the parameter, as shown in the following code snippet:
CreateData(factory);
25. Once we have data available in the database, we want to use some LINQ queries to create on-screen reports. Thus, add a call to a reporting method, as shown in the following code snippet: QueryData(factory);
26. As the last two lines of the Main method, add the following code snippet: Console.Write("\r\n\nHit enter to exit:"); Console.ReadLine();
27. Now, implement the CreateData method. This method creates two stars with
planets, our Sun, and a star called 61 Virginis, which has three planets. Some more stars, having no known planets, are created as well. For each star, we define its name, relative mass (our sun has mass equal to 1), the surface color, and the classification. All stars, including their planets, are then stored in the database, as shown in the following code snippet: private static void CreateData(ISessionFactory factory) { var sun = new Star {Name = "Sun", Mass = 1, Class = StarTypes.G, Color = SurfaceColor.WhiteToYellow}; var planets = new List { new Planet{Name = "Merkur", IsHabitable = false, Sun = sun}, new Planet{Name = "Venus", IsHabitable = false, Sun = sun}, // please consult the sample code for full list of planets }; sun.Planets = planets; var virginis61 = new Star { Name = "61 Virginis", Mass = 0.95, Class = StarTypes.G, Color = SurfaceColor.WhiteToYellow }; var planets2 = new List { new Planet{Name = "Planet 1", IsHabitable = false, Sun = virginis61}, new Planet{Name = "Planet 2", IsHabitable = true, Sun = virginis61}, new Planet{Name = "Planet 3", IsHabitable = false, [ 256 ]
Chapter 9 Sun = virginis61}, }; virginis61.Planets = planets2; var stars = new List { sun, virginis61, new Star{Name = "10 Lacertra", Mass = 60, Class = StarTypes.O, Color = SurfaceColor.Blue}, new Star{Name = "Spica", Mass = 18, Class = StarTypes.B, Color = SurfaceColor.Blue}, // please consult the sample code for full list of stars }; using (var session = factory.OpenSession()) using (var tx = session.BeginTransaction()) { foreach (var star in stars) { session.Save(star); } tx.Commit(); } }
What just happened? In the preceding exercise, we prepared our system such that we will be able to create some reports using LINQ to NHibernate. We first defined a domain, and then we added XML mapping files to the project to map the domain to the underlying database. Then we configured NHibernate, created some data, and used NHibernate to persist the data to the database.
Time for action – Creating the reports Now that we have prepared the system, we are ready to generate some reports based on the data just persisted in the database using the LINQ to NHibernate provider.
1.
First, implement the method QueryData in the Program class, as shown in the following code snippet: private static void QueryData(ISessionFactory factory) { using (var session = factory.OpenSession()) [ 257 ]
Writing Queries using (var tx = session.BeginTransaction()) { PrintListOfStars(session); PrintListOfBigBlueStars(session); PrintSumOfStarMassPerClass(session); PrintListOfHabitablePlanets(session); tx.Commit(); } }
2.
Note how this method creates a session, a transaction, and, inside the transaction, calls four reporting methods, passing the session object to each of them.
3.
Let's implement the first of these methods, which shall print a list of all stars ordered by their respective names on-screen, as shown in the following code snippet: private static void PrintListOfStars(ISession session) { Console.WriteLine("\r\n\nList of stars ------------------\r\n"); var stars = session.Query() .OrderBy(s => s.Name); foreach (var star in stars) { Console.WriteLine("{0} ({1}, {2})", star.Name, star.Class, star.Color); } }
4.
Note how, in the preceding code snippet, we use the Query extension method to get IQueryable, which we can then sort by using the OrderBy method. Furthermore, note that the query is only executed the moment we start iterating over it (in the foreach loop).
5.
Next, we want to try filtering and ordering by multiple properties, even in the reverse order. The goal is to print on-screen a list of all stars with a blue surface color and a relative mass greater than 15: private static void PrintListOfBigBlueStars(ISession session) { Console.WriteLine("\r\n\nList of big blue stars -------\r\n"); var stars = session.Query() .Where(s => s.Color == SurfaceColor.Blue && s.Mass > 15) .OrderByDescending(s => s.Mass) .ThenBy(s => s.Name);
[ 258 ]
Chapter 9 foreach (var star in stars) { Console.WriteLine("{0} ({1}, {2}, Mass={3})", star.Name, star.Class, star.Color, star.Mass); } }
6.
In the next reporting method, we want to group our list of stars and use an aggregate function (Sum) to calculate the total relative mass of the stars stored in the database, grouped by their class. We then want to print the class and the total relative mass on-screen, as shown in the following code snippet: private static void PrintSumOfStarMassPerClass(ISession session) { Console.WriteLine("\r\n\nSum of masses per class -------\r\n"); var starMasses = session.Query() .GroupBy(s => s.Class) .Select(g => new {Class = g.Key, TotalMass = g.Sum(s => s.Mass)}); foreach (var mass in starMasses) { Console.WriteLine("Class={0}, Total Mass={1}", mass.Class, mass.TotalMass); } }
7.
Lastly, we implement a method that prints a list of habitable planets sorted by the Sun and by the planet name, as shown in the following code snippet: private static void PrintListOfHabitablePlanets(ISession session) { Console.WriteLine("\r\n\nList of habitable planets------\r\n"); var planets = session.Query() .Where(p => p.IsHabitable) .OrderBy(p => p.Sun.Name) .ThenBy(p => p.Name); foreach (var planet in planets) { Console.WriteLine("Star='{0}', Planet='{1}'", planet.Sun.Name, planet.Name); } }
8.
Note how we query a list of planets (and not suns) in the preceding code snippet.
[ 259 ]
Writing Queries
What just happened? We used the LINQ to NHibernate provider to define various queries, which include filtering, sorting, grouping, and aggregating. We used the resulting datasets to print an on-screen report.
Criteria queries NHibernate defines an alternative API that we can use to query our data from the database. These kinds of queries are called criteria queries. The queries are created by composing a set of criteria. Different types of criteria exist for filtering, sorting, projecting (or mapping), as well as grouping sets of records. In the past, it was only possible to define queries using magic strings. With the introduction of NHibernate 3, it is also possible to define stronglytyped criteria queries.
Untyped criteria queries In this section, we shall see how we can create criteria queries that result in the same set of records as in the examples used when we discussed the LINQ to NHibernate provider. All of it starts by defining the root of our criteria query, as shown in the following code snippet: var query = session.CreateCriteria();
The method CreateCriteria returns an object that implements the interface ICriteria. If we want to get a list of all products, then we need to use the method List of the interface ICriteria, as shown in the following code snippet: var products = session.CreateCriteria().List();
The method List() returns IList. Contrary to LINQ to NHibernate, the query is executed immediately when the List method is called. There is also a non-generic List method defined on the ICriteria interface. This method returns an object of type IList. The individual elements of this list are arrays of objects, that is, object[]. The number of objects and their order depends on the number of fields and their respective orders in the query. If we want to limit the number of records to be returned by the query, then we can use the
SetMaxResults function. To get the first ten products from the database, use this query, as
shown in the following code snippet:
var first10Products = session.CreateCriteria() .SetMaxResults(10) .List(); [ 260 ]
Chapter 9
Now, let's get a filtered list of products, say only all discontinued products, as shown in the following code snippet: var discontinuedProducts = session.CreateCriteria() .Add(Restrictions.Eq("Discontinued", true)) .List();
Please note the usage of a magic string to define the property name, which we want to use for filtering in the preceding query. Filtering is done by adding one to many restriction criteria to the query. Thus, if we want to get a list of all active products that need to be reordered, we would use the following code snippet: var discontinuedProducts = session.CreateCriteria() .Add(Restrictions.Eq("Discontinued", false)) .Add(Restrictions.GeProperty("ReorderLevel", "UnitsOnStock")) .List();
Although this is certainly very flexible, it is also very error prone when compared to the usage of LINQ to NHibernate. Imagine you make a typo when defining the restrictions in the preceding query and name the property UnitSonStock instead of UnitsOnStock. You would only realize this error during runtime. The static class Restrictions has many more handy functions for defining filter criteria of all kinds. Now, let's discuss how we can map a set of records. This is also called a projection. We are projecting one type and receive another type as a result of the operation. Unfortunately, projections using the Criteria API are not as easy as when using LINQ. We have to first define what fields we want to project. Then we additionally define a transformer, which takes those values and puts them into the desired target type, as shown in the following code snippet: var productsLookup = session.CreateCriteria() .SetProjection(Projections.ProjectionList() .Add(Projections.Property("Id")) .Add(Projections.Property("Name")) ) .SetResultTransformer( new AliasToBeanResultTransformer(typeof (NameID))) .List();
[ 261 ]
Writing Queries
In the preceding example, we used the SetProjection method to define the mapping. We selected the properties Id and Name of the product and we want to put them into an object of type NameID. The class NameID is defined as shown in the following code snippet: public class NameID { public int Id { get; set; } public string Name { get; set; } }
We are using AliasToBeanResultTransformer defined by NHibernate to transform the results of the query into a list of NameID objects. Note that the names of the properties in the target object must match the names of the projected properties. If this is not the case, then the Add method of the ProjectionList has an overload where we can define an alias. The alias has to correspond with the name in the target object. NHibernate 3 brings some slight improvements to the Criteria API. In most cases, the usage of magic strings can now be avoided and lambda expressions can be used to define the properties of interest in a type-safe way. Instead of defining a projection like this Projections.Property("Name"), we can now use Projections.Property(p => p.Name).
Sorting a result set is straightforward. We just add another criterion to our query, as shown in the following code snippet: var sortedProducts = session.CreateCriteria() .AddOrder(Order.Asc("Name")) .List();
To define a list of products that is sorted in reverse order, we just use the Desc method of the static Order class. We can sort by as many properties as we want. Just add another sort criterion for each additional field to the query. Grouping, which was a separate function in the LINQ provider, is part of the projection in the criteria query API. Assuming we want to group our products by the property Category and count the number of rows per category, we would use the following query: var productsGrouped = session.CreateCriteria() .SetProjection(Projections.ProjectionList() .Add(Projections.GroupProperty("Category")) .Add(Projections.RowCount(), "Num") ) .List();
[ 262 ]
Chapter 9
The criteria API is best suited if we have to dynamically generate queries based on, say, user selections. As an example, consider an application which allows a user to flexibly select filter criterions. Other than that, a LINQ or a HQL query are more readable, and thus more maintainable on the long run.
Strongly-typed criteria queries A new feature of NHibernate 3 is the ability to define criteria queries that do not use "magic" strings but are strongly-typed. For this purpose, the method QueryOver has been added to the ISession interface. Here, the generic parameter T represents the entity type we want to query. When using the QueryOver API, we specify the root of our queries, as shown in the following code snippet: var query = session.QueryOver();
To simply get a list of all products in the database, use this query, as shown in the following code snippet: var products = session.QueryOver().List();
Here, in contradiction to the Criteria API, we do not have to specify the return type in the list as it is already declared at the QueryOver level. If we want to limit the number of records returned by a query, then we can use the Take method. Our query then looks very similar to the one we used when discussing LINQ to NHibernate, as shown in the following code snippet: var first10Products = session.QueryOver() .Take(10) .List();
The API allows us also to filter the result set by using the familiar Where method. To get a list of all discontinued products use the following code snippet: var discontinuedProducts = session.QueryOver() .Where(p => p.Discontinued) .List();
We can, of course, combine multiple filters to, for example, get a list of all active products that need to be reordered, as shown in the following code snippet: var productsToReorder = session.QueryOver() .Where(p => p.Discontinued == false) .Where(p => p.ReorderLevel >= p.UnitsOnStock) .List(); [ 263 ]
Writing Queries
Instead of using multiple Where statements, we can also use a single one and combine the individual predicates into one, using Boolean operators: var productsToReorder = session.QueryOver() .Where(p => p.Discontinued == false && p.ReorderLevel >= p.UnitsOnStock) .List();
So far, so good! If you are familiar with LINQ, then the preceding expressions should not be of any surprise to you. Sorting the results is again very similar to LINQ when using the QueryOver API. The only difference is that LINQ defines OrderBy and OrderByDescending to define ascending and descending ordering, while the QueryOver API only defines an OrderBy method. However, this method has to be combined with a call to either Asc or Desc. When sorting by multiple fields, both the API have a ThenBy (and ThenByDescending for LINQ) method. To get a list of products sorted by Name ascending and by UnitPrice descending, we use the following code snippet: var sortedProducts = session.QueryOver() .OrderBy(p => p.Name).Asc .ThenBy(p => p.UnitPrice).Desc .List();
As it was in the Criteria API, it is also the hardest part to define a mapping using the QueryOver API. If I only want to retrieve Id and Name of all products and fill them into an array of NameID objects, then it can be done using the following code snippet: var productsLookup = session.QueryOver() .Select(p => p.Id, p => p.Name) .TransformUsing(Transformers.AliasToBean()) .List();
Note how we use the Select method to define the list of properties we want to select (or project). Each property is defined by a lambda expression, for example, p => p.Name to select the Name property. Then, we use the TransformUsing method to declare how NHibernate should transform the result of the projection. In the preceding case, we selected the AliasToBean transformer declaring NameID as the target type of the transformation. There are other transformers defined by NHibernate and you can even implement your own ones. The static class Transformers gives us a list of available transformers. Finally, we end the query with a call to the List method. We declare the target type here; otherwise, NHibernate would assume that the target type is still Product, as declared when calling the QueryOver method.
[ 264 ]
Chapter 9
A transformer used by a criteria query or a QueryOver query must implement the interface IResultTransformer. This interface has two methods that allow transformations on the row level, as well as transformations of the result set as a whole. When looking at the transformation at the row level, the transformer has a simple task to do; namely, to transform a tuple of objects (object[] tuple) to whatever target type we want. The tuple contains the value of the fields we declared in the projection. In the case where we select the properties Id and Name of the Product entity, we would have to convert a tuple with two elements (object[2] tuple) to a target type NameID by mapping tuple[0] to the property Id of the NameID object and tuple[1] to the Name property of the NameID object.
When transforming data by using projections, we can also group the result sets and apply aggregate functions to the fields. To group all products by Category and then count the number of rows for each category, as well as evaluate the average unit price per category, and the sum of units on stock per category, we would have the following query: var productsGrouped = session.QueryOver() .Select(Projections.Group(p => p.Category), Projections.Avg(p => p.UnitPrice), Projections.Sum(p => p.UnitsOnStock), Projections.RowCount()) .List();
To keep things simple, we didn't define a transformation in the preceding example and just let NHibernate return the rows of the result set as tuples of objects.
Time for action – Using QueryOver to retrieve data In this example, we want to add some products to the database and use the QueryOver method to retrieve a special selection of these products. We want to use the Loquacious configuration and ConfOrm mapping in this example to refresh our memory a bit.
1.
Open SQL Server Management Studio and log in to your local SQL Server Express edition.
2.
Create a new database called QueryOverSample.
3.
Open Visual Studio and create a new project. Use the Console Application template and name the project QueryOverSample.
[ 265 ]
Writing Queries
4.
Add references to the NHibernate.dll, NHibernate.ByteCode.Castle.dll, and ConfOrm.dll assemblies, located in the lib folder of the project.
5.
Add a new class to the project. Name it Category.cs.
6.
Add the following code snippet to define a Category entity for this new class file: using System; namespace QueryOverSample { public class Category { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual string Description { get; set; } } }
7.
Add a new class file to the project. Name it Product.cs.
8.
Add the following code snippet to the file to define a Product entity: using System; namespace QueryOverSample { public class Product { public virtual Guid Id { get; set; } public virtual string Name { get; set; } public virtual Category Category { get; set; } public virtual decimal UnitPrice { get; set; } public virtual bool Discontinued { get; set; } public virtual int ReorderLevel { get; set; } public virtual int UnitsOnStock { get; set; } } }
9.
To the Program class, add a static method to create an NHibernate Configuration object using Loquacious configuration and pointing to the local SQL Server Express edition database named QueryOverSample: private static Configuration GetConfiguration() { var cfg = new Configuration(); [ 266 ]
Chapter 9 cfg.SessionFactory() .Proxy .Through() .Integrate .LogSqlInConsole() .Using() .Connected .Through() .By() .Using(new SqlConnectionStringBuilder { DataSource = @".\SQLEXPRESS", InitialCatalog = "QueryOverSample", IntegratedSecurity = true }); return cfg; }
If the preceding code snippet looks strange to you, then please have a look at Chapter 8, Configuration, and re-read the section which introduces configuration via the Fluent API defined in NHibernate's Loquacious namespace.
10. Now we want to add a method to the Program class to define the mappings for our domain. To do this, we use mapping by the convention provided by ConfOrm, as shown in the following code snippet: private static void AddMappings(Configuration configuration) { var types = new[] {typeof (Category), typeof (Product)}; var orm = new ObjectRelationalMapper(); orm.TablePerClass(types); var mapper = new Mapper(orm); var hbmMappings = mapper.CompileMappingFor(types); configuration.AddDeserializedMapping(hbmMappings, "MyDomain"); }
You can find more details about how to use convention-based mappings using ConfOrm in Chapter 5, Mapping the Model to the Database, of this book.
[ 267 ]
Writing Queries
11. Add a static method to the Program class to (re) create the database schema given an NHibernate configuration object, as shown in the following code snippet:
private static void BuildSchema(Configuration configuration) { new SchemaExport(configuration).Execute(true, true, false); }
12. Now add another static method to the Program class, which will create our data, as shown in the following code snippet:
private static void AddProductsAndCategories( ISessionFactory sessionFactory) { }
13. Add code to this method, which creates five categories with names
Category 1, Category2, and so on. Each category will have a random
number of 0 to 9 associated products. Each product has a random value for the properties UnitPrice, UnitsOnStock, and ReorderLevel. The product is set as discontinued if a random number between 0 and 10 is greater than 8 (that is rather rare), as shown in the following code snippet:
var categories = new List(); var products = new List(); var random = new Random((int) DateTime.Now.Ticks); for (var i = 1; i c.Name).Asc .List(); foreach(var category in categories) { Console.WriteLine("Category: {0}", category.Name); } }
20. Add the following code snippet just before the tx.Commit(); statement in the PrintReports method to call the preceding reporting method: PrintListOfCategories(session);
[ 270 ]
Chapter 9
21. Run the application. You should see something similar to the following screenshot:
22. Review the query that NHibernate sent to the database server to retrieve the list of categories.
23. Add another reporting method that retrieves the list of all products that are not
discontinued and that need to be reordered as the respective reorder level is equal or higher than the number of units on stock. The list of products shall be ordered by category name, and then by product name, as shown in the following code snippet: private static void PrintProductsToReorder(ISession session) { Console.WriteLine("\r\nList of products to reorder:\r\n"); Product productAlias = null; Category categoryAlias = null; var products = session.QueryOver(() => productAlias) .JoinAlias(() => productAlias.Category, () => categoryAlias) .Where(() => productAlias.Discontinued == false) .Where(() => productAlias.ReorderLevel >= productAlias.UnitsOnStock) .OrderBy(() => categoryAlias.Name).Asc .ThenBy(() => productAlias.Name).Asc .List(); Console.WriteLine(); foreach (var product in products) { Console.WriteLine( "Category: {0}, Product: {1} (Units on stock: {2})", product.Category.Name, product.Name, product.UnitsOnStock); } } [ 271 ]
Writing Queries
24. Add code to the PrintReports method to call the preceding method. 25. Run the application and you should see something similar to the following
screenshot (note that due to the usage of random numbers, the number of products listed can vary with each run):
26. Again, review the SQL statement created by NHibernate to retrieve the list of products to reorder.
What just happened? We created a simple domain and mapped it, with the aid of ConfORM, to the underlying database. We then added some data to the database and, finally, we used the QueryOver API to create some basic reports whose results we have output to the console.
Have a go hero Define yet another reporting method, which returns the top ten (active) products with the highest number of units in stock. The list must be sorted by the number of units in stock. For each product, print the category name, product name, and units in stock on the screen.
Hibernate Query Language This is the original query language of NHibernate. It strongly resembles SQL, but is more object-oriented than its counterpart. HQL queries are defined as strings, and thus are not type-safe. On the other hand, and on a positive note, HQL gives support to dynamic entities for which a class does not exist at all. Parameters in HQL queries are defined by prefixing a name with a colon, for example:name. [ 272 ]
Chapter 9
In this section, we will see how to write HQL queries that are equivalent to the queries introduced in the section The LINQ to NHibernate provider. Each HQL query is created by calling the CreateQuery method of the ISession interface and passing the HQL string as a parameter. To query a list of all products, one would write the following code snippet: var products = session.CreateQuery("from Product p").List();
Note the familiar List method we already encountered when discussing the criteria query API. To limit the number of records returned from a query, we can use the method
SetMaxResults. To skip a number of records, we can use the SetFirstResult method: var first10Products = session.CreateQuery("from Product p") .SetFirstResult(10) .SetMaxResults(10) .List();
Starting with NHibernate 3.2, we can write the preceding query as "from Product skip 10 take 10", which is a more concise way than when using the SetFirstResult and SetMaxResult methods.
We can filter the list of products and, say, retrieve all discontinued products only, as shown in the following code snippet: var discontinuedProducts = session .CreateQuery("from Product p where p.Discontinued") .List();
We can define filters using the parameters, as shown in the following code snippet: var hql = "from Product p" + " where p.Category = :category" + " and p.UnitPrice