Modeling Tricks My Relational Database Never Taught Me

4,956 views

Published on

In this session we will explore several modeling scenarios from my own experience that can easily be achieved using RavenDB, but difficult (if not nearly impossible) to build using a classic relational database. The focus will be on helping those accustomed to SQL Server or other relational databases learn good document modeling skills by example, with a summary of document modeling guidelines at the end.

0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
4,956
On SlideShare
0
From Embeds
0
Number of Embeds
2,570
Actions
Shares
0
Downloads
14
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

Modeling Tricks My Relational Database Never Taught Me

  1. 1. Modeling Tricks My Relational Database Never Taught Me David Boike @DavidBoike make-awesome.com
  2. 2. About Me • Principal Consultant, ILM Professional Services • NServiceBus Champion • Author of Learning NServiceBus • Husband, father, geek, amateur beer brewer • @DavidBoike • www.make-awesome.com
  3. 3. SQL Server
  4. 4. SQL Server
  5. 5. SQL Server • SQL first appeared – 1974 – Hard drive price: $185/MB, $189,000/GB • Microsoft SQL Server 1.0 – 1989 – Hard drive price: $7.48/MB, $7,659/GB • Current Version: SQL 2012 – November 2013: 0.004¢/MB, 4.096¢/GB http://www.jcmit.com/diskprice.htm
  6. 6. Entity Framework
  7. 7. SQL Server
  8. 8. RavenDB
  9. 9. What is RavenDB? • Fully transactional document database • Dead simple API Store(object entity) Delete<T>(T entity) Load<T>(string id) Query<T>() • Smart LINQ-powered client library • Safe by default • Powerful indexes • Replication/sharding • Fraction of SQL Server’s cost • Fraction of SQL Server’s requirements
  10. 10. RavenDB
  11. 11. Document Modeling
  12. 12. Bad Document Modeling
  13. 13. Take the red pill…
  14. 14. Inheritance Oh boy, Com Sci 101 again!
  15. 15. Inheritance: Flat
  16. 16. Cliché Polymorphism Examples • Animals (cat, dog, horse) • Cars (sedan, truck, minivan, SUV) – More broadly, machines (bike, motorcycle, car) • Shapes (circle, rectangle, square)
  17. 17. Inheritance: Table per Class Ale BeerId AleProp1 AleProp2 Beer BeerId Type Name ABV IBU Lager BeerId LagerProp1 LagerProp2 Stout BeerId StoutProp1 StoutProp2
  18. 18. Inheritance: Table per Top-Level Class
  19. 19. Inheritance: Extended Attributes AttributeNames: foo:S:0:11:intVal:S:11:2: AttributeValues: Hello World42 Deserialized: foo=“Hello World” bar=“42”
  20. 20. Inheritance: XML Attributes
  21. 21. Raven Inheritance public class BeerListModel { public string Id { get; set; } public List<BeerModel> Beers { get; set; } public BeerListModel() { Beers = new List<BeerModel>(); } } public abstract class BeerModel { public string Name { get; set; } public decimal ABV { get; set; } public int IBU { get; set; } } public class AleModel : BeerModel { public string AleProp1 { get; set; } public string AleProp2 { get; set; } } public class LagerModel : BeerModel { public string LagerProp1 { get; set; } public string LagerProp2 { get; set; } } public class StoutModel : BeerModel { public string StoutProp1 { get; set; } public string StoutProp2 { get; set; } }
  22. 22. Raven Inheritance
  23. 23. Inheritance: Lessons • Inheritance/Polymorphism in SQL Server sucks • In RavenDB, it’s no big deal • Key: With RavenDB we can model things just like they exist in real life.
  24. 24. Hierarchy
  25. 25. SQL Hierarchy SELECT * FROM Category WHERE SiteId = @SiteId AND ParentId = @ParentId
  26. 26. SQL Hierarchy SELECT * FROM Category WHERE SiteId = @SiteId
  27. 27. Raven Hierarchy #1 public class CategoryTree { // Sites/42/Categories public string Id { get; set; } public List<Category> RootCategories { get; set; } public CategoryTree() { RootCategories = new List<Category>(); } }
  28. 28. Raven Hierarchy #1 public class Category { public string CategoryId { get; set; } public string Name { get; set; } // Other Stuff public List<Category> ChildCategories { get; set; } public Category() { ChildCategories = new List<Category>(); } }
  29. 29. Raven Hierarchy #1
  30. 30. Raven Hierarchy #2 public class Category { public string Id { get; set; } public string Name { get; set; } // Other Stuff } public class CategoryTree { public string Id { get; set; } public List<CategoryRef> RootCategories { get; set; } } public class CategoryRef { public string CategoryId { get; set; } public List<CategoryRef> ChildCategories { get; set; } } // Don't forget default constructors!
  31. 31. Raven Hierarchy #2
  32. 32. Loading Hierarchy • Method 1 – One document, just load it • Method 2 – Multiple Documents – Normally to load within collections • .Include("RootCategories,CategoryId") • Does not work recursively • Works fine for one level
  33. 33. Loading Hierarchy var tree = RavenSession .Load<CategoryTree>("Sites/42/CategoryTree"); // Recursively build list of ids needed string[] ids = RecurseOnYourOwnTime(tree); // ;-) var cats = RavenSession .Load<Category>(ids) .ToDictionary(doc => doc.Id);
  34. 34. Hierarchy: Lessons • Model by Units of Change and Transactional Boundaries – Guided our choice of hierarchy implementation – Think about actors in your use cases • Always initialize collections and child objects – Nobody likes a NullReferenceException • We can have global “singleton” documents for configuration – Site/Configuration, Site/Categories, etc. – Great candidates for Aggressive Caching
  35. 35. Duplication You want me to do what?
  36. 36. Wordpress ERD
  37. 37. Wordpress ERD Comments Comment Meta Options Posts PostMeta Term Relationships Users UserMeta Term Taxonomy Links Terms
  38. 38. Copy a Post • 3 tables involved – wp_posts – wp_postmeta (17 types) – wp_term_relationships • More if we want to copy comments – wp_comments – wp_commentmeta
  39. 39. Copy a Post • Fairly simple example – New row in Posts • Store new PostId – New rows in PostMeta w/ PostId – New rows in TermRelationships w/ PostId • What if we had to copy a table related to TermRelationships? • What if an object graph spans dozens of tables? • What if PostMeta contains hidden ids linking to other tables? • P.S. Revisions are stored as additional records in Posts!
  40. 40. Raven Post Model Example public class WordPressPost { public string Id { get; set; } public string AuthorId { get; set; } public DateTimeOffset PostDate { get; set; } public string Contents { get; set; } public string Title { get; set; } public string Excerpt { get; set; } public PostStatus Status { get; set; } // etc. public List<string> Categories { get; set; } public List<string> Tags { get; set; } public List<string> Series { get; set; } public List<string> RelatedPostIds { get; set; } public List<PostLink> Links { get; set; } public List<PostMedia> Media { get; set; } // Comments?... }
  41. 41. Options for Comments 1. Add to WordPressPost : public List<PostComment> Comments { get; set; } 2. Document per comment 3. Separate document for comments • WordPressPosts/1 • WordPressPosts/1/comments 4. Capped documents • WordPressPosts/1 • WordPressPosts/1/comments/1 • WordPressPosts/1/comments/2 • WordPressPosts/1/comments/3
  42. 42. Capped Documents Bounded Contexts Unbounded Contexts Posts/1 Posts/1 Posts/1/Comments Posts/1/ Comments/3 Posts/1/ Comments/2 Posts/1/ Comments/1
  43. 43. How to duplicate? // Global.asax / App_Start config Mapper.Initialize(cfg => { cfg.CreateMap<BlogPost, BlogPost>() .ForMember(p => p.Id, opts => opts.Ignore()); }); Can also decorate model’s Id property with IgnoreMapAttribute but this leaks a dependency to AutoMapper in your models.
  44. 44. How to duplicate? BlogPost post = RavenSession.Load<BlogPost>("BlogPosts/1"); BlogPost dupe = Mapper.Map<BlogPost>(post); // dupe.Id == null RavenSession.Store(dupe); // dupe.Id == "BlogPosts/2"
  45. 45. Why duplicate? • User Duplicate function (duh) • Auditing/History – Posts/1/History/yyyyMMddHHmmss – Consider Versioning bundle for auditing EVERY document modification • Preview – Posts/1/Preview/ecbe7b49c3de4c8394880bdd81eeae97 • Use Expiration Bundle • Complex Edit/Publish/Cancel – Posts/1/Edit/davidboike • Use Expiration Bundle
  46. 46. Duplication: Lessons • Thinking about duplication can help to identify units of change, but can’t get us all the way • Make use of semantic IDs • Duplicating data enables many advanced scenarios • Think about possible document growth • Break down large documents within one unit of change into slimmer documents
  47. 47. NServiceBus Saga Storage Hey remember that book I mentioned?
  48. 48. NServiceBus/Sagas 101 • NServiceBus – Framework for creating distributed systems – Transactional message handlers w/ auto-retry – Publish/Subscribe • NServiceBus Sagas – Correlated message handlers with shared state – Transactional state stored between messages – Similar in concept to ASP.NET Session State
  49. 49. Saga Storage Interface public interface ISagaPersister { T Get<T>(Guid sagaId); T Get<T>(string property, object value); void Save(IContainSagaData saga); void Update(IContainSagaData saga); void Complete(IContainSagaData saga); } public interface IContainSagaData { Guid Id { get; set; } string Originator { get; set; } string OriginalMessageId { get; set; } }
  50. 50. Saga Storage History • NServiceBus 2.0 – SQL Storage via Fluent NHibernate • It sucked – Built my own based on XmlSerializer • That also sucked, just not as much • NServiceBus 3.0 – RavenDB Saga Persister became default – Villagers rejoiced!
  51. 51. Fluent NHibernate public class MySagaData : IContainSagaData { public virtual int MessagesReceived { get; set; } // Don't touch! NServiceBus uses these internally! public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } } MySagaData Id Originator OriginalMessageId MessagesReceived
  52. 52. Fluent NHibernate public class MySagaData : IContainSagaData { public virtual int MessagesReceived { get; set; } // Don't touch! NServiceBus uses these internally! public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } } MySagaData Id Originator OriginalMessageId MessagesReceived
  53. 53. Fluent NHibernate public class MySagaData : BaseSagaData { public virtual int MessagesReceived { get; set; } } public abstract class BaseSagaData : IContainSagaData { public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } } MySagaData Id MessagesReceived BaseSagaData Id Originator OriginalMessageId
  54. 54. Fluent NHibernate public class MySagaData : IContainSagaData { public virtual List<string> MessageTypesReceived { get; set; } // Don't touch! public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } }
  55. 55. Fluent NHibernate public class MySagaData : IContainSagaData { public virtual List<string> MessageTypesReceived { get; set; } // Don't touch! public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } }
  56. 56. Fluent NHibernate public class MySagaData : IContainSagaData { public virtual List<ReceivedMessage> MessageTypesReceived { get; set; } // Don't touch! public virtual Guid Id { get; set; } public virtual string Originator { get; set; } public virtual string OriginalMessageId { get; set; } } public class ReceivedMessage { public virtual Guid Id { get; set; } public virtual string MessageTypeReceived { get; set; } } MySagaData Id Originator OriginalMessageId ReceivedMessage Id MySagaDataId MessageTypeReceived
  57. 57. Raven Saga Persistence public class MySagaData : ContainSagaData { public List<string> MessageTypesReceived { get; set; } } -OR- public class MySagaData : ContainSagaData { public HashSet<Type> MessageTypesReceived { get; set; } }
  58. 58. Back to ISagaPersister public interface ISagaPersister { T Get<T>(Guid sagaId); T Get<T>(string property, object value); void Save(IContainSagaData saga); void Update(IContainSagaData saga); void Complete(IContainSagaData saga); } Need to be able to load a saga by any identifiable property that matches on the incoming message and saga data.
  59. 59. More Real-Life Saga Shipping Saga: • Waits for OrderAccepted + OrderBilled events • Publishes OrderShipped event public class ShippingData : ContainSagaData { [Unique] public long OrderId { get; set; } public List<string> MessageTypesReceived { get; set; } } UniqueAttribute • NHibernate persister would create a unique index on the column • RavenDB indexes are asynchronous and NOT consistent – Consistency only guaranteed on Store/Load by ID
  60. 60. Pointer Documents // PubSub.Shipping.ShippingData/OrderId/{GUID-1} { "SagaId": "{GUID-2}", "UniqueValue": 12345, "SagaDocId": “ShippingData/{GUID-2}" } // ShippingData/{GUID-2} { "OrderId": 12345, "Originator": "PubSub.Sales@ILM-LION", "OriginalMessageId": "{GUID-3}" }
  61. 61. OpenID/OAuth • Login with multiple 3rd-party identities also requires pointer document modeling • OpenID identity URLs make for horrible document IDs – Use the SHA1 hash of the login provider and login identifier in the document ID instead. • Easy way with MVC5 – RavenDB.AspNet.Identity NuGet package – https://github.com/ILMServices/RavenDB.AspNet.Identity – At least look at the source and make fun of the author (That’s me)
  62. 62. RavenDB.AspNet.Identity { // ApplicationUsers/DavidBoike (Some boring properties removed) "UserName": "DavidBoike", "Logins": [ { "LoginProvider": "Google", "ProviderKey": "https://www.google.com/accounts/o8/id?id=AItOawnt..." } ] } { // IdentityUserLogins/95c8f50aa725d62af9c7e397ca3a89889dd76514 "UserId": "ApplicationUsers/DavidBoike", "Provider": "Google", "ProviderKey": "https://www.google.com/accounts/o8/id?id=AItOawnt..." }
  63. 63. NServiceBus: Lessons • Document databases are ideal for storing random objects with little friction • Unique constraints must be treated specially to ensure consistency – Custom pointer documents – Unique Constraints bundle for more generic functionality (though I prefer not to overuse it) • Use RavenDB.AspNet.Identity for OpenID – Better yet send me a pull request!
  64. 64. Final Thoughts Document modeling is not the same as SQL. If you try to make it so, you will fail. So don’t be that guy.
  65. 65. Final Thoughts Above all else, think about how and why data changes. – Units of Change – Transactional Boundaries – Everything else is window dressing
  66. 66. Final Thoughts Like relational modeling, document modeling is a skill that is acquired through experience – Model things like you would in the real world – Don’t be afraid of trying and failing – Create a /FakeData/Create action in your site to bootstrap your database to make this easy – Especially in the early stages of an application, experiment often and regenerate when needed
  67. 67. Questions? @DavidBoike make-awesome.com

×