{"id":13413685,"url":"https://github.com/albrow/zoom","last_synced_at":"2025-03-14T19:32:59.544Z","repository":{"id":47120686,"uuid":"11463545","full_name":"albrow/zoom","owner":"albrow","description":"A blazing-fast datastore and querying engine for Go built on Redis.","archived":false,"fork":false,"pushed_at":"2023-02-02T20:22:29.000Z","size":933,"stargazers_count":309,"open_issues_count":2,"forks_count":25,"subscribers_count":21,"default_branch":"master","last_synced_at":"2024-09-30T23:13:04.873Z","etag":null,"topics":["go","redis"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/albrow.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2013-07-17T00:32:34.000Z","updated_at":"2024-09-10T05:23:25.000Z","dependencies_parsed_at":"2023-02-18T00:15:52.229Z","dependency_job_id":null,"html_url":"https://github.com/albrow/zoom","commit_stats":null,"previous_names":[],"tags_count":45,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albrow%2Fzoom","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albrow%2Fzoom/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albrow%2Fzoom/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/albrow%2Fzoom/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/albrow","download_url":"https://codeload.github.com/albrow/zoom/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243635695,"owners_count":20322979,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","redis"],"created_at":"2024-07-30T20:01:46.418Z","updated_at":"2025-03-14T19:32:59.200Z","avatar_url":"https://github.com/albrow.png","language":"Go","readme":"Zoom\n====\n\n[![Version](https://img.shields.io/badge/version-0.18.0-5272B4.svg)](https://github.com/albrow/zoom/releases)\n[![Circle CI](https://img.shields.io/circleci/project/albrow/zoom/master.svg)](https://circleci.com/gh/albrow/zoom/tree/master)\n[![GoDoc](https://godoc.org/github.com/albrow/zoom?status.svg)](https://godoc.org/github.com/albrow/zoom)\n\nA blazing-fast datastore and querying engine for Go built on Redis.\n\nRequires Redis version \u003e= 2.8.9 and Go version \u003e= 1.2. The latest version of\nboth is recommended.\n\nFull documentation is available on\n[godoc.org](http://godoc.org/github.com/albrow/zoom).\n\n\nTable of Contents\n-----------------\n\n\u003c!-- toc --\u003e\n\n- [Development Status](#development-status)\n- [When is Zoom a Good Fit?](#when-is-zoom-a-good-fit)\n- [Installation](#installation)\n- [Initialization](#initialization)\n- [Models](#models)\n  * [What is a Model?](#what-is-a-model)\n  * [Customizing Field Names](#customizing-field-names)\n  * [Creating Collections](#creating-collections)\n  * [Saving Models](#saving-models)\n  * [Updating Models](#updating-models)\n  * [Finding a Single Model](#finding-a-single-model)\n  * [Finding Only Certain Fields](#finding-only-certain-fields)\n  * [Finding All Models](#finding-all-models)\n  * [Deleting Models](#deleting-models)\n  * [Counting the Number of Models](#counting-the-number-of-models)\n- [Transactions](#transactions)\n- [Queries](#queries)\n  * [The Query Object](#the-query-object)\n  * [Using Query Modifiers](#using-query-modifiers)\n  * [A Note About String Indexes](#a-note-about-string-indexes)\n- [More Information](#more-information)\n  * [Persistence](#persistence)\n  * [Atomicity](#atomicity)\n  * [Concurrent Updates and Optimistic Locking](#concurrent-updates-and-optimistic-locking)\n- [Testing \u0026 Benchmarking](#testing--benchmarking)\n  * [Running the Tests](#running-the-tests)\n  * [Running the Benchmarks](#running-the-benchmarks)\n- [Contributing](#contributing)\n- [Example Usage](#example-usage)\n- [License](#license)\n\n\u003c!-- tocstop --\u003e\n\nDevelopment Status\n------------------\n\nZoom was first started in 2013. It is well-tested and going forward the API\nwill be relatively stable. However, it is not actively maintained and there\nare some known performance issues with queries that use more than one filter.\n\nAt this time, Zoom can be considered safe for use in low-traffic production\napplications. However, I would recommend that you look at more actively\nmaintained alternatives.\n\nZoom follows semantic versioning, but offers no guarantees of backwards\ncompatibility until version 1.0. You can also keep an eye on the\n[Releases page](https://github.com/albrow/zoom/releases) to see a full changelog\nfor each release. In addition, starting with version 0.9.0,\n[migration guides](https://github.com/albrow/zoom/wiki/Migration-Guide) will be\nprovided for any non-trivial breaking changes, making it easier to stay up to\ndate with the latest version.\n\n\nWhen is Zoom a Good Fit?\n------------------------\n\nZoom might be a good fit if:\n\n1. **You are building a low-latency application.** Because Zoom is built on top of\n\tRedis and all data is stored in memory, it is typically much faster than datastores/ORMs\n\tbased on traditional SQL databases. Latency will be the most noticeable difference, although\n\tthroughput may also be improved.\n2. **You want more out of Redis.** Zoom offers a number of features that you don't get\n\tby using a Redis driver directly. For example, Zoom supports a larger number of types\n\tout of the box (including custom types, slices, maps, complex types, and embedded structs),\n\tprovides tools for making multi-command transactions easier, and of course, provides the\n\tability to run queries.\n3. **You want an easy-to-use datastore.** Zoom has a simple API and is arguably easier to\n\tuse than some ORMs. For example, it doesn't require database migrations and instead builds\n\tup a schema based on your struct types. Zoom also does not typically require any knowledge\n\tof Redis in order to use effectively. Just connect it to a database and you're good to go!\n\nZoom might ***not*** be a good fit if:\n\n1. **You are working with a lot of data.** Redis is an in-memory database, and Zoom does not\n\tyet support sharding or Redis Cluster. Memory could be a hard constraint for larger applications.\n\tKeep in mind that it is possible (if expensive) to run Redis on machines with up to 256GB of memory\n\ton cloud providers such as Amazon EC2.\n2. **You need advanced queries.** Zoom currently only provides support for basic queries and is\n\tnot as powerful or flexible as something like SQL. For example, Zoom currently lacks the\n\tequivalent of the `IN` or `OR` SQL keywords. See the\n\t[documentation](http://godoc.org/github.com/albrow/zoom/#Query) for a full list of the types\n\tof queries supported.\n\n\nInstallation\n------------\n\nZoom is powered by Redis and needs to connect to a Redis database. You can install Redis on the same\nmachine that Zoom runs on, connect to a remote database, or even use a Redis-as-a-service provider such\nas Redis To Go, RedisLabs, Google Cloud Redis, or Amazon Elasticache.\n\nIf you need to install Redis, see the [installation instructions](http://redis.io/download) on the official\nRedis website.\n\nTo install Zoom itself, run `go get -u github.com/albrow/zoom` to pull down the\ncurrent master branch, or install with the dependency manager of your choice to\nlock in a specific version.\n\n\nInitialization\n--------------\n\nFirst, add github.com/albrow/zoom to your import statement:\n\n``` go\nimport (\n\t // ...\n\t github.com/albrow/zoom\n)\n```\n\nThen, you must create a new pool with \n[`NewPool`](http://godoc.org/github.com/albrow/zoom/#NewPool). A pool represents\na pool of connections to the database. Since you may need access to the pool in\ndifferent parts of your application, it is sometimes a good idea to declare a\ntop-level variable and then initialize it in the `main` or `init` function. You\nmust also call `pool.Close` when your application exits, so it's a good idea to\nuse defer.\n\n``` go\nvar pool *zoom.Pool\n\nfunc main() {\n\tpool = zoom.NewPool(\"localhost:6379\")\n\tdefer func() {\n\t\tif err := pool.Close(); err != nil {\n\t\t\t// handle error\n\t\t}\n\t}()\n\t// ...\n}\n```\n\nThe `NewPool` function accepts an address which will be used to connect to\nRedis, and it will use all the\n[default values](http://godoc.org/github.com/albrow/zoom/#DefaultPoolOptions)\nfor the other options. If you need to specify different options, you can use the\n[`NewPoolWithOptions`](http://godoc.org/github.com/albrow/zoom/#NewPoolWithOptions)\nfunction.\n\nFor convenience, the\n[`PoolOptions`](http://godoc.org/github.com/albrow/zoom/#PoolOptions) type has\nchainable methods for changing each option. Typically you would start with\n[`DefaultOptions`](http://godoc.org/github.com/albrow/zoom/#DefaultOptions) and\ncall `WithX` to change value for option `X`.\n\nFor example, here's how you could initialize a Pool that connects to Redis using\na unix socket connection on `/tmp/unix.sock`:\n\n``` go\noptions := zoom.DefaultPoolOptions.WithNetwork(\"unix\").WithAddress(\"/tmp/unix.sock\")\npool = zoom.NewPoolWithOptions(options)\n```\n\n\nModels\n------\n\n### What is a Model?\n\nModels in Zoom are just structs which implement the `zoom.Model` interface:\n\n``` go\ntype Model interface {\n  ModelID() string\n  SetModelID(string)\n}\n```\n\nTo clarify, all you have to do to implement the `Model` interface is add a getter and setter\nfor a unique id property.\n\nIf you want, you can embed `zoom.RandomID` to give your model all the\nrequired methods. A struct with `zoom.RandomID` embedded will generate a pseudo-random id for itself\nthe first time the `ModelID` method is called iff it does not already have an id. The pseudo-randomly\ngenerated id consists of the current UTC unix time with second precision, an incremented atomic\ncounter, a unique machine identifier, and an additional random string of characters. With ids generated\nthis way collisions are extremely unlikely.\n\nFuture versions of Zoom may provide additional id implementations out of the box, e.g. one that assigns\nauto-incremented ids. You are also free to write your own id implementation as long as it satisfies the\ninterface.\n\nA struct definition serves as a sort of schema for your model. Here's an example of a model for a person:\n\n``` go\ntype Person struct {\n\t Name string\n\t Age  int\n\t zoom.RandomID\n}\n```\n\nBecause of the way Zoom uses reflection, all the fields you want to save need to be exported.\nUnexported fields (including unexported embedded structs with exported fields) will not\nbe saved. This is a departure from how the  encoding/json and  encoding/xml packages\nbehave. See [issue #25](https://github.com/albrow/zoom/issues/25) for discussion.\n\nAlmost any type of field is supported, including custom types, slices, maps, complex types,\nand embedded structs. The only things that are not supported are recursive data structures and\nfunctions.\n\n### Customizing Field Names\n\nYou can change the name used to store the field in Redis with the `redis:\"\u003cname\u003e\"` struct tag. So\nfor example, if you wanted the fields to be stored as lowercase fields in Redis, you could use the\nfollowing struct definition:\n\n``` go\ntype Person struct {\n\t Name string    `redis:\"name\"`\n\t Age  int       `redis:\"age\"`\n\t zoom.RandomID\n}\n```\n\nIf you don't want a field to be saved in Redis at all, you can use the special struct tag `redis:\"-\"`.\n\n### Creating Collections\n\nYou must create a `Collection` for each type of model you want to save. A\n`Collection` is simply a set of all models of a specific type and has methods\nfor saving, finding, deleting, and querying those models. `NewCollection`\nexamines the type of a model and uses reflection to build up an internal schema.\nYou only need to call `NewCollection` once per type. Each pool keeps track of\nits own collections, so if you wish to share a model type between two or more\npools, you will need to create a collection for each pool.\n\n``` go\n// Create a new collection for the Person type.\nPeople, err := pool.NewCollection(\u0026Person{})\nif err != nil {\n\t // handle error\n}\n```\n\n\nThe convention is to name the `Collection` the plural of the corresponding\nmodel type (e.g. \"People\"), but it's just a variable so you can name it\nwhatever you want.\n\n`NewCollection` will use all the\n[default options](http://godoc.org/github.com/albrow/zoom/#DefaultCollectionOptions)\nfor the collection.\n\nIf you need to specify other options, use the\n[`NewCollectionWithOptions`](http://godoc.org/github.com/albrow/zoom/#NewCollectionWithOptions)\nfunction. The second argument to `NewCollectionWithOptions` is a\n[`CollectionOptions`](http://godoc.org/github.com/albrow/zoom#CollectionOptions).\nIt works similarly to `PoolOptions`, so you can start with\n[`DefaultCollectionOptions`](http://godoc.org/github.com/albrow/zoom/#DefaultCollectionOptions)\nand use the chainable `WithX` methods to specify a new value for option `X`.\n\nHere's an example of how to create a new `Collection` which is indexed, allowing\nyou to use Queries and methods like `FindAll` which rely on collection indexing:\n\n``` go\noptions := zoom.DefaultCollectionOptions.WithIndex(true)\nPeople, err = pool.NewCollectionWithOptions(\u0026Person{}, options)\nif err != nil {\n\t// handle error\n}\n```\n\nThere are a few important points to emphasize concerning collections:\n\n1. The collection name cannot contain a colon.\n2. Queries, as well as the `FindAll`, `DeleteAll`, and `Count` methods will not\n\twork if `Index` is `false`. This may change in future versions.\n\nIf you need to access a `Collection` in different parts of\nyour application, it is sometimes a good idea to declare a top-level variable\nand then initialize it in the `init` function:\n\n```go\nvar (\n\tPeople *zoom.Collection\n)\n\nfunc init() {\n\tvar err error\n\t// Assuming pool and Person are already defined.\n\tPeople, err = pool.NewCollection(\u0026Person{})\n\tif err != nil {\n\t\t// handle error\n\t}\n}\n```\n\n\n### Saving Models\n\nContinuing from the previous example, to persistently save a `Person` model to\nthe database, we use the `People.Save` method. Recall that in this example,\n\"People\" is just the name we gave to the `Collection` which corresponds to the\nmodel type `Person`.\n\n``` go\np := \u0026Person{Name: \"Alice\", Age: 27}\nif err := People.Save(p); err != nil {\n\t // handle error\n}\n```\n\nWhen you call `Save`, Zoom converts all the fields of the model into a format\nsuitable for Redis and stores them as a Redis hash. There is a wiki page\ndescribing\n[how zoom works under the hood](https://github.com/albrow/zoom/wiki/Under-the-Hood) in more detail.\n\n### Updating Models\n\nSometimes, it is preferable to only update certain fields of the model instead\nof saving them all again. It is more efficient and in some scenarios can allow\nsafer simultaneous changes to the same model (as long as no two clients update\nthe same field at the same time). In such cases, you can use `UpdateFields`.\n\n``` go\nif err := People.UpdateFields([]string{\"Name\"}, person); err != nil {\n\t// handle error\n}\n```\n\n`UpdateFields` uses \"last write wins\" semantics, so if another caller updates\nthe same field, your changes may be overwritten. That means it is not safe for\n\"read before write\" updates. See the section on\n[Concurrent Updates](#concurrent-updates-and-optimistic-locking) for more\ninformation.\n\n### Finding a Single Model\n\nTo retrieve a model by id, use the `Find` method:\n\n``` go\np := \u0026Person{}\nif err := People.Find(\"a_valid_person_id\", p); err != nil {\n\t // handle error\n}\n```\n\nThe second argument to `Find` must be a pointer to a struct which satisfies `Model`, and must have a type corresponding to\nthe `Collection`. In this case, we passed in `Person` since that is the struct type that corresponds to our `People`\ncollection. `Find` will mutate `p` by setting all its fields. Using `Find` in this way allows the caller to maintain type\nsafety and avoid type casting. If Zoom couldn't find a model of type `Person` with the given id, it will return a\n`ModelNotFoundError`.\n\n### Finding Only Certain Fields\n\nIf you only want to find certain fields in the model instead of retrieving all\nof them, you can use `FindFields`, which works similarly to `UpdateFields`.\n\n``` go\np := \u0026Person{}\nif err := People.FindFields(\"a_valid_person_id\", []string{\"Name\"}, p); err != nil {\n\t// handle error\n}\nfmt.Println(p.Name, p.Age)\n// Output:\n// Alice 0\n```\n\nFields that are not included in the given field names will not be mutated. In\nthe above example, `p.Age` is `0` because `p` was just initialized and that's\nthe zero value for the `int` type.\n\n### Finding All Models\n\nTo find all models of a given type, use the `FindAll` method:\n\n``` go\npeople := []*Person{}\nif err := People.FindAll(\u0026people); err != nil {\n\t // handle error\n}\n```\n\n`FindAll` expects a pointer to a slice of some registered type that implements `Model`. It grows or shrinks the slice as needed,\nfilling in all the fields of the elements inside of the slice. So the result of the call is that `people` will be a slice of\nall models in the `People` collection.\n\n`FindAll` only works on indexed collections. To index a collection, you need to\ninclude `Index: true` in the `CollectionOptions`.\n\n### Deleting Models\n\nTo delete a model, use the `Delete` method:\n\n``` go\n// ok will be true iff a model with the given id existed and was deleted\nif ok, err := People.Delete(\"a_valid_person_id\"); err != nil {\n\t// handle err\n}\n```\n\n`Delete` expects a valid id as an argument, and will attempt to delete the model with the given id. If there was no model\nwith the given type and id, the first return value will be false.\n\nYou can also delete all models in a collection with the `DeleteAll` method:\n\n``` go\nnumDeleted, err := People.DeleteAll()\nif err != nil {\n  // handle error\n}\n```\n\n`DeleteAll` will return the number of models that were successfully deleted.\n`DeleteAll` only works on indexed collections. To index a collection, you need\nto include `Index: true` in the `CollectionOptions`.\n\n### Counting the Number of Models\n\nYou can get the number of models in a collection using the `Count` method:\n\n``` go\ncount, err := People.Count()\nif err != nil {\n  // handle err\n}\n```\n\n`Count` only works on indexed collections. To index a collection, you need\nto include `Index: true` in the `CollectionOptions`.\n\n\nTransactions\n------------\n\nZoom exposes a transaction API which you can use to run multiple commands efficiently and atomically. Under the hood,\nZoom uses a single [Redis transaction](http://redis.io/topics/transactions) to perform all the commands in a single\nround trip. Transactions feature delayed execution, so nothing touches the database until you call `Exec`. A transaction\nalso remembers its errors to make error handling easier on the caller. The first error that occurs (if any) will be\nreturned when you call `Exec`.\n\nHere's an example of how to save two models and get the new number of models in\nthe `People` collection in a single transaction.\n\n``` go\nnumPeople := 0\nt := pool.NewTransaction()\nt.Save(People, \u0026Person{Name: \"Foo\"})\nt.Save(People, \u0026Person{Name: \"Bar\"})\n// Count expects a pointer to an integer, which it will change the value of\n// when the transaction is executed.\nt.Count(People, \u0026numPeople)\nif err := t.Exec(); err != nil {\n  // handle error\n}\n// numPeople will now equal the number of `Person` models in the database\nfmt.Println(numPeople)\n// Output:\n// 2\n```\n\nYou can execute custom Redis commands or run custom Lua scripts inside a\n[`Transaction`](http://godoc.org/github.com/albrow/zoom/#Transaction) using the\n[`Command`](http://godoc.org/github.com/albrow/zoom/#Transaction.Command) and\n[`Script`](http://godoc.org/github.com/albrow/zoom/#Transaction.Script) methods.\nBoth methods expect a\n[`ReplyHandler`](http://godoc.org/github.com/albrow/zoom/#ReplyHandler) as an\nargument. A `ReplyHandler` is simply a function that will do something with the\nreply from Redis. `ReplyHandler`'s are executed in order when you call `Exec`.\n\nRight out of the box, Zoom exports a few useful `ReplyHandler`s. These include\nhandlers for the primitive types `int`, `string`, `bool`, and `float64`, as well\nas handlers for scanning a reply into a `Model` or a slice of `Model`s. You can\nalso write your own custom `ReplyHandler`s if needed.\n\n\nQueries\n-------\n\n### The Query Object\n\nZoom provides a useful abstraction for querying the database. You create queries by using the `NewQuery`\nconstructor, where you must pass in the name corresponding to the type of model you want to query. For now,\nZoom only supports queries on a single collection at a time.\n\nYou can add one or more query modifiers to the query, such as `Order`, `Limit`, and `Filter`. These methods\nreturn the query itself, so you can chain them together. The first error (if any) that occurs due to invalid\narguments in the query modifiers will be remembered and returned when you attempt to run the query.\n\nFinally, you run the query using a query finisher method, such as `Run` or `Count`. Queries feature delayed\nexecution, so nothing touches the database until you execute the query with a finisher method.\n\n### Using Query Modifiers\n\nYou can chain a query object together with one or more different modifiers. Here's a list\nof all the available modifiers:\n\n- [`Order`](http://godoc.org/github.com/albrow/zoom/#Query.Order)\n- [`Limit`](http://godoc.org/github.com/albrow/zoom/#Query.Limit)\n- [`Offset`](http://godoc.org/github.com/albrow/zoom/#Query.Offset)\n- [`Include`](http://godoc.org/github.com/albrow/zoom/#Query.Include)\n- [`Exclude`](http://godoc.org/github.com/albrow/zoom/#Query.Exclude)\n- [`Filter`](http://godoc.org/github.com/albrow/zoom/#Query.Filter)\n\nYou can run a query with one of the following query finishers:\n\n- [`Run`](http://godoc.org/github.com/albrow/zoom/#Query.Run)\n- [`IDs`](http://godoc.org/github.com/albrow/zoom/#Query.IDs)\n- [`Count`](http://godoc.org/github.com/albrow/zoom/#Query.Count)\n- [`RunOne`](http://godoc.org/github.com/albrow/zoom/#Query.RunOne)\n\nHere's an example of a more complicated query using several modifiers:\n\n``` go\npeople := []*Person{}\nq := People.NewQuery().Order(\"-Name\").Filter(\"Age \u003e=\", 25).Limit(10)\nif err := q.Run(\u0026people); err != nil {\n\t// handle error\n}\n```\n\nFull documentation on the different modifiers and finishers is available on\n[godoc.org](http://godoc.org/github.com/albrow/zoom/#Query).\n\n### A Note About String Indexes\n\nBecause Redis does not allow you to use strings as scores for sorted sets, Zoom relies on a workaround\nto store string indexes. It uses a sorted set where all the scores are 0 and each member has the following\nformat: `value\\x00id`, where `\\x00` is the NULL character. With the string indexes stored this way, Zoom\ncan issue the ZRANGEBYLEX command and related commands to filter models by their string values. As a consequence,\nhere are some caveats to keep in mind:\n\n- Strings are sorted by ASCII value, exactly as they appear in an [ASCII table](http://www.asciitable.com/),\n  not alphabetically. This can have surprising effects, for example 'Z' is considered less than 'a'.\n- Indexed string values may not contain the NULL or DEL characters (the characters with ASCII codepoints\n  of 0 and 127 respectively). Zoom uses NULL as a separator and DEL as a suffix for range queries.\n\n\nMore Information\n----------------\n\n### Persistence\n\nZoom is as persistent as the underlying Redis database. If you intend to use Redis as a permanent\ndatastore, it is recommended that you turn on both AOF and RDB persistence options and set `fsync` to\n`everysec`. This will give you good performance while making data loss highly unlikely.\n\nIf you want greater protections against data loss, you can set `fsync` to `always`. This will hinder performance\nbut give you persistence guarantees\n[very similar to SQL databases such as PostgreSQL](http://redis.io/topics/persistence#ok-so-what-should-i-use).\n\n[Read more about Redis persistence](http://redis.io/topics/persistence)\n\n### Atomicity\n\nAll methods and functions in Zoom that touch the database do so atomically. This is accomplished using\nRedis transactions and Lua scripts when necessary. What this means is that Zoom will not\nput Redis into an inconsistent state (e.g. where indexes to not match the rest of the data).\n\nHowever, it should be noted that there is a caveat with Redis atomicity guarantees. If Redis crashes\nin the middle of a transaction or script execution, it is possible that your AOF file can become\ncorrupted. If this happens, Redis will refuse to start until the AOF file is fixed. It is relatively\neasy to fix the problem with the `redis-check-aof` tool, which will remove the partial transaction\nfrom the AOF file.\n\nIf you intend to issue Redis commands directly or run custom scripts, it is highly recommended that\nyou also make everything atomic. If you do not, Zoom can no longer guarantee that its indexes are\nconsistent. For example, if you change the value of a field which is indexed, you should also\nupdate the index for that field in the same transaction. The keys that Zoom uses for indexes\nand models are provided via the [`ModelKey`](http://godoc.org/github.com/albrow/zoom/#Collection.ModelKey),\n[`AllIndexKey`](http://godoc.org/github.com/albrow/zoom/#Collection.AllIndexKey), and\n[`FieldIndexKey`](http://godoc.org/github.com/albrow/zoom/#Collection.FieldIndexKey) methods.\n\nRead more about:\n- [Redis persistence](http://redis.io/topics/persistence)\n- [Redis scripts](http://redis.io/commands/eval)\n- [Redis transactions](http://redis.io/topics/transactions)\n\n### Concurrent Updates and Optimistic Locking\n\nZoom 0.18.0 introduced support for basic optimistic locking. You can use\noptimistic locking to safely implement concurrent \"read before write\" updates.\n\nOptimistic locking utilizes the `WATCH`, `MULTI`, and `EXEC` commands in Redis\nand only works in the context of transactions. You can use the\n[`Transaction.Watch`](https://godoc.org/github.com/albrow/zoom#Transaction.Watch)\nmethod to watch a model for changes. If the model changes after you call `Watch`\nbut before you call `Exec`, the transaction will not be executed and instead\nwill return a\n[`WatchError`](https://godoc.org/github.com/albrow/zoom#WatchError). You can\nalso use the `WatchKey` method, which functions exactly the same but operates on\nkeys instead of models.\n\nTo understand why optimistic locking is useful, consider the following code:\n\n``` go\n// likePost increments the number of likes for a post with the given id.\nfunc likePost(postID string) error {\n  // Find the Post with the given postID\n  post := \u0026Post{}\n  if err := Posts.Find(postID, post); err != nil {\n\t return err\n  }\n  // Increment the number of likes\n  post.Likes += 1\n  // Save the post\n  if err := Posts.Save(post); err != nil {\n\t return err\n  }\n}\n```\n\nThe line `post.Likes += 1` is a \"read before write\" operation. That's because\nthe `+=` operator implicitly reads the current value of `post.Likes` and then\nadds to it.\n\nThis can cause a bug if the function is called across multiple goroutines or\nmultiple machines concurrently, because the `Post` model can change in between\nthe time we retrieved it from the database with `Find` and saved it again with\n`Save`.\n\nYou can use optimistic locking to avoid this problem. Here's the revised code:\n\n```go\n// likePost increments the number of likes for a post with the given id.\nfunc likePost(postID string) error {\n  // Start a new transaction and watch the post key for changes. It's important\n  // to call Watch or WatchKey *before* finding the model.\n  tx := pool.NewTransaction()\n  if err := tx.WatchKey(Posts.ModelKey(postID)); err != nil {\n    return err\n  }\n  // Find the Post with the given postID\n  post := \u0026Post{}\n  if err := Posts.Find(postID, post); err != nil {\n\t return err\n  }\n  // Increment the number of likes\n  post.Likes += 1\n  // Save the post in a transaction\n  tx.Save(Posts, post)\n  if err := tx.Exec(); err != nil {\n  \t // If the post was modified by another goroutine or server, Exec will return\n  \t // a WatchError. You could call likePost again to retry the operation.\n    return err\n  }\n}\n```\n\nOptimistic locking is not appropriate for models which are frequently updated,\nbecause you would almost always get a `WatchError`. In fact, it's called\n\"optimistic\" locking because you are optimistically assuming that conflicts will\nbe rare. That's not always a safe assumption.\n\nDon't forget that Zoom allows you to run Redis commands directly. This\nparticular problem might be best solved by the `HINCRBY` command.\n\n```go\n// likePost atomically increments the number of likes for a post with the given\n// id and then returns the new number of likes.\nfunc likePost(postID string) (int, error) {\n\t// Get the key which is used to store the post in Redis\n\tpostKey := Posts.ModelKey(postID, post)\n\t// Start a new transaction\n\ttx := pool.NewTransaction()\n\t// Add a command to increment the number of Likes. The HINCRBY command returns\n\t// an integer which we will scan into numLikes.\n\tvar numLikes int\n\ttx.Command(\n\t\t\"HINCRBY\",\n\t\tredis.Args{postKey, \"Likes\", 1},\n\t\tzoom.NewScanIntHandler(\u0026numLikes),\n\t)\n\tif err := tx.Exec(); err != nil {\n\t\treturn 0, err\n\t}\n\treturn numLikes, nil\n}\n```\n\nFinally, if optimistic locking is not appropriate and there is no built-in Redis\ncommand that offers the functionality you need, Zoom also supports custom Lua\nscripts via the\n[`Transaction.Script`](https://godoc.org/github.com/albrow/zoom#Transaction.Script)\nmethod. Redis is single-threaded and scripts are always executed atomically, so\nyou can perform complicated updates without worrying about other clients\nchanging the database.\n\nRead more about:\n- [Redis Commands](http://redis.io/commands)\n- [Redigo](https://github.com/garyburd/redigo), the Redis Driver used by Zoom\n- [`ReplyHandler`s provided by Zoom](https://godoc.org/github.com/albrow/zoom)\n- [How Zoom works Under the Hood](https://github.com/albrow/zoom/wiki/Under-the-Hood)\n\n\nTesting \u0026 Benchmarking\n----------------------\n\n### Running the Tests\n\nTo run the tests, make sure you're in the root directory for Zoom and run:\n\n```\ngo test\n```   \n\nIf everything passes, you should see something like:\n\n```\nok    github.com/albrow/zoom  2.267s\n```\n\nIf any of the tests fail, please [open an issue](https://github.com/albrow/zoom/issues/new) and\ndescribe what happened.\n\nBy default, tests and benchmarks will run on localhost:6379 and use database #9. You can change the address,\nnetwork, and database used with flags. So to run on a unix socket at /tmp/redis.sock and use database #3,\nyou could use:\n\n```\ngo test -network=unix -address=/tmp/redis.sock -database=3\n```\n\n### Running the Benchmarks\n\nTo run the benchmarks, make sure you're in the root directory for the project and run:\n\n```\ngo test -run=none -bench .\n```   \n\nThe `-run=none` flag is optional, and just tells the test runner to skip the tests and run only the benchmarks\n(because no test function matches the pattern \"none\"). You can also use the same flags as above to change the\nnetwork, address, and database used.\n\nYou should see some runtimes for various operations. If you see an error or if the build fails, please\n[open an issue](https://github.com/albrow/zoom/issues/new).\n\nHere are the results from my laptop (2.8GHz quad-core i7 CPU, 16GB 1600MHz RAM) using a socket connection with Redis set\nto append-only mode:\n\n```\nBenchmarkConnection-8                  \t 5000000\t       318 ns/op\nBenchmarkPing-8                        \t  100000\t     15146 ns/op\nBenchmarkSet-8                         \t  100000\t     18782 ns/op\nBenchmarkGet-8                         \t  100000\t     15556 ns/op\nBenchmarkSave-8                        \t   50000\t     29307 ns/op\nBenchmarkSave100-8                     \t    3000\t    546427 ns/op\nBenchmarkFind-8                        \t   50000\t     24767 ns/op\nBenchmarkFind100-8                     \t    5000\t    374947 ns/op\nBenchmarkFindAll100-8                  \t    5000\t    383919 ns/op\nBenchmarkFindAll10000-8                \t      30\t  47267433 ns/op\nBenchmarkDelete-8                      \t   50000\t     29902 ns/op\nBenchmarkDelete100-8                   \t    3000\t    530866 ns/op\nBenchmarkDeleteAll100-8                \t    2000\t    730934 ns/op\nBenchmarkDeleteAll1000-8               \t     200\t   9185093 ns/op\nBenchmarkCount100-8                    \t  100000\t     16411 ns/op\nBenchmarkCount10000-8                  \t  100000\t     16454 ns/op\nBenchmarkQueryFilterInt1From1-8        \t   20000\t     82152 ns/op\nBenchmarkQueryFilterInt1From10-8       \t   20000\t     83816 ns/op\nBenchmarkQueryFilterInt10From100-8     \t   10000\t    144206 ns/op\nBenchmarkQueryFilterInt100From1000-8   \t    2000\t   1010463 ns/op\nBenchmarkQueryFilterString1From1-8     \t   20000\t     87347 ns/op\nBenchmarkQueryFilterString1From10-8    \t   20000\t     88031 ns/op\nBenchmarkQueryFilterString10From100-8  \t   10000\t    158968 ns/op\nBenchmarkQueryFilterString100From1000-8\t    2000\t   1088961 ns/op\nBenchmarkQueryFilterBool1From1-8       \t   20000\t     82537 ns/op\nBenchmarkQueryFilterBool1From10-8      \t   20000\t     84556 ns/op\nBenchmarkQueryFilterBool10From100-8    \t   10000\t    149463 ns/op\nBenchmarkQueryFilterBool100From1000-8  \t    2000\t   1017342 ns/op\nBenchmarkQueryOrderInt100-8            \t    3000\t    386156 ns/op\nBenchmarkQueryOrderInt10000-8          \t      30\t  50011375 ns/op\nBenchmarkQueryOrderString100-8         \t    2000\t   1004530 ns/op\nBenchmarkQueryOrderString10000-8       \t      20\t  77855970 ns/op\nBenchmarkQueryOrderBool100-8           \t    3000\t    387056 ns/op\nBenchmarkQueryOrderBool10000-8         \t      30\t  49116863 ns/op\nBenchmarkComplexQuery-8                \t   20000\t     84614 ns/op\n```\n\nThe results of these benchmarks can vary widely from system to system, and so the benchmarks\nhere are really only useful for comparing across versions of Zoom, and for identifying possible\nperformance regressions or improvements during development. You should run your own benchmarks that\nare closer to your use case to get a real sense of how Zoom will perform for you. High performance\nis one of the top priorities for this project.\n\n\nContributing\n------------\n\nSee [CONTRIBUTING.md](https://github.com/albrow/zoom/blob/master/CONTRIBUTING.md).\n\n\nExample Usage\n-------------\n\n[albrow/people](https://github.com/albrow/people) is an example HTTP/JSON API\nwhich uses the latest version of Zoom. It is a simple example that doesn't use\nall of Zoom's features, but should be good enough for understanding how Zoom can\nwork in a real application.\n\n\nLicense\n-------\n\nZoom is licensed under the MIT License. See the LICENSE file for more information.\n","funding_links":[],"categories":["Go","ORM","网络相关库","Relational Databases","\u003cspan id=\"orm\"\u003eORM\u003c/span\u003e"],"sub_categories":["HTTP Clients","Advanced Console UIs","ORM","\u003cspan id=\"高级控制台用户界面-advanced-console-uis\"\u003e高级控制台用户界面 Advanced Console UIs\u003c/span\u003e","HTTP客户端","高級控制台界面","交流","高级控制台界面"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falbrow%2Fzoom","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falbrow%2Fzoom","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falbrow%2Fzoom/lists"}