You are on page 1of 8

040.050.

Queries and Filters


Queries and filters are where Sterling truly shines. Both keys and indexes are stored in memory and exposed as lists that you can run LINQ to Object queries against. The actual objects are lazily loaded, so that access to disk actually doesn't happen until you need the full object. An example scenario for querying is a contact list. While a contact may contain addresses, names, phone numbers, and other information, often you may only search by name. When you define an index with name, the names are stored in memory and can be queried using LINQ. Once you have selected a contact, you can then access the value from disk and load the remaining fields.

Querying Keys The simplest query in Sterling is the key query. This query provides you with a list of type TableKey which contains two values: the value of the key, and a LazyValue that references the instance. The LazyValue will only load the instance from disk when it is explicitly accessed. Whenever you specify two values in the Query, you are implying to Sterling you wish to get a list of key values back. The TableKey structure looks like this:

As long as you don't access the LazyValue.Value property on the key, you will always be working with in the inmemory copy of the key collection. Accessing the nested property will result in the instance being loaded from isolated storage. In this way you can filter your queries and only access the full instances you are looking for. In the following example, while the entire key list is queried, only the instances with a key value of 4 and 5 will be returned from isolated storage: var range = from k in _databaseInstance.Query<TestModel, int>() where k.Key > 3 && k.Key < 6 orderby k.Key select k.LazyValue.Value; Caching Sterling automatically caches loaded classes in the key collection (and indexes indirectly refer to the key collection). The cache is not aware of changes made locally in your application. Instead, the cache tracks save operations. The key collection for a class contains a lazy-loaded value. The first time this value is referenced, the instance is loaded from disk. This results in the lazy value being populated with the instance. Any subsequent load will return the same instance. Saving or deleting the instance will result in the lazy value being reset. This will clear the value from cache, and the next load will again retrieve the instance from disk. Therefore, load guarantees you will always obtain the most recently saved value (with the exception noted below), but will avoid redundant disk access when the value has not been updated since the last access. If you wish to force a load from disk and bypass the query cache, simply retrieve the key from the query and call load directly when d. Loading Instances. Caching Side Effects

Keep in mind this may cause unwanted side effects. If you modify the loaded value without saving it, other queries will access the same instance. Typically, a class will not be modified unless it is for a subsequent save operation. If you are loading a class for editing and will not persist the class until an edit is confirmed, it is recommended that you copy the loaded instance to another model or load the class directly (not from the query). This side-effect will only occur if you have multiple areas of the application accessing the same class simultaneously. This case should be the exception due to the nature of Silverlight as a client technology. An application user will not typically access multiple copies of the same class simultaneously. This behavior is intentional because the database exists solely on the client computer and is accessed by a single user at a time. The safe pattern to follow is to use the classes from queries for all read-only views. Any time an object will be edited, load the object explicitly first. To load a single instance from cache (will be loaded from isolated storage the first time, or any time after a save operation has been issued) use this format (assume key contains the id of the instance): var item = database.Query<MyClass,int>().Where(q=>q.Key.Equals(key).FirstOrDefault(); Querying Indexes Querying indexes works the same way as querying keys. Remember that the generic always begins with the class type and ends with the key type. When querying indexes, you simply specify the index name. It is a good practice to provide the index names as constants on the database object to access them consistently throughout your application. Whenever you specify three or four values and pass a string to Query you are requesting an index. TheTableIndex returned looks like this:

The following example returns a list of key values that are sorted by the field "data" using an index. These key values can then be used to load the instance on demand when needed: var descending = from k in _databaseInstance.Query<TestModel, string, int>(TestDatabaseInstance.DATAINDEX) orderby k.Index descending select k.Key; This is the index definition: .WithIndex<TestModel, string, int>(DATAINDEX, t => t.Data) Complex Queries LINQ to Objects is a very powerful query engine and allows for almost unlimited variety in the queries that are possible. The key is to restrict the queries and joins to values that are on indexes and keys so the sorts and filters will take place entirely in memory. You can then access the underlying values that are the result of the filter and use those to load the instances from isolated storage.

In this example that is shipped with the Sterling source, a class contains a list of nutrients. The query joins to an index of definitions for the nutrients, then joins to another table with the nutrient amounts, and returns a list of nutrient descriptions that contain amounts, descriptions, and units of measure all populated from the index values (therefore, nothing will have to access isolated storage on disk). Notice this allows for joins, ordering, and arranging the results in a new type. return from n in CurrentFoodDescription.Nutrients join nd in SterlingService.Current.Database.Query<NutrientDefinition, string, string, int>( FoodDatabase.NUTR_DEFINITION_UNITS_DESC) on n.NutrientDefinitionId equals nd.Key join nd2 in SterlingService.Current.Database.Query<NutrientDefinition, int, int>( FoodDatabase.NUTR_DEFINITION_SORT) on nd.Key equals nd2.Key orderby nd2.Index select new NutrientDescription { Amount = n.AmountPerHundredGrams, Description = nd.Index.Item2, UnitOfMeasure = nd.Index.Item1 };

040.060. Delete, Truncate, and Purge


There are several ways to remove instances of data, clear tables, and even purge entire databases with the Sterling engine. The b. Database Backup and Restore can be used to ensure data is not permanently lost. You may also want to read the 7. Sterling Recipes for examples of creating pseudo-transactions.

Deleting Individual Instances To delete an individual instance, you can pass either an instance or a key to the engine: // delete by instance var actual = _databaseInstance.Load<TestModel>(key); _databaseInstance.Delete(actual); // delete by key _databaseInstance.Delete(typeof(TestModel), key); You can receive notification before an instance is deleted (and even prevent deletion by throwing an exception) by using a trigger. Learn more by reading about g. Triggers and Interceptors. Truncating the Entire Table To truncate the entire table, which essentially deletes all instances and flushes both indexes and keys, simply pass the type of the table to the database engine like this:

_databaseInstance.Truncate(typeof(TestModel)); Note that truncate bypasses all triggers. Purge the Entire Database The Sterling tests must purge the entire database each time to start with a "clean slate." The command to purge a database is simple (and irreversible if you have not generated a backup): _databaseInstance.Purge(); The database will be usable after the purge, but will, of course, be empty until you save a new instance to it.

040.070. Triggers and Interceptors


Saving and loading instances is great, but often there are additional steps you would like to take. For example, wouldn't it be nice if your class that has a "dirty flag" just to keep track of saving was automatically reset once the object was saved? Auto-generation of identifiers is also important for databases as is being able to save a class without a key and then having the database generate one for you. Additional concerns include compression and encryption of the data being saved.

Sterling addresses these concerns through triggers and interceptors. There is an important difference between the two. Triggers operate on instances (qualified types) and are called before save, after save, and before delete.Interceptors operate on the serialized buffer of data. They take a byte buffer and supply a modified buffer that presumably has been compressed, encrypted, or otherwise manipulated through a reversible transformation. Interceptors are called both after serializing the instance to a member but before persisting to disk, and after loading from disk but before deserializing into an instance.

Triggers

All triggers inherit directly from BaseSterlingTrigger<TInstanceType, TKeyType> and are similar to the table definition in that the generic type is closed with the type of instance being saved and the type of the key for the instance.

The trigger base class requires three methods to be overridden: BeforeSave, AfterSave, and BeforeDelete.

The before save method returns a boolean. This allows you to perform any validation checks, and if the instance should not be saved, you may return false. This will prevent Sterling from saving the object and also raise aSterlingTriggerException (this can be caught and ignored or processed as you see fit). Returning true will continue with the save. The method is passed the instance, so you are free to manipulate the instance as needed before the save takes place.

The after save method is void. It simply passes the instance for manipulation after it has been saved but before it is returned to the calling code. This is where you might reset a dirty flag, for example.

The before delete method simply passes the key to the trigger, and returns a boolean. Return true to allow the delete, or false to prevent the delete.

The following code example shows a custom trigger. The trigger will prevent saving any instance with an id of 5, and deleting any instance with an id of 99. If the instance has an id of 0 or less, it will generate a next-up integer and effectively auto-assign the key before saving. For a better implementation of this that can be reused across types, read the 7. Sterling Recipes.

Trigger Definition

public class TriggerClassTestTrigger : BaseSterlingTrigger<TriggerClass, int> { public const int BADSAVE = 5; public const int BADDELETE = 99; private int _nextKey; public TriggerClassTestTrigger(int nextKey) { _nextKey = nextKey; } public override bool BeforeSave(TriggerClass instance) { if (instance.Id == BADSAVE) return false; if (instance.Id > 0) return true; instance.Id = _nextKey++; return true;

} public override void AfterSave(TriggerClass instance) { instance.IsDirty = false; } public override bool BeforeDelete(int key) { return key != BADDELETE; } }
Triggers can be registered and unregistered on the fly. Global triggers should be registered as part of the database activation process and do not need to be unregistered. You may have certain conditions that require a trigger, and can wrap those conditions by registering the trigger and then unregistering it when complete.

The following code grabs the highest key in the database, then creates an instance of the trigger class, passing the key into the constructor, and registers the instance:

var nextKey = _databaseInstance.Query<TriggerClass, int>().Any() ? (from keys in _databaseInstance.Query<TriggerClass, int>() select keys.Key).Max() + 1 : 1;

_databaseInstance.RegisterTrigger(new TriggerClassTestTrigger(nextKey));

To unregister a trigger, call UnRegisterTrigger with the trigger instance (in the example above, you would create the trigger and assign it to a variable, then pass the variable to the register method as a reference to pass to unregister later).

Interceptors

Interceptors manipulate byte streams and can be used for compression, encryption, or other types of manipulations. All interceptors derive from BaseSterlingByteInterceptor and overload methods: Save and Load. Both methods receive a byte buffer and return a byte buffer.

The following interceptor performs an exclusive-or operation on the buffer, effectively flipping one of the bits.

Remember these operations must be reversible, so the same operation is performed on load. In the example of compression or encryption, the save would compress and/or encrypt, and the load would decompress and/or decrypt.

Example Byte Interceptor

01 public class ByteInterceptor : BaseSterlingByteInterceptor 02 { 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 } } } return retVal; override public byte[] Load(byte[] sourceStream) { var retVal = new byte[sourceStream.Length]; for (var x = 0; x < sourceStream.Length; x++) { retVal[x] = (byte)(sourceStream[x] ^ 0x80); // xor } } return retVal; override public byte[] Save(byte[] sourceStream) { var retVal = new byte[sourceStream.Length]; for (var x = 0; x < sourceStream.Length; x++) { retVal[x] = (byte)(sourceStream[x] ^ 0x80); // xor

Multiple interceptors can be registered. Sterling will automatically call them in the order they are registered for save, and in the reverse order for load. If you register a compression then an encryption, Sterling will compress, then encrypt, then save the stream, and upon load will load the stream, decrypt, then decompress before attempting to deserialize the instance.

Interceptors are registered by type, and can be registered and unregistered at any point (so, like triggers, you can isolate certain operations to use interceptors if and when needed). The following code registers the interceptor:

_databaseInstance.RegisterInterceptor<ByteInterceptor>();

The following code unregisters the interceptor:

_databaseInstance.UnRegisterInterceptor<ByteInterceptor>();

There are many third-party encryption and compression libraries available. To reduce the Sterling footprint, Sterling does not ship these directly but instead provides the appropriate hooks for you to have the flexibility to include them in your own project. Keep in mind the additional overhead will come with a performance cost. For less sensitive data, often simple exclusive-or operations on a buffer can be a fast and efficient way to obfuscate on local disk and make it difficult to read by casual users.