| Well, I've been fooling
(fooling precedes serious study, which can precede serious use) with
Bamboo Prevalence some more and so far I'm pretty impressed. If you aren't
familiar with this piece of work, I'd suggest you visit my first
article here, read up and look over the links, and then come back
here.
As a side note, Stephan Meyn has been working on his "XSD
Migration Kit" for
BP, "Bamboo Builder", and his latest iteration now supports foreign
keys. If you are interested in a tool that will take DataSets that you
can drop onto a form, and use CodeDom to generate complete Bamboo Prevalence
- compliant classes including a Loader that will get all your data out
of your favorite database and save it as a snapshot from the BP engine,
visit him at http://www.users.bigpond.net.au/meyn/Bamboo/.
N.B. 10/31/2003: Stephan upped a new
article about his stuff on the codeproject.com
site here.
I've been encouraging Stephan to put it out open source
in some way, as its a concept that I believe has some significant uses
especially in migrating apps. This is really the key to creating "standalone" distributable
apps that include their own data store that is completely independent
of any need to have a database on the user's machine. You just distribute
the populated Bamboo Prevalence snapshot file, and your users are "good
to go"! Now if somebody will just port the BinaryFormatter (all
66 classes enums and structs!) to the Compact Framework, you are ready
to do Pocket PC apps with BP too. That would be nice. [Author's note: see
N.B. below about CompactFormatter]
What I did here was take another
look at what types of applications Bamboo Prevalence can be used for,
without getting myself all involved in a whole big project. Most people
who look at BP and are reluctant to begin using it are either still too "hung
up" on abandoning their
SQL as a way to manipulate their business objects, or else they make
the somewhat fallacious claim that saving a snapshot of your data's
state to the hard drive is too "risky". I say "fallacious"
-- because this is exactly what SQL Server and other RDBMS's do- they
save your data to files on the hard drive. So, far I'm convinced that
BP does an excellent job of what it's supposed to do persistence-wise;
that is, to log all the actions that would need to be replayed to recreate
state faithfully and quickly to the persistence medium as advertised.
In addition, you can exert as much or as little (e.g. TransparentPrevalence
attribute) control over this process as you desire.
Now I'm not anti-database
at all, I've spent a lot of time learning to work with Oracle, SQL
Server, and others. But I really like the idea of representing your data
with OOP-oriented classes and logic. It's much more intuitive, and lends
to a cleaner business logic design pattern than always having to rely
on ADO.NET and some sort of data access layer.
The only other issue I've seen come up is that
developers say if they have a database that's really big, they're not
too happy with the idea of storing 100% of it in memory. However, I believe
that getting a huge increase in speed as well as being able to work
with types (objects) that represent your business logic ( rather than
SQL - oriented database and ADO.NET stuff) are benefits that should
encourage developers to come up with strategies for doing partial loads
from their regular RDBMS and so on, into the persistence engine. Since
the project is open source and well - documented, enterprising coders
should not find it too difficult to cobble on their own custom "snapshot
takers" and so on. I'd like to see some example work in this area,
so if you're a Bamboo Prevalence afficionado, please share it with us.
Finally, the Bamboo Prevalence classes are organized
into nicely planned pieces. You can implement the IRecord interface,
and this will give you an entree into the Collections, Indexing, FullText
and XPathObjectNavigator classes that provide a rich set of methods
for querying, searching, sorting and comparing your objects. It's a paradigm
shift to be sure, but the benefits to independent thinkers who like
this concept could be significant. Now let's take a look at my basic
objects for implementing a web - based Blogging Engine (I decided to
call it BAMBlog, for lack of anything more imaginative -- please
forgive...).
First, your basic "Blog" item, a-la Bamboo Prevalence.
Note that the only unusual things that may be seen here are that the
class is marked with SerializableAttribute (required for the persistence
engine) and that I've implemented IComparer, along with a little
ReverserClass. This allows me to sort my entries by date descending
so that the most recent entries appear first in the blog display.
using System;
using System.Diagnostics;
using System.Collections;
using Bamboo.Prevalence.Indexing;
namespace BlogLibrary
{
public class ReverserClass : IComparer
{
int IComparer.Compare( Object x, Object y )
{
return( (new CaseInsensitiveComparer()).Compare( y, x ) );
}
}
[Serializable ]
public class Blog : IComparable
{
private Guid mID;
private string mUser;
private string mPassword;
private DateTime mBlog_Date;
private string mBlog_Title;
private string mBlog_Summary;
private string mBlog_Text;
private bool mEncrypted;
private DateTime mLast_Updated;
public string ID{get{return mID.ToString();}}
public string User{get{return mUser;}}
public string Password{get{return mPassword;}}
public DateTime Blog_Date{get{return mBlog_Date;}}
public string Blog_Title {get{return mBlog_Title;}}
public string Blog_Summary{get{return mBlog_Summary;}}
public string Blog_Text
{
get{return mBlog_Text;}
set{mBlog_Text=value;}
}
public bool Encrypted{get{return mEncrypted;}}
public DateTime Last_Updated{get{return mLast_Updated;}} public int CompareTo(object obj)
{
if(obj is Blog)
{
Blog temp = (Blog) obj;
return this.mBlog_Date.CompareTo(temp.mBlog_Date);
}
throw new ArgumentException("object is not a Blog");
}
internal Blog(System.Guid ID,string User,string Password, DateTime Blog_Date,
string Blog_Title, string Blog_Summary, string Blog_Text,
bool Encrypted,DateTime Last_Updated)
{
mID=ID;
mUser = User;
mPassword=Password;
mBlog_Date =Blog_Date;
mBlog_Title=Blog_Title;
mBlog_Summary =Blog_Summary;
mBlog_Text=Blog_Text;
mEncrypted=Encrypted;
mLast_Updated=Last_Updated;
}
}
}
|
OK, now we move to the BlogManager class, which handles all the details
of a collection of our little "BAMBlogs":
using System;
using System.Collections;
using System.IO;
using Bamboo.Prevalence;
using Bamboo.Prevalence.Attributes;
using Bamboo.Prevalence.Util;
using System.Web;
using System.Diagnostics;
using Bamboo.Prevalence.Indexing;
namespace BlogLibrary
{
[Serializable,TransparentPrevalence]
public sealed class BlogManager : MarshalByRefObject,IRecord
{
static private readonly PrevalenceEngine mEngine;
static public readonly BlogManager Instance = new BlogManager();
static BlogManager()
{
string path=HttpContext.Current.Server.MapPath(".");
mEngine = PrevalenceActivator.CreateTransparentEngine(typeof(BlogManager), path);
Instance = mEngine.PrevalentSystem as BlogManager;
SnapshotTaker taker = new SnapshotTaker(mEngine,TimeSpan.FromHours(1),
Bamboo.Prevalence.Util.CleanUpAllFilesPolicy.Default);
}
private ArrayList mBlogs = new ArrayList();
// IRecord Interface implementation (not used yet)--
public object this[string strID]
{
get
{
int idx=mBlogs.BinarySearch(strID);
if(idx <0)
throw new ArgumentException("Item not found");
return (Blog)mBlogs[idx];
}
}
public Blog CreateBlog(System.Guid ID,
string User,string Password, DateTime Blog_Date, string Blog_Title, string Blog_Summary, string Blog_Text, bool Encrypted,DateTime Last_Updated)
{
Blog blog = new Blog(ID, User, Password, Blog_Date, Blog_Title,Blog_Summary, Blog_Text,Encrypted,Last_Updated);
mBlogs.Add(blog);
return blog;
}
[Query]
public Blog[] GetAllBlogs( )
{
// sort by most recent Date desc via IComparer
IComparer myComparer = new ReverserClass();
mBlogs.Sort( myComparer );
return (Blog[])mBlogs.ToArray(typeof(Blog));
}
[Query]
public Blog[] GetAllBlogs(string strKey)
{
IComparer myComparer = new ReverserClass();
mBlogs.Sort( myComparer );
return (Blog[])mBlogs.ToArray(typeof(Blog));
}
[Query]
public Blog[] GetBlogsBySearchString(string srchString)
{
if(srchString=="") // no search term, send back everything
{
IComparer myComparer1 = new ReverserClass();
mBlogs.Sort( myComparer1 );
return (Blog[])mBlogs.ToArray(typeof(Blog));
}
ArrayList returnedBlogs = new ArrayList();
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.Blog_Summary.ToUpper().IndexOf(srchString.ToUpper())!=-1 || testBlog.Blog_Text.ToUpper().IndexOf(srchString.ToUpper())!=-1)
returnedBlogs.Add(testBlog);
}
IComparer myComparer = new ReverserClass();
returnedBlogs.Sort( myComparer );
return (Blog[])returnedBlogs.ToArray(typeof(Blog));
}
[Query]
public Blog[] GetBlogsByDateRange(DateTime startDate, DateTime endDate)
{
ArrayList returnedBlogs = new ArrayList();
mBlogs.Sort();
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.Blog_Date>=startDate && testBlog.Blog_Date<=endDate && testBlog.Blog_Text!="")
returnedBlogs.Add(testBlog);
}
IComparer myComparer = new ReverserClass();
returnedBlogs.Sort( myComparer );
Debug.WriteLine("Count: " + returnedBlogs.Count.ToString());
return (Blog[])returnedBlogs.ToArray(typeof(Blog));
}
public bool CreateNewUser(string userName, string password)
{
if (!AuthenticateUser(userName,password))
{
Guid gooid =Guid.NewGuid();
CreateBlog(gooid,userName,password,DateTime.Now,userName + "'s Blog",""," ",
false,DateTime.Now);
return true;
}
else
{
return false;
}
}
[Query]
public Blog[] GetBlogByID(string strID)
{
ArrayList returnedBlogs = new ArrayList();
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.ID.ToUpper()==strID.ToUpper() )
returnedBlogs.Add(testBlog);
}
return (Blog[])returnedBlogs.ToArray(typeof(Blog));
}
[Query]
public Blog[] GetBlogByUser(string userName)
{
ArrayList returnedBlogs = new ArrayList();
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.User.ToUpper()==userName.ToUpper() )
returnedBlogs.Add(testBlog);
}
IComparer myComparer = new ReverserClass();
returnedBlogs.Sort( myComparer );
return (Blog[])returnedBlogs.ToArray(typeof(Blog));
}
[Query]
public bool AuthenticateUser(string userName,string password)
{
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.User==userName && testBlog.Password ==password )
return true;
}
return false;
}
public Blog UpdateBlog( string strID,string User, string Password,DateTime Blog_Date, string Blog_Title,string Blog_Summary, string Blog_Text, bool Encrypted,DateTime Last_Updated)
{
Blog blog=null;
ArrayList returnedBlogs = new ArrayList();
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.ID==strID )
{
mBlogs.RemoveAt(i);
Guid gooid = Guid.NewGuid();
blog = new Blog(gooid,User,Password,Blog_Date, Blog_Title,Blog_Summary,Blog_Text, Encrypted,Last_Updated);
mBlogs.Add(blog);
}
}
return blog;
}
public void DeleteBlog(string strID)
{
for(int i=0;i<mBlogs.Count;i++)
{
Blog testBlog = (Blog)mBlogs[i];
if(testBlog.ID==strID)
{
mBlogs.RemoveAt(i);
}
}
}
public void TakeSnapshot()
{
mEngine.TakeSnapshot();
}
}
} |
In particular, take note of this code:
SnapshotTaker taker = new SnapshotTaker(mEngine,TimeSpan.FromHours(1),
Bamboo.Prevalence.Util.CleanUpAllFilesPolicy.Default);
--which tells the engine to take a full
snapshot and automatically clean up all old unneccessary .snapshot and
.commandLog files once per hour, which it faithfully does. That's really
overkill for a web app, because the engine can use the commandlogs from
each session to completely restore the state. A more realistic one would
be say, once every 12 hours. This just keeps the size down, and prevents
having a large collection of files. You can see the other methods are
all basically to get blogs by date, search string, and so on. I have
a field "Encrypted"
that I intended to use for private blogs but I decided that was overkill.
You can see that in order to keep the complexity
down, I've put the username and password in as fields for each Blog item.
This serves a dual pupose; I want to display the user with each blog
item in the UI, and I also wanted to provide an easy method for authentication
without having to add another class and write a bunch of pseudo "JOIN"
code. So my "AuthenticateUser" method becomes quite simple,
yet very functional:
[Query]
public bool AuthenticateUser(string
userName,string password)
{
for(int i=0;i<mBlogs.Count;i++)
{
Blog
testBlog = (Blog)mBlogs[i];
if(testBlog.User==userName && testBlog.Password
==password )
return true; }
return false;
}
This allows us to create a multi-user
blog where visitors can see all the blog items, but users can log in
and be able to add and edit only their own items. I haven't spent a lot
of time on this, so it's still a bit rough around the edges, but it's
been a lot of fun and very instructional in learning how to use the Bamboo
Prevalence Engine.
The rest of the code mostly deals with the
UI - Webforms and codebehind for display (2 kinds - a DataGrid view and
a more classic "Blog
View"), search, date range, LogIn and New User, Edit blog, Add Blog,
and so on. You can try
it out here: I'd be interested in reader feedback on this one - comments,
new ideas and whatever. Feel free to post ideas and comments to our forums
here.
N.B. Angelo Scotto has
put together what he calls the CompactFormatter .
The CompactFormatter is a very lean (24K) set of classes that mimic the
BinaryFormatter and is 100% Compact Framework compatible. To use CompactFormatter
with Bamboo Prevalence, all you need to do is replace all references
to BinaryFormatter with CompactFormatter, and disambiguate
the [Serializable] attributes by setting a "using yo=Serialization.Formatters"
and then [yo.Serializable] wherever there is a [Serializable] decoration. Finally,
you'll need to supply a parameterless constructor for any classes that do not
have one, as the CompactFormatter requires this to load and interrogate the
class via Reflection. I've wired this up as described, and it works great. Sweet!
There is another problem, however, in porting
Bamboo Prevalence to the Compact Framework, and that is the fact that
BP in it's current iteration requires the use of the Remoting namespaces,
which aren't present in the Compact Framework. Until some enterprising
soul comes up with an alternative way to handle the creation of the BP
persistence engine for CF, that pretty much does in any chance of successfully
porting Bamboo Prevalence to Compact Framework.
Download the Source Code that accompanies this article |