Monday, September 7, 2009

Meddling with Object Databases (Db4o).

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 }

Seriously, that's all there is to it... Add a property? No changes needed, it just "works"... Yep the test passes! Sorry people, but that's freekin AWESOME!
Ok enough salivating. Let's write another test.

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.