Transcript Slide 1
Object-Relational Mapping in the Microsoft World by Benjamin Day Benjamin Day Consulting, Inc. About the speaker • Owner, Benjamin Day Consulting, Inc. Email: [email protected] Web: http://www.benday.com Blog: http://blog.benday.com • Trainer Visual Studio Team System, Team Foundation Server • Microsoft MVP for C# • Microsoft VSTS/TFS Customer Advisory Council • Leader of Beantown.NET INETA User Group Agenda • • • • • Overview of “the problem” Brief discussion of the options LINQ to SQL NHibernate Entity Framework THE PROBLEM The Problem • Mismatch between objects and relational databases Objects = inheritance, polymorphism, composition Database = rows of data with relationships to other rows of data One Table Per Class • The class looks like the table • Great candidate for a typed dataset Simple Works nicely with DataAdapter because of RowState Limitations of Typed Dataset Route • Tight coupling with the database • Inheritance is difficult • How do you validate data? Null, data type, and length check validation is handled Serious validation lives outside of the dataset not very object-oriented • The ‘object’ route is better for validation Validation goes in the “set” property Object - DataRow Hybrid • Every object wraps a typed DataRow • Properties control access to columns on the DataRow Facilitates more complex validation • DataRow becomes a data transfer object Still works nicely for System.Data integration Primitive Obsession • Validation in the get / set properties is ok but is phone number validation really the responsibility of the Person class? • James Shore’s “Primitive Obsession” Too many plain scalar values Phone number isn’t really just a string http://www.jamesshore.com/Blog/ Coarse-Grained vs. Fine-Grained Object Model • Fine-grained = More object-oriented • Data and properties are split into actual responsibilities coarse-grained fine-grained But how do you save it? • Four classes go to one table? • Five instances go to one table? Inheritance, Collections, and Relationships • Employee is a Person • Employee has Underlings • Employee has a Supervisor Limitations of Object - DataRow Hybrid • Inheritance >1 DataRow at once One for base class, one for descendent • Which instance of DataSet is the object using? Complicates saves Client has to worry about the internal state of the object • How to save parent-child relationships? Employee has FK to a Supervisor Supervisor needs to be saved first • Objects have ability to modify internal state of other objects wrapping the same DataSet bugs & encapsulation • Number of records in the DataSet tends to grow rather than shrink How to clear out old, unused records in the DataSet? How do you know when they’re old? Object-Only Model • No more DataRows • Classes are member variables, methods and properties • Clean “Domain Model” pattern Business tier worries about itself Decoupled from the database • Straightforward inheritance, polymorphism Complications in the Object-Only Model • Data access (“Mapper”) tier has to “adapt” data and structures from the database into populated business objects and vice versa • How to manage IsDirty for INSERT vs UPDATE? Adding IsDirty logic to domain objects is a violation of the “separation of concerns” • Concurrency management? • Transaction management? Do you really want to solve these problems? • Problems are solvable • Everybody writes their own solution • Persistence is a distraction from solving The Real Business Problem AVAILABLE OPTIONS Options • • • • • • LINQ to SQL Entity Framework NHibernate Wilson OR Mapper LLBLGen And more… LINQ to SQL • Available in Visual Studio 2008 • “Better typed dataset” LINQ to SQL: Pros / Cons Pros • You don’t have to write data access code • Good for Rapid Application Prototyping • Integrated into Visual Studio • Designer support • Will generate the database schema for you Cons • SQL Server only • Limited inheritance modeling (Table Per Hierarchy only) • Closely tied to the database • Unusual way of handling stored procedures • Generates code Entity Framework • Microsoft’s first, real ORM solution (well…I guess this depends on who you talk to) • In beta • Rumored to be released with Visual Studio 2008 sp1 Entity Framework: Pros / Cons Pros • Full-featured • Not open source • Integrated into Visual Studio • Designer support (eventually) • Support for non-SQL Server databases (eventually) • Supports LINQ • Support Table-per-Type Cons • Still in beta • Not open source • Version 1 NHibernate • • • • Full-featured ORM solution Open source Based on Java’s Hibernate framework Uses XML to map tables/columns to objects/properties NHibernate: Pros / Cons Pros • Rich selection of mappings • Inheritance modelling • Polymorphic queries • Established solution with lots of current users • Support for multiple database vendors • Open source • Free Cons • Open source • Free • Not from Microsoft • 3rd party library • Limited to zero GUI support • Currently doesn’t support LINQ …but does have HQL LINQ TO SQL Using the LINQ to SQL O/R Designer • Define classes • Set up inheritance LINQ to SQL vs. Typed DataSets • My $0.02 – LINQ to SQL is the new Typed DataSet LINQ to SQL vs. LINQ to DataSets • • • • DataSets in VS2008 are query-able with LINQ DataTable’s object model has changed DataTable now extends from TypeTableBase<T> TypedTableBase<T> extends DataTable • Backwards compatible • No special way to fill the DataSet/DataTable with LINQ to SQL • Still uses DataAdapter LINQ to SQL vs. Raw SQL Access • Hopefully you don’t do this… • The data access strategy of masochists everywhere LINQ to SQL vs. NHibernate • NHibernate is LINQ to SQL’s older, more successful cousin • 5+ years in .NET & Java • Full-featured ORM framework • Maps tables/columns to classes/properties • Uses xml mapping files or attributes • Comprehensive inheritance modeling • Has LINQ-like object query syntax • HQL – Hibernate Query Language • Multi-vendor database support • Oracle, SQLServer (2000 & 2005), MySql, Sybase, etc • Lets you focus on the business problem rather than persistence Using LINQ to SQL with Unit Tests • When testing database code, database must be in a known state • Easiest way: Wipe the database between tests DataContext.DeleteDatabase() DataContext.CreateDatabase() Database schema always in sync with code • Harder way: Unit test manages transaction Rollback at end of unit test LINQ to SQL in an n-tier Application • Common BaseClass • Hooking into save events Auto-updating: ModifiedDate, ModifiedBy • Keeping your code organized with the “Service Layer” pattern Common Business Base Class • Each business class should probably have similar fields Id ModifiedDate, ModifiedBy CreateDate, CreatedBy • Bummer: LINQ to SQL isn’t great at this (NHibernate does this effortlessly) Mapped columns must be defined on the concrete Code Demo • Introduce a common business base class Code Demo • Implement the partials and auto-populate the base class properties Auto-update base class properties from DataContext • Generated DataContext & other objects are partial classes • Generated code gives you partial methods on DataContext for each object InsertXxx(), UpdateXxx(), DeleteXxx() • Create your own partial class and create your own implementation of the method • Don’t forget to call ExecuteDynamicInsert(), ExecuteDynamicUpdate() or ExecuteDynamicDelete() Other fun stuff with the partial methods • Your partial implementations wipe out the LINQ to SQL default implementation • (Who cares? This is boring.) • You could put your own implementation that uses stored procedures in your partials! Service Layer Pattern “Defines an application’s boundary with a layer of services that establishes a set of available operations and coordinates the application’s response in each operation.” -Randy Stafford From “Patterns Of Enterprise Application Architecture” by Martin Fowler, Randy Stafford, et al. Chapter 9 Why Service Layer? • Formally defines supported business tier operations (aka methods) • Methods provide ideal target for unit testing • Keeps code organized Code review: anything complex not in the service layer refactor Keeps code out of the UI • Isolates the Domain Model (business) objects Minimize usage of the Domain Model objects outside of the Business tier Service Layer in LINQ? • CRUD operations for each business object • Any specialized “get” operations Centralized place for any custom from-where-select’s • Factory methods • Create a BusinessFacade<T> ENTITY FRAMEWORK Entity Framework Overview • • • • Still in beta Official release with VS2008 sp1 Trying to be more than just an ORM 3 layers Conceptual Model (classes) Storage / Logical Model (table definitions) Mapping layer NHIBERNATE What NHibernate isn’t • “Hibernate” has nothing to do with Windows Hibernate • Not object serialization • Not a code generator NHibernate To The Rescue • .NET port of the Java-based Hibernate Object-Relational Framework NHibernate 1.2 is (roughly) Hibernate 2.1 + some features of Hibernate 3.0 • “Hibernate's goal is to relieve the developer from 95 percent of common data persistence related programming tasks.” Facilitates saves and retrieves of objects • “Hibernate provides transparent persistence, the only requirement for a persistent class is a no-argument constructor.” Keeps your objects clean • Open-source, free software under LGPL http://www.jboss.org/opensource/lgpl/faq • Ported by Tom Barrett, Mike Doerfler, Peter Smulovics, and Sergey Koshcheyev What is it? • Object Relational Mapping Framework • XML-based • Mapping files *.hbm.xml Classes/Properties Tables/Columns 1+ mapping files • hibernate.cfg.xml Database Dialect: SQL Server, Oracle, MySQL, etc. Which assemblies to manage Which mapping files to use What it will do for you. • Make your life easier • Simplify your data access • Allow you to focus on your business tier hibernate.cfg.xml <?xml version="1.0" encoding="utf-8" ?> <hibernate-configuration xmlns="urn:nhibernate-configuration-2.0" > <session-factory name="NHibernate.Test"> <!–- Writes all SQL to Console --> <property name="show_sql">true</property> <property name="connection.provider">NHibernate.Connection.DriverConnectionProvider</p roperty> <property name="query.substitutions">true 1, false 0, yes 'Y', no 'N'</property> <!–- SQL SERVER --> <property name="connection.connection_string" >Server=localhost\sql2005;initial catalog=bugs;User ID=sa;Password=sa_password;Min Pool Size=2</property> <property name="dialect">NHibernate.Dialect.MsSql2000Dialect</property> <property name="connection.driver_class“ >NHibernate.Driver.SqlClientDriver</property> <!-- mapped assemblies --> <mapping assembly="NHibernateResearch.Business" /> </session-factory> </hibernate-configuration> Mapping files (*.hbm.xml) <?xml version="1.0" encoding="utf-8" ?> <hibernate-mapping xmlns="urn:nhibernate-mapping-2.0"> <class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person"> <id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /> </id> <timestamp name="LastModified" /> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> </class> </hibernate-mapping> <class> • Simplest mapping • Maps a class to a database table • Attributes “name” = name of the persistent class Fully qualified class name, assembly name (no “.dll”) “table” = name of the table • Required element <id> = defines object identity • Common elements <timestamp>, <version> for optimistic concurrency <property> for mapping class properties to database columns <id> <id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /> </id> • • • Required element of <class> mapping Used to establish object identity, database primary key Attributes • “name” – name of the property “column” – (optional) name of the table column “type” – (optional) data type for the property “unsaved-value” – used to determine INSERT vs. UPDATE For “identity” columns, specify a <generator> class=“native” uses int identity column (SQL Server) or sequence (Oracle) class=“guid” generates guid keys • For non-”identity” columns use <composite-id> This approach is strongly discouraged <property> <property name="LastName" not-null="true"></property> <property name="Html"> <column name="HtmlContent" sql-type="text" not-null="true"></column> </property> • • • Child element to <class> Used to map a property to a database column Attributes “name” – name of the property “column” – (optional) name of the database column “not-null” – (optional) describes the nullability of the database column defaults to nullable “type” – (optional) datatype for the property • Optional <column> element describes information about the database column “sql-type” attribute – override default column datatype “length” attribute – override default column length Mapping the Person class <class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person"> <id name="PersonId" type="System.Int32" unsaved-value="0"> <generator class="native" /> </id> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> <property name="Email" not-null="true"></property> <property name="HomePhone" not-null="true"></property> <property name="WorkPhone" not-null="true"></property> </class> The NHibernate Session • Main point of contact ISession interface • SessionFactory.OpenSession() • Objects are associated the ISession Lazy loading • Save, delete, and retrieve operations Saving • SaveUpdate() Person person1 = new Person(); // create person1.Name.FirstName = "firstname1"; // set properties person1.Name.LastName = "lastname1"; session.SaveOrUpdate(person1); session.Flush(); session.Close(); Transactions • Call BeginTransaction() on the session public virtual void Save(object item) { ITransaction tx = null; try { tx = m_session.BeginTransaction(); m_session.SaveOrUpdate(item); tx.Commit(); } catch (Exception ex) { if (tx != null) tx.Rollback(); throw ex; } } Retrieving • Query / HQL syntax HQL = Hibernate Query Language SQL-like queries against the object model Use for more complex queries (JOINs) • Criteria syntax Build the query programmatically • NHibernate Allows Polymorphic Querying HQL Sample •Find Employees by Supervisor’s Name HQL Sample public IList FindSupervisorEmployees( string firstName, string lastName) { string hqlQuery = @" from Person p where p.Supervisor.FirstName = :firstName and p.Supervisor.LastName = :lastName"; IQuery query = Session.CreateQuery(hqlQuery); query.SetParameter("firstName", firstName); query.SetParameter("lastName", lastName); return query.List(); } ICriteria: Load All By Type • Gets an IList of objects by type or interface public IList GetList(Type type) { ICriteria criteria = m_session.CreateCriteria(type); return criteria.List(); } • Polymorphic queries Piano : MusicalInstrument Flute : MusicalInstrument CreateCriteria(typeof(MusicalInstrument)) Returns mix of Piano and Flute objects ICriteria: Load By Property Value • Gets a list of objects by type where a property has a certain value public IList Get(Type type, string propertyName, object propertyValue) { ICriteria criteria = m_session.CreateCriteria(type); criteria.Add(Expression.Eq(propertyName, propertyValue)); IList list = criteria.List(); return list; } Deleting • Session.Delete(object instance) Making Person More Fine-grained fine-grained coarse-grained fine-grained Mapping Fine-grained Person <component> <component name="Name" class="NHibernateResearch.Business.Name, NHibernateResearch.Business"> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> </component> • Used to map columns in a table to properties on a different class • Object does not have it’s own “identity” No primary key, no <id> Only exists in relation to the containing class Cannot save an instance of “Name” by itself to the database Mapping Person Using <component> <class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person"> <id name="PersonId" unsaved-value="0"> <generator class="native" /> </id> <component name="Name" class="NHibernateResearch.Business.Name, NHibernateResearch.Business"> <property name="FirstName" not-null="true"></property> <property name="LastName" not-null="true"></property> </component> <component name="Email" class="NHibernateResearch.Business.Email, NHibernateResearch.Business"> <property name="Address" column="EmailAddress" not-null="true"></property> </component> <component name="WorkPhone" class="NHibernateResearch.Business.Phone, NHibernateResearch.Business"> <property name="Number" column="WorkPhone" not-null="true"></property> </component> <component name="HomePhone" class="NHibernateResearch.Business.Phone, NHibernateResearch.Business"> <property name="Number" column="HomePhone" not-null="true"></property> </component> </class> Concurrency Columns • • • • Child elements of <class> Must be declared immediately after the <id> <timestamp> – uses date/time data to manage concurrency <version> – uses a numeric version id Mapping Inheritance • Employee is a Person <joined-subclass> <class name="NHibernateResearch.Business.Person, NHibernateResearch.Business" table="Person"> <id name="PersonId" unsaved-value="0"> <generator class="native" /> </id> ... <joined-subclass name="NHibernateResearch.Business.Employee, NHibernateResearch.Business" table="Employee"> <key column="EmployeeId"/> <property name="Title" not-null="true" /> <many-to-one name="Supervisor" column="SupervisorId" not-null="false" cascade="saveupdate"></many-to-one> <bag name="Underlings" lazy="true" inverse="true" cascade="save-update"> <key column="SupervisorId"></key> <one-to-many class="NHibernateResearch.Business.Employee, NHibernateResearch.Business" ></one-to-many> </bag> </joined-subclass> </class> • • • Subclass gets its own table for its properties/columns <joined-subclass> element goes inside of the superclass’ <class> or <joined-subclass> element <key> element defines the name of the column to use to join from the superclass’ <id> (Person.PersonId Employee.EmployeeId) Mapping Associations & Collections • Supervisor is an Employee • Employee has Underlings Associations • Associations define relationships between classes • NHibernate uses the association mappings to automatically store and retrieve related objects • <one-to-one> – file has one owner • <one-to-many> – directory has many files • <many-to-one> – sub-directory has one parent directory (many sub-directories, one parent) • <many-to-many> – Many users, many roles intersection of a user and a role <many-to-one>, <one-to-many> <joined-subclass name="NHibernateResearch.Business.Employee, NHibernateResearch.Business" table="Employee"> <key column="EmployeeId"/> <property name="Title" not-null="true" /> <many-to-one name="Supervisor" column="SupervisorId" not-null="false" cascade="save-update”></many-to-one> <bag name="Underlings" lazy="true" inverse="true" cascade="save-update"> <key column="SupervisorId"></key> <one-to-many class="NHibernateResearch.Business.Employee, NHibernateResearch.Business“ /> </bag> </joined-subclass> • • • • • • SupervisorId is a foreign-key to the supervisor’s EmployeeId Many-to-one turns the SupervisorId into an instance of Employee not-null=“false” means that the SupervisorId is nullable cascade=“save-update” tells NHibernate to check the many-to-one relationship for changes when an INSERT or UPDATE is requested The association syntax is also used to populate collections of objects <bag> populates an IList of Employee objects for the current employee select * from employee where supervisorId=@currentEmployeeId Collections Mapping Element Interface Implementation Description <list> IList ArrayList Ordered collection <bag> IList ArrayList Unordered collection, allows duplicates <map> IDictionary Hashtable Dictionary (key/value pairs) • Use associations to store collections of reference types – <one-to-many>, <many-to-many> • Use <element> tag to store collections of value types – Similar syntax to <property> Collections: <bag> Collection of objects <bag name="Children" lazy="true" cascade="all"> <key column="ParentId"></key> <one-to-many class="classname"></one-to-many> </bag> Collection of values <bag name="Children" lazy="true" cascade="all"> <key column="ParentId"></key> <element column=“columnName“ type="System.Int32"/> </bag> • • • • System.Collections.IList If collection of values, allows duplicates If collection of objects, duplicates get eaten By default, items in the collection same order as they in the tables “order-by” attribute available to do database sorts on data Collections: <list> <list name="Children" lazy="true" cascade="all"> <key column="ParentId"></key> <index column="indexValue"></index> <one-to-many class=“classname" ></one-to-many> </list> • • • System.Collections.IList Uses an numeric, zero-based index column for sorting Missing index values become null child0.Index = 0; // no child with Index of 1 child1.Index = 2; parent.Children.Add(child0); parent.Children.Add(child1); session.Save(parent); // reload the parent parent.Children[0] child0; parent.Children[1] null; parent.Children[2] child1; Collections: <map> <map name="Children" cascade="all"> <key column="ParentId"></key> <index column="indexval" type="System.Int32"></index> <one-to-many class="classname" /> </map> • • • • • • System.Collections.IDictionary Unsorted collection uses Hashtable Collection of key/value pairs Use <index> for a value type key Use <index-many-to-many> for an object key <index-many-to-many column="ItemId" class="classname" /> If sorted, collection uses SortedList “order-by” attribute available to do database sorts on data “sort” attribute for sorting using an implementation of IComparer Collections • Lazy loading lazy = “true” Expose collections interface not concrete class ArrayList IList Hashtable IDictionary Lazy-load proxy loads on first access • “inverse” attribute for bi-directionality “child” has a reference back to the parent Employee has Supervisor Supervisor has Employees • “cascade” attribute The “cascade” attribute • Controls when changes to child objects are saved • Options: “none” – no cascading saves/deletes “save-update” – cascade for INSERTs and UPDATEs “delete” – cascade on DELETEs only “all” – cascade on INSERT, UPDATE, DELETE “all-delete-orphan” – same as all, automatically delete any child objects when the parent’s collection NHibernate, Collections, and Generics • Current release is NHibernate 1.2.1 • Now supports generics Nullable Columns & ValueTypes • Nullable columns should be avoided even if you don’t use NHibernate • Sometimes you need them • Sometimes you’re stuck with them (legacy databases) • Under .NET 2.0 – Use the “?” syntax for the nullable properties on your classes Auditing Info • Auditing info common to all tables / classes CreateDate, CreatedBy LastModified, LastModifiedBy • Base class functionality • How to know when to update the values? ILifecycle • Allows class to perform actions at certain times during the NHibernate “lifecycle” of the instance • OnSave(), OnUpdate(), OnDelete(), OnLoad() • Slightly “pollutes” object model with NHibernate specific code • Now is deprecated IInterceptor Code Demo • Refactor Person to extend BusinessBase • Implement auditing on Person Intellisense for NHibernate • • • • • Eliminate the tedium Eliminate the potential errors Learn what else is in there Download the source Schemas in “src\NHibernate” • Copy to Visual Studio’s “Schemas” directory • nhibernate-configuration-2.0.xsd nhibernate-mapping-2.0.xsd nhibernate-generic.xsd “c:\program files\Microsoft Visual Studio 8\Xml\Schemas” Restart Visual Studio Contains(), Evict(), Lock(), Refresh() • Methods on NHibernate Session • Contains(object) – Queries the session to determine if it is managing the supplied object Is the object persistent for the session • Evict(object) – Remove an object from the session’s control • Lock(object, LockMode) – Request that the object becomes persistent for the session • Refresh(object) – Update the object with the current data from the database “assembly” & “namespace” • Attributes on <hibernate-mapping> • Specifies default Assembly name Namespace • This name="Com.Benday.ContentManagement.Business. DomainModel.Folder, Com.Benday.ContentManagement.Business” name=“Folder” Simplify Data Access With Generics • Persistent objects operations are almost identical ISession Save(), Get(), Delete() ICriteria GetBy***() • Leads to code duplication in your façade (aka Factory, Finder, Data Access) classes • Use .NET 2.0 generics to Simplify Gain compile-time type safety Code Walkthrough • Com.Benday.NHibernate • BusinessFacade<> Mapping: <query> • Allows you to write HQL complex queries and store it separately from the code • Assigned a unique name • Accessed through ISession.GetNamedQuery() • Executed via IQuery.List() Mapping: <sql-query> • • • • • Write SQL queries against the native database Still brings back persistent objects ISession.GetNamedQuery(), IQuery.List() <return class=“classname” alias=“table alias” /> Table alias must be in “{ }” Design Strategies • Model to schema Create the object model first, then create mappings Have NHibernate generate the database schema Allows you to be much more database independent Concentrate on creating a good object model and less about storage This is the best to start learning NHibernate -Write some code, write some mappings -Export the schema through NHibernate -Look at the database tables and FK relationships and see if it’s what you expected • Schema to model Database first, then classes, then mappings Legacy development More difficult, more about the needs of the database and less focus on the object model Create Database Using SchemaExport • NHibernate.Tool.hbm2ddl namespace • Reads hbm’s and classes creates database schema • Reads the database dialect generates for your db (MySql, Sql Server, Oracle, etc etc) public static void ExportSchema() { Configuration config = new Configuration(); config.Configure(); SchemaExport exporter = new SchemaExport(config); exporter.Drop(true, true); exporter.Create(true, true); } • Great for unit testing always have a clean db db model always in sync with the hbm’s Code Demo • • • • • • Use BusinessFacade<> Use schema export from unit tests Create an fixture base class Create a PersonFixture Create a Person class Play around with [TestInitialize] & [TestCleanup] Mappings: Create a UNIQUE database constraint • “unique-key” attribute on the <column> mapping • Set the same value on all properties that should participate in the key Benjamin Day • • • • • • • Consultant, Trainer Architecture, Development, Project Coaching Microsoft VSTS Customer Advisory Council MVP for C# Leader of Beantown .NET User Group VSLive, O’Reilly Conferences Team System, Team Foundation Server, NHibernate, C#, ASP.NET, TDD, WCF • http://blog.benday.com • [email protected]