-
Notifications
You must be signed in to change notification settings - Fork 25
Migration Guide
This wiki page will guide you through the process of migrating between different versions of Zoom. Not all versions will come with a new guide, only those that break backwards compatibility in a non-trivial way. Zoom uses semantic versioning, but as of now has not yet it version 1.0, so there are no backwards compatibility promises. After Zoom hits version 1.0, backwards compatibility will not be broken between minor releases.
Zoom version 0.9.0 introduces some major breaking changes. The main reason for these changes is improving and simplifying syntax, but there have also been some breaking changes that fix bugs, especially changes around string indexes. Here is an overview of them:
- Relationship support has been removed.
- Registering a Model now returns a *ModelType which you must assign to a variable.
- All the basic operations (Save, FindById, Delete, etc.) have been refactored and are now methods on *ModelType. Some have also been renamed.
- Query constructors are now a method on *ModelType, and some finisher methods have been removed.
- Values of indexed string fields must not contain the DEL or NULL ASCII characters.
- The Configuration struct now has a Password field, which replaces the old way of specifying a password with a special url.
- Transactions are exported and replace functionality of the old MSave and MFindById functions.
Relationship support has been completely removed in 0.9.0. However, in its stead Transactions are now exported. It is possible to use Transactions to cover a lot of the same functionality and build relationships for yourself. The old implementation was buggy, but handling relationships manually helps remove the bugs and the leaky abstraction layer. Note that in the near future, Zoom will offer callback functions such as BeforeSave and AfterFind which will make it even easier to manage relationships on your own. If relationships are important to your application, it might be best to wait for callback support before migrating.
Here's an example of how you might manage relationships using a Transaction. Say you have a Person model and a Person can own a pet:
type Person struct {
Name string
PetId string
Pet *Pet `redis:"-"` // Tell Zoom not to store this field in Redis. We'll handle it manually.
zoom.DefaultData
}
type Pet struct {
Name string
AnimalType string
zoom.DefaultData
}
Now we can define a SavePersonWithPet function which will preserve the relationship:
func SavePersonWithPet (person *Person) error {
t := zoom.NewTransaction()
if person.Pet != nil {
// Save the pet model and update the PetId field
t.Save(Pets, person.Pet)
person.PetId = person.Pet.Id()
}
t.Save(Persons, person)
// Execute the transaction to make all the saves atomically
if err := t.Exec(); err != nil {
return err
}
return nil
}
Now we'll need to write a corresponding function for finding a Person and their Pet (if any). In this example, we're going to sacrifice atomicity for the simplicity that comes with writing the relationship code in go.
func FindPersonWithPet (id string, person *Person) error {
if err := Persons.Find(id, person); err != nil {
return err
}
if person.PetId != "" {
// Find the corresponding Pet model and scan its values into person.Pet
if err := Pets.Find(person.PetId, person.Pet); err != nil {
return err
}
}
return nil
}
If you wanted to preserve atomicity, you would need to write a lua script and execute the script as part of a transaction. Future versions of Zoom will export commonly used ReplyHandlers to make it easier to scan a response from a lua script or Redis command into a model, and therefore easier to preserve atomicity with custom commands. If atomicity is important to you, you might want to wait to migrate.
For one-to-many or many-to-many relationships, you would need to use a slice of strings called PetIds instead of just a single string for the PetId. Zoom is perfectly capable of saving a slice of strings as a field inside a struct, so all you have to do is iterate through those ids and find the corresponding models (it's best to do it in a single transaction).
The way you register models has changed. Now, the Register and RegisterName methods return a *ModelType which you should assign to a variable. *ModelType has all the methods such as Save, Find, and Delete, that used to be top-level functions. The reason for this change is that you no longer have to use strings (which are prone to typos and cannot be checked by the compiler) to identify model types.
Here's the new way to register model types:
Persons, err := Register(&Person{})
if err != nil {
// handle error
}
Convention is to name the *ModelType the plural of the corresponding type, but it's just a variable so you can name it whatever you want. We'll see how *ModelType is used in the next section.
All the basic operations such as FindById and Delete are now methods on *ModelType, and some of the names have changed. For example, the old code to find a model by its id looked like this:
model, err := zoom.FindById("Person", "a_valid_person_id")
if err != nil {
// handle error
}
// Type assert model to *Person
person, ok := model.(*Person)
if !ok {
// report an error
}
The new code would look like this:
person := &Person{}
if err := Persons.Find("a_valid_person_id", person); err != nil {
// handle error
}
Note that the old FindById function has been removed and that the new Find method behaves like the old ScanById function. This allows you to avoid a type assertion and simplifies code. See the documentation for a full list of the methods on *ModelType or the README for examples of how to use them.
The syntax for queries is mostly unchanged, but there are two differences. First, the constructor for a new Query is now a method on *ModelType. So whereas old code would look like zoom.NewQuery("Person")
, the new code would be Persons.NewQuery()
. The motivation for this change is the same as the motivation for making other functions methods on *ModelType, you get to avoid using a string as an identifier for the model type, which helps prevent typos that the compiler can't catch.
The second change is that the old finisher method called Run, which returned an interface and required type casting has been removed. The new Run method behaves like the old Scan. The IdsOnly method has also been renamed to simply Ids.
Check out the documentation for Queries for any other minor changes that might not have been mentioned here.
The implementation for string indexes has been changed to fix subtle bugs. In older versions of Zoom, a space was used as a separator for string indexes, so values with spaces in them may have caused problems. In version 0.9.0, the separator has been changed to the NULL ASCII character. The DEL character was and still is used as a suffix for certain queries. As a result, string index values must not contain the NULL or DEL characters. If they do, the results of queries may be incorrect.
In older version of Zoom, you could connect to a password protected database by providing a special url of the form redis://user:pass@host:123
. Version 0.9.0 no longer supports this, and instead you should set the Password field of the Configuration struct that you pass to zoom.Init.
In version 0.9.0, the Transaction type is exported. It replaces the functionality of the old methods with the "M" prefix. As an example, old code to find multiple models at the same time may have looked like this:
models := []*Person{}
ids := []string{"1", "2", "3"}
if err := zoom.MScanById(ids, &models); err != nil {
// handle error
}
The new code would look like this:
ids := []string{"1", "2", "3"}
models := make(*Person, len(ids))
t := NewTransaction()
for i, id := range ids {
t.Find(id, models[i])
}
if err := t.Exec(); err != nil {
// handle error
}
Transactions are much more flexible than what is shown here. You can combine different types of commands, as well as issue commands manually or invoke custom lua scripts. For more information about Transactions, check out the documentation.