-
Notifications
You must be signed in to change notification settings - Fork 7
Introduction
- Uses ADO.NET API. Currently implemented for Sqlite. Tested with:
- System.Data.Sqlite
- Mono.Data.Sqlite
- Simple fluent mapping
- POCO entities -- no base class, no interface to implement, no attributes
- Convention-based with a few overrides
- Many-to-one associations (belongs to)
- One-to-many associations (collections):
- Loading - automatically load collections, lazily or eagerly, as specified per collection
- Cascading - automatically save/update/delete changes to collections, as specified per collection
- Filtering - selectively load items based on criteria specified per collection
- ICriteria API for building complex query predicates
- Basic session cache
- Use Linq predicates for queries and to filter collections (very limited at present)
- Create any kind of parameterized raw SQL query in code
- Basic UnitOfWork is built in
- Database migration utility
- Automatically deploy database changes based on migrations that you write (forward only)
- Very useful for embedded database scenarios
- Very lightweight (currently about 70k)
Startup might look something like this:
var sessionFactory = Fluently.Configure
.ConnectionString("Data source=MyDatabase.sqlite")
.DatabaseAdapter(DbAdapter.Sqlite)
.Domain(d =>
{
d.IdConvention(x => x.EntityType.Name + "Id")
.Access(Access.Property)
.Generator(Generator.GuidComb);
d.ListParentIdColumnNameConvention(x => x.ParentType.Name + "_id");
d.BelongsToColumnNameConvention(x => x.PropertyName + "_id");
d.Entity<Person>(e => {
e.Property(x => x.FirstName);
e.Property(x => x.LastName);
e.Property(x => x.Active);
e.Property(x => x.MemberSince);
});
d.Entity<Forum>(e => {
e.List(x => x.Posts).ParentIdColumn("PostedTo_id");
e.Property(x => x.Name);
e.Property(x => x.TimeOfDayLastUpdated);
});
d.Entity<Post>(e => {
e.Property(x => x.Title);
e.Property(x => x.Body);
e.Property(x => x.DatePosted);
e.BelongsTo(x => x.Poster).Column("Poster_id");
});
})
.Build();
UnitOfWork.Initialize(sessionFactory);
using (UnitOfWork.Start())
{
DatabaseMigrator.Execute();
}
Notice that you do not have to map the Id property for any entity; Catnap will map it automatically per convention. All other persisted properties must be mapped and must have both a getter and a setter. The getters and setters do not have to be public. Notice the conventions specified at the beginning of the domain mapping. These are optional, and if omitted Catnap's will apply default conventions.
There are some special considerations for using Catnap with MonoTouch.
The following code fetches a forum, changes its title, removes a post and adds a post, then saves the forum. The Posts collection is loaded lazily. Adds/deletes to Posts are cascaded when the forum is saved. At the start of the using block a connection is opened and a transaction started. At the end of the using block the transaction is committed (or rolled back in the case of an error) and the connection is closed.
using (var uow = UnitOfWork.Start())
{
var person = uow.Session.Get<Person>(1);
var forum = uow.Session.Get<Forum>(1);
forum.Title = "Annoying Complaints";
forum.RemovePost(forum.Posts.First());
forum.AddPost(new Post
{
Title = "Please help!",
Body "Now!",
Person = person
});
uow.Session.SaveOrUpdate(forum);
}
Criteria provides a way to specify conditions for a query. Derive from Criteria class:
public class TimeEntriesCriteria : Criteria<TimeEntry>
{
public TimeEntriesCriteria Project(int projectId)
{
return Equal(x => x.Project, projectId);
}
public TimeEntriesCriteria Billed(bool billed)
{
return Equal(x => x.Billed, billed);
}
}
Usage:
var criteria = new TimeEntriesCriteria()
.Project(projectId)
.Billed(false)
var unbilledTimeEntriesForProject = session.List(criteria);
Or build custom criteria as needed:
var criteria = Criteria.For<TimeEntry>()
.Equal(x => x.Project, projectId)
.Where(x => Date < new DateTime(2009, 1, 1);
var oldTimeEntriesForProject = session.List(criteria);
The following unit test illustrates more complex criteria:
public class when_creating_a_complex_condition
{
static ICriteria<Person> criteria;
static IDbCommandSpec target;
static ISessionFactory sessionFactory;
Establish context = () =>
{
sessionFactory = Fluently.Configure
.Domain(d =>
d.Entity<Person>(e =>
{
e.Id(x => x.Id).Access(Access.Property);
e.Property(x => x.FirstName);
e.Property(x => x.LastName);
e.Property(x => x.MemberSince);
}))
.Build();
criteria = Criteria.For<Person>()
.Less("Bar", 1000)
.GreaterOrEqual("Bar", 300)
.Or(or =>
{
or.NotEqual(x => x.FirstName, "Tim");
or.And(and =>
{
and.Equal("Foo", 25);
and.Equal("Baz", 500);
});
})
.And(and =>
{
and.LessOrEqual(x => x.MemberSince, new DateTime(2000, 1, 1));
and.Greater(x => x.MemberSince, new DateTime(1980, 1, 1));
and.Where(x => x.LastName == "Scott" || x.LastName == "Jones");
})
.Null(x => x.LastName)
.NotNull("FirstName");
};
Because of = () => target = criteria.Build(sessionFactory.New());
It should_render_correct_sql = () => target.CommandText.Should().Equal(
"((Bar < @0) and (Bar >= @1) and ((FirstName != @2) or ((Foo = @3) and (Baz = @4))) and ((MemberSince <= @5) and (MemberSince > @6) and ((LastName = @7) or (LastName = @8))) and (LastName is NULL) and (FirstName is not NULL))");
It should_contain_expected_parameters = () =>
{
target.Parameters.Should().Count.Exactly(9);
target.Parameters.Should().Contain.One(x => x.Name == "@0" && x.Value.Equals(1000));
target.Parameters.Should().Contain.One(x => x.Name == "@1" && x.Value.Equals(300));
target.Parameters.Should().Contain.One(x => x.Name == "@2" && x.Value.Equals("Tim"));
target.Parameters.Should().Contain.One(x => x.Name == "@3" && x.Value.Equals(25));
target.Parameters.Should().Contain.One(x => x.Name == "@4" && x.Value.Equals(500));
target.Parameters.Should().Contain.One(x => x.Name == "@5" && x.Value.Equals(new DateTime(2000, 1, 1)));
target.Parameters.Should().Contain.One(x => x.Name == "@6" && x.Value.Equals(new DateTime(1980, 1, 1)));
target.Parameters.Should().Contain.One(x => x.Name == "@7" && x.Value.Equals("Scott"));
target.Parameters.Should().Contain.One(x => x.Name == "@8" && x.Value.Equals("Jones"));
};
}
The criteria API is limited in that it only supports conditions on simple properties. It does not support conditions on nested properties or aggregate operations on collections.
Where Criteria leaves off, DbCommandSpec picks up. It allows you to do more complex queries and still return entities instead of raw data. For example:
var command = new DbCommandSpec()
.SetCommandText(
@"select e.* from TimeEntry e
inner join Project p on p.Id = e.ProjectId
inner join Client c on c.Id = p.ClientId
where c.Id = @clientId")
.AddParameter(clientId => id);
var timeEntries = UnitOfWork.Current.Session.List<TimeEntry>(command);
In cases where you want to fetch raw data, perhaps to populate DTOs, you can use ExecuteQuery. For example:
var command = new DbCommandSpec()
.SetCommandText(
@"select e.Id, c.Name from TimeEntry e
inner join Project p on e.ProjectId = p.Id
inner join Client c on p.ClientId = c.Id
where c.Name = @name")
.AddParameter("@name", "Acme")
var timeEntries = UnitOfWork.Current.Session.ExecuteQuery(command);
timeEntries
is an IEnumerable<IDictionary<string, object>>
, each item of which is a row. Each item in the dictionary is a key-value pair with Key being the projection name and Value being the value.
var command = new DbCommandSpec()
.SetCommandText("create table Foo (Bar varchar(200))");
UnitOfWork.Current.Session.ExecuteNonQuery(command);
var command = new DbCommandSpec()
.SetCommandText("select count(*) from Foo where Bar = 'bar'");
int count = UnitOfWork.Current.Session.ExecuteScalar<int>(command);
- Automated tests. Catnap started as an experimental project that has evolved into something useful. While it was developed with loose coupling and testability in mind, TDD/BDD was not used. Getting the code under test will be necessary to allow for fluid change and growth.
- Unit tests, lots of unit tests.
- More integration tests.
- Session cache handle cascading. To cascade we must retrieve the entire persisted collection in order to synchronize changes. Therefore, cascading is currently only appropriate for collections of a small size.
- Lazy loading of BelongsTo (many-to-one) properties.
- Create adapters for other ADO.NET providers. This might require some work to the core code to support differences in SQL dialect.