Been awhile huh? Yeah, I've been busy making a living. Blogging doesn't pay much!
So I got myself a palm pre and actually started listening to podcasts and heard one on object databases. So I thought I'd give it a shot. I was about to head out of country for a few days with my wife, but knew I'd have about 3 or 4 hours of plane rides to play. So I downloaded DB40.
Tutorial was easy enough, so I figured I'd get right too it. Essentially you get a Factory (Db40Factory) that you can call methods on that'll return you IObjectContainer that's essentially a Transaction. IObjectContainer has Save(Object), QueryByExample(Object) and Delete(Object). It has some other nifty stuff, but that'll solve 90% of the problems you come across. The beauty of it all is that there is NO mapping code since it's just serializing the objects. NONE! I love me some Fluent NHibernate and with the AutoMapping stuff, it's becoming less and less painful, but still. It ain't free, and it doesn't directly contribute to the problems of the domain, so anything I can do to lesson the non problem solving code, I'll try.
Anyway, here goes...
I use the following Interface for all my Domain Entities.
1 namespace Ellemy.Business
2 {
3 /// <summary>
4 /// Any Object that has an Id
5 /// </summary>
6 public interface IEntity<IdType>
7 {
8 /// <summary>
9 /// The Id of the Entity
10 /// </summary>
11 IdType Id { get; }
12 }
13 }
Most of my Repositories follow the same pattern so I made a quick Generic Repository interface that will take any Domain object that is an IEntity.
1 using System;
2 using System.Collections.Generic;
3 using Ellemy.Business;
4
5 namespace Ellemy.Data
6 {
7 public interface IRepository<ENTITY_TYPE, ID_TYPE> where ENTITY_TYPE : IEntity<ID_TYPE>
8 {
9 /// <summary>
10 /// Gets the <see cref="ENTITY_TYPE"/> with the given <see cref="IEntity{IdType}.Id"/>
11 /// </summary>
12 /// <param name="id"></param>
13 /// <returns></returns>
14 ENTITY_TYPE Get(ID_TYPE id);
15
16 /// <summary>
17 /// Saves the Entity
18 /// </summary>
19 /// <param name="entity"></param>
20 /// <returns></returns>
21 ENTITY_TYPE Save(ENTITY_TYPE entity);
22
23 /// <summary>
24 /// Returns objects that have the properties matching the object sent
25 /// </summary>
26 /// <param name="prototype">The object to match</param>
27 /// <returns></returns>
28 IEnumerable<ENTITY_TYPE> GetByExample(ENTITY_TYPE prototype);
29
30 /// <summary>
31 /// Returns objects that have properties matching the object sent
32 /// </summary>
33 /// <param name="protoType"></param>
34 /// <param name="sortExpression"></param>
35 /// <returns></returns>
36 IEnumerable<ENTITY_TYPE> GetByExample(ENTITY_TYPE protoType, Comparison<ENTITY_TYPE> sortExpression);
37
38 /// <summary>
39 /// Returns all instances of <see cref="ENTITY_TYPE"/> in the Database
40 /// </summary>
41 /// <returns></returns>
42 IEnumerable<ENTITY_TYPE> GetAll();
43
44 /// <summary>
45 /// Returns all instances of <see cref="ENTITY_TYPE"/> in the Database
46 /// </summary>
47 /// <param name="sortExpression"></param>
48 /// <returns></returns>
49 IEnumerable<ENTITY_TYPE> GetAll(Comparison<ENTITY_TYPE> sortExpression);
50
51 /// <summary>
52 /// Removes the item from the Database
53 /// </summary>
54 /// <param name="entity"></param>
55 void Delete(ENTITY_TYPE entity);
56 }
57 }
Next came a quick Test fixture for the repo I was going to write.
10 [TestFixture]
11 public class Inheriting_Db4oRepository
12 {
13 private ProductRepository repository;
14
15 private void Act()
16 {
17 repository = new ProductRepository();
18 }
19 [Test]
20 public void the_save_method_will_save_the_object()
21 {
22 var productId = Guid.NewGuid();
23 var product = new Product {Id = productId};
24 Act();
25 repository.Save(product);
26 repository.Close();
27
28 var db = Db4oFactory.OpenFile("DB");
29 Assert.IsNotNull(db.QueryByExample(product));
30 db.Close();
31
32
33
34 }
This is how I'll want it to work... Of course even the test doesn't yet compile. Here's the abstract class.
6 namespace Ellemy.Data
7 {
8 /// <summary>
9 /// A base repository
10 /// </summary>
11 /// <typeparam name="ENTITY_TYPE"></typeparam>
12 /// <typeparam name="ID_TYPE"></typeparam>
13 public abstract class Db4oRepository<ENTITY_TYPE, ID_TYPE> :
14 IDisposable,IRepository<ENTITY_TYPE,ID_TYPE>
15 where ENTITY_TYPE : IEntity<ID_TYPE>
16 {
17 private readonly Func<ID_TYPE, ENTITY_TYPE> newInstance;
18
19 protected Db4oRepository(IObjectContainer db, Func<ID_TYPE, ENTITY_TYPE> newInstance)
20 {
21 this.newInstance = newInstance;
22 Db = db;
23 }
The Function field here is to allow us to account for the fact that IEntity does NOT have a setter for Id (most domain entities probably don't want one since they'll create it via a ctor or some other method. I usually have on that takes the Id. I made a silly little class for test consumption. Here it is.
1 using System;
2 using Ellemy.Business;
3
4 namespace Ellemy.IntegrationTests.Data
5 {
6 /// <summary>
7 /// A Product
8 /// </summary>
9 public class Product : IEntity<Guid>
10 {
11 /// <summary>
12 /// The Id
13 /// </summary>
14 public Guid Id
15 {
16 get;
17 set;
18
19 }
20 /// <summary>
21 /// The Name of the Product
22 /// </summary>
23 public String Name { get; set; }
24 /// <summary>
25 /// An identifier for the Product, must be unique in the system
26 /// </summary>
27 public String Code { get; set; }
28 }
29 }
Now that we've got the class we want to persist, let's make that ProductRepository the test refers to.
167 public class ProductRepository:Db4oRepository<Product,Guid>
168 {
169 public ProductRepository():this(Db4oFactory.OpenFile("DB"),id => new Product {Id = id}){}
170 public ProductRepository(IObjectContainer db, Func<Guid, Product> newInstance) : base(db, newInstance)
171 {
172
173 }
174
175
176 }
Note the Db4oFactory.OpenFile() call, that's the simpliest way to create an instance of an IObjectContext with Db4o. I'll post a quick server I wrote soon. Point is, the Repo doesn't care how it gets it, I just needs one. I eventually used StructureMap for the IOC on this, but we'll get there eventually. Also note the Lambda for the Product where it sets the Id. It'll be used eventually for Querying. To be totally honest, it didn't start there. True to emergent design principles, I didn't add it until I got to the query by example stuff, but I'm writing this blog after the fact :).
Ok, well, now we've got a failing unit test, but our code compiles... Let's go make Save() work!
Hold your breath here, it's gotta be tough to persist some entity right?
56 /// <summary>
57 /// Saves the Entity
58 /// </summary>
59 /// <param name="entity"></param>
60 /// <returns></returns>
61 public ENTITY_TYPE Save(ENTITY_TYPE entity)
62 {
63 Db.Store(entity);
64 Db.Commit();
65 return entity;
66 }
35 [Test]
36 public void the_Get_method_will_retrieve_an_object_by_id()
37 {
38 var productId = Guid.NewGuid();
39 var newProduct = new Product {Id = productId};
40 var db = Db4oFactory.OpenFile("DB");
41 db.Store(newProduct);
42 db.Close();
43
44
45 Act();
46
47 var product = repository.Get(productId);
48 Assert.AreEqual(productId,product.Id);
49 }
So in this test, we use Db4o to store a Product to the Db, and user our Repo to retrieve it.
Let's go make it pass.
Once again, the simplicity is amazing!
44 /// <summary>
45 /// Gets the <see cref="ENTITY_TYPE"/> with the given <see cref="IEntity{IdType}.Id"/>
46 /// </summary>
47 /// <param name="id"></param>
48 /// <returns></returns>
49 public ENTITY_TYPE Get(ID_TYPE id)
50 {
51 var example = newInstance.Invoke(id);
52 var result = (ENTITY_TYPE) Db.QueryByExample(example)[0];
53 return result;
54 }
So we take the newInstance Func, and invoke it to get an instance of the object we're querying for. Db4o does all the work for us. All we've gotta do is the cast!
Next test was the Query by example. This code is pretty much the same.
50 [Test]
51 public void GetByExample_returns_objects_that_match()
52 {
53 string name = SaveProduct();
54 Act();
55
56 var productsFromRepo = repository.GetByExample(new Product {Name = name});
57 var productFound = 0;
58 foreach (var productFromRepo in productsFromRepo)
59 {
60 Assert.AreEqual(name, productFromRepo.Name);
61 productFound++;
62 }
63 Assert.GreaterOrEqual(productFound,1,"Did not find the product");
64 }
81 private static string SaveProduct()
82 {
83 var name = "The Product";
84 var id = Guid.NewGuid();
85 var product = new Product {Id = id, Name = name};
86 var db = Db4oFactory.OpenFile("DB");
87 db.Store(product);
88 db.Close();
89 return name;
90 }
And the code that makes it pass.
77 /// <summary>
78 /// Returns objects that have the properties matching the object sent
79 /// </summary>
80 /// <param name="prototype">The object to match</param>
81 /// <returns></returns>
82 public IEnumerable<ENTITY_TYPE> GetByExample(ENTITY_TYPE prototype)
83 {
84 foreach (var entity in Db.QueryByExample(prototype))
85 {
86 yield return (ENTITY_TYPE) entity;
87 }
88 }
Ok, so now, lets make Delete work.
The test?
119 [Test]
120 public void Delete_removes_the_object_from_the_db()
121 {
122 var id = Guid.NewGuid();
123 var product = new Product {Id = id};
124 var db = Db4oFactory.OpenFile("DB");
125 db.Store(product);
126 db.Close();
127 db.Dispose();
128
129 Act();
130 var p = repository.Get(id);
131 repository.Delete(p);
132 repository.Close();
133
134 db = Db4oFactory.OpenFile("DB");
135 var r = db.QueryByExample(product);
136 Assert.IsEmpty(r);
137 db.Close();
138 db.Dispose();
139 }
As you can imagine, the code is ridiculously simple.
124 /// <summary>
125 /// Removes the item from the Database
126 /// </summary>
127 /// <param name="entity"></param>
128 public void Delete(ENTITY_TYPE entity)
129 {
130 Db.Delete(entity);
131 Db.Commit();
132 }
I then added some sorting, but I won't post the code here, It just took a Comparision<T> and applied it before returning the results.
So, what about related objects? How do those work? I wasn't totally sure, so I created a "Family" object that had a Husband, Wife, and IEnumerable<Person> Children property.
Here's the code for 'em.
113 public class Person:IEntity<Guid>
114 {
115 public Person()
116 {
117 Id = Guid.NewGuid();
118 }
119 public Guid Id
120 {
121 get;set;
122 }
123 public string Name { get; set; }
124 }
125
126 public class Family:IEntity<Guid>
127 {
128 public Family()
129 {
130 Id = Guid.NewGuid();
131 }
132 public Person Husband { get; set; }
133 public Person Wife { get; set; }
134 public IEnumerable<Person> Children { get; set; }
135 public Guid Id
136 {
137 get; set;
138 }
139 }
Then I made 2 concrete Db4o Repos.
98 public class PersonRepository:Db4oRepository<Person,Guid>{
99 public PersonRepository():this(Db4oFactory.OpenFile("DB"),x => new Person{Id = x}){}
100 private PersonRepository(IObjectContainer db, Func<Guid, Person> newInstance) : base(db, newInstance)
101 {
102 }
103 }
104
105 public class FamilyRepository:Db4oRepository<Family,Guid>
106 {
107 public FamilyRepository():this(Db4oFactory.OpenFile("DB"),x => new Family{Id = x}){}
108 private FamilyRepository(IObjectContainer db, Func<Guid, Family> newInstance) : base(db, newInstance)
109 {
110 }
111 }
No new production code, I just wanted to explore behavior, so I wrote this.
21 [Test]
22 public void Nested_objects_are_updated()
23 {
24 var family = TheOHaraFamily();
25 var repository = new FamilyRepository();
26 repository.Save(family);
27 repository.Close();
28
29 var personRepo = new PersonRepository();
30 var elliott = personRepo.Get(family.Husband.Id);
31 elliott.Name = "Elliott M O'Hara";
32 personRepo.Save(elliott);
33 personRepo.Close();
34
35 repository = new FamilyRepository();
36 var f1 = repository.Get(family.Id);
37 repository.Close();
38 Assert.AreEqual(elliott.Name,f1.Husband.Name);
39
40 }
57 private static Family TheOHaraFamily()
58 {
59 var family = new Family
60 {
61 Husband = new Person {Name = "Elliott O'Hara"},
62 Wife = new Person {Name = "Heather O'Hara"},
63 Children = new List<Person> {new Person {Name = "Parker O'Hara"}}
64 };
65 return family;
66 }
It passes!!
What about if I delete the O'Hara family, does it kill me? I HOPE not! I mean, I understand "until death do you part", but I don't think the Family repo should.
41 [Test]
42 public void child_objects_are_not_deleted()
43 {
44 var family = TheOHaraFamily();
45 var repo = new FamilyRepository();
46 repo.Save(family);
47 repo.Delete(family);
48 repo.Close();
49
50 var personRepo = new PersonRepository();
51 var heather = personRepo.Get(family.Wife.Id);
52 personRepo.Close();
53 Assert.IsNotNull(heather);
54
55
56 }
So this test creates 3 people via the Family, but then delete the Family instance and checks that the wife still exists.... It too passes!
Anyway, I encourage you to go download db4o and give it a shot. Feel free to rip off my repo if you'd like. Here's the code as txt.
Freakin' awesome! Why the hell aren't we all using db4o? (simple is beatiful)
ReplyDeleteDB4O is pretty sweet. I have had some issues getting it to run correctly in an ASP.Net shared hosting environment, but when you can get the environment right it saves a lot of time and effort.
ReplyDeleteOf course, it makes most DBAs shake in their boots -- "WHAT!? There's no MSSQL Server!".
If you are building an open source object oriented application, use DB4O and save yourself a lot of repetative mapper code. If you are building a commercial application, I would suggest you give them a call to talk about their licensing stuff. They are very easy to talk to and thier much pricing is reasonable.
Mark Ewer
Chief Architect, Stick Figure Software
@Mark I wrote a little stand alone server. That solves the shared hosting environment stuff, you just have to communicate through ports that the environment lets you.
ReplyDeleteSomeone just needs to set up a shared Db4o hosting service. It wouldn't be hard, it would work great and it would solve a lot of problems.
Maybe that would be a great project. Ideas forming....
How much would you pay for a hosted object db??? ;)
I don't think you need a GUID. That's more of a relational database thing. Objects have identity, so just use the object.
ReplyDeleteThey've got Entity if you provide it. I chose an Id as a way to do that because it's convent.
ReplyDeleteI'd love to see some way without Id (or the like) to treat an Entity as a non-value type... I can't think of anything. Feel free to show me :)
Hi Elliott,
ReplyDeleteVery good article, I have a small suggestion/question.
Why not force IdType : IComparable<IdType>
this will transform Get into this:
ENTITY_TYPE Get(ID_TYPE id)
{
ENTITY_TYPE result = _db.Query<ENTITY_TYPE>(p => p.Id.CompareTo(id) == 0).SingleOrDefault();
return result;
}
The id's must be comparable anyway and we avoid using the ugly function new....
How about it ?
Hmmm.. Interesting...
ReplyDeleteI think I like it!
Thanks!
You should also check out McObject’s Perst, an open source, object-oriented embedded database. Versions are available for .NET (including .NET Compact Framework and Silverlight), Java (including Android) and Java ME (for those BlackBerry midlets). More info on the Perst embedded database - http://www.mcobject.com/perst
ReplyDeleteCheck out Eloquera Database ( www.eloquera.com ).
ReplyDeleteEloquera is a native .NET object database and completely FREE for the commercial use.
Save with a single line of code.
Supports SQL and LINQ queries.
and can do many other things...