{"id":13483763,"url":"https://github.com/soveran/ohm-crystal","last_synced_at":"2025-12-30T01:08:21.698Z","repository":{"id":53563357,"uuid":"59414711","full_name":"soveran/ohm-crystal","owner":"soveran","description":"Ohm for Crystal","archived":false,"fork":false,"pushed_at":"2022-01-10T09:43:31.000Z","size":33,"stargazers_count":70,"open_issues_count":4,"forks_count":9,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-03-22T15:50:01.328Z","etag":null,"topics":["lesscode","ohm","redis"],"latest_commit_sha":null,"homepage":null,"language":"Crystal","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/soveran.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING","funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-05-22T13:48:38.000Z","updated_at":"2024-09-04T13:23:00.000Z","dependencies_parsed_at":"2022-09-15T17:01:13.505Z","dependency_job_id":null,"html_url":"https://github.com/soveran/ohm-crystal","commit_stats":null,"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fohm-crystal","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fohm-crystal/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fohm-crystal/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/soveran%2Fohm-crystal/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/soveran","download_url":"https://codeload.github.com/soveran/ohm-crystal/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245871682,"owners_count":20686246,"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":["lesscode","ohm","redis"],"created_at":"2024-07-31T17:01:15.055Z","updated_at":"2025-12-30T01:08:21.658Z","avatar_url":"https://github.com/soveran.png","language":"Crystal","funding_links":[],"categories":["ORM/ODM Extensions"],"sub_categories":[],"readme":"Ohm ॐ\n=====\n\nObject-hash mapping library for Redis.\n\n![CI](https://github.com/soveran/ohm-crystal/workflows/Crystal%20CI/badge.svg)\n\nDescription\n-----------\n\nOhm is a library for storing objects in [Redis][redis], a persistent key-value\ndatabase. It has very good performance.\n\nCommunity\n---------\n\nMeet us on IRC: [#ohm](irc://chat.freenode.net/#ohm) on\n[freenode.net](http://freenode.net/).\n\nRelated projects\n----------------\n\nThese are libraries in other languages that were inspired by Ohm.\n\n* [Ohm](https://github.com/soveran/ohm) for Ruby, created by soveran\n* [JOhm](https://github.com/xetorthio/johm) for Java, created by xetorthio\n* [Lohm](https://github.com/slact/lua-ohm) for Lua, created by slact\n* [ohm.lua](https://github.com/amakawa/ohm.lua) for Lua, created by amakawa\n* [Nohm](https://github.com/maritz/nohm) for Node.js, created by maritz\n* [Redisco](https://github.com/iamteem/redisco) for Python, created by iamteem\n* [redis3m](https://github.com/luca3m/redis3m) for C++, created by luca3m\n* [Ohmoc](https://github.com/seppo0010/ohmoc) for Objective-C, created by seppo0010\n* [Sohm](https://github.com/xxuejie/sohm.lua) for Lua, compatible with Twemproxy\n\nArticles and Presentations\n--------------------------\n\n* [Simplicity](http://files.soveran.com/simplicity)\n* [How to Redis](http://www.paperplanes.de/2009/10/30/how_to_redis.html)\n* [Redis and Ohm](http://carlopecchia.eu/blog/2010/04/30/redis-and-ohm-part1/)\n* [Ohm (Redis ORM)](http://blog.s21g.com/articles/1717) (Japanese)\n* [Redis and Ohm](http://www.slideshare.net/awksedgreep/redis-and-ohm)\n* [Ruby off Rails](http://www.slideshare.net/cyx.ucron/ruby-off-rails)\n* [Data modeling with Redis and Ohm](http://www.sitepoint.com/semi-relational-data-modeling-redis-ohm/)\n\nGetting started\n---------------\n\nInstall [Redis][redis]. On most platforms it's as easy as grabbing the sources,\nrunning make and then putting the `redis-server` binary in the PATH.\n\nOnce you have it installed, you can execute `redis-server` and it will\nrun on `localhost:6379` by default. Check the `redis.conf` file that comes\nwith the sources if you want to change some settings.\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n  ohm:\n    github: soveran/ohm-crystal\n```\n\nOr you can grab the code from [http://github.com/soveran/ohm-crystal][ohm].\n\n## Connecting to a Redis database\n\nOhm uses a lightweight Redis client called [Resp][resp]. To connect\nto a Redis database, you will need to set an instance of `Resp`, with\nan URL of the form `redis://:\u003cpasswd\u003e@\u003chost\u003e:\u003cport\u003e/\u003cdb\u003e`, through the\n`Ohm.redis=` method, e.g.\n\n```crystal\nrequire \"ohm\"\n\nOhm.redis = Resp.new(\"redis://127.0.0.1:6379\")\n\nOhm.redis.call \"SET\", \"Foo\", \"Bar\"\n\nOhm.redis.call \"GET\", \"Foo\"\n# =\u003e \"Bar\"\n```\n\nOhm defaults to a Resp connection to \"redis://127.0.0.1:6379\". The\nexample above could be rewritten as:\n\n```crystal\nrequire \"ohm\"\n\nOhm.redis.call \"SET\", \"Foo\", \"Bar\"\n\nOhm.redis.call \"GET\", \"Foo\"\n# =\u003e \"Bar\"\n```\n\nAll Ohm models inherit the same connection settings from `Ohm.redis`.\n\nModels\n------\n\nOhm's purpose in life is to map objects to a key value datastore. It\ndoesn't need migrations or external schema definitions. Take a look at\nthe example below:\n\n### Example\n\n```crystal\nclass Party \u003c Ohm::Model\n  attribute :name\n  reference :venue, Venue\n  set :participants, Person\n  counter :votes\n\n  index :name\nend\n\nclass Venue \u003c Ohm::Model\n  attribute :name\n  collection :parties, Party, :venue_id\nend\n\nclass Person \u003c Ohm::Model\n  attribute :name\nend\n```\n\nAll models have the `id` attribute built in, you don't need to declare it.\n\nThis is how you interact with IDs:\n\n```crystal\nparty = Party.create({\"name\" =\u003e \"Ohm Worldwide Party 2031\"})\nparty.id\n# =\u003e \"1\"\n\n# Find an party by id\nparty == Party[1]\n# =\u003e true\n\n# Update an party\nparty.update({\"name\" =\u003e \"Ohm Worldwide Party 2032\"})\nparty.name\n# =\u003e \"Ohm Worldwide Party 2032\"\n\n# Trying to find a non existent party\nParty[2]\n# =\u003e nil\n\n# Finding all the parties\nParty.all.to_a\n# =\u003e [\u003cParty::0x102736570 name='Ohm Worldwide Party 2032'\u003e]\n```\n\nThis example shows some basic features, like attribute declarations\nand querying. Keep reading to find out what you can do with models.\n\nAttribute types\n---------------\n\nOhm::Model provides 4 attribute types:\n\n* `Ohm::Model.attribute`,\n* `Ohm::Model.set`\n* `Ohm::Model.list`\n* `Ohm::Model.counter`\n\nand 2 meta types:\n\n* `Ohm::Model.reference`\n* `Ohm::Model.collection`.\n\n### attribute\n\nAn `attribute` is just any value that can be stored as a string.\nIn the example above, we used this field to store the party's `name`.\nIf you want to store any other data type, you have to convert it\nto a string first. Be aware that Redis will return a string when\nyou retrieve the value.\n\n### set\n\nA `set` in Redis is an unordered list, with an external behavior\nsimilar to that of Ruby arrays, but optimized for faster membership\nlookups.  It's used internally by Ohm to keep track of the instances\nof each model and for generating and maintaining indexes.\n\n### list\n\nA `list` is like an array in Ruby. It's perfectly suited for queues\nand for keeping elements in order.\n\n### counter\n\nA `counter` is like a regular attribute, but the direct manipulation\nof the value is not allowed. You can retrieve, increase or decrease\nthe value, but you can not assign it. In the example above, we used\na counter attribute for tracking votes. As the increment and decrement\noperations are atomic, you can rest assured a vote won't be counted\ntwice.\n\n### reference\n\nIt's a special kind of attribute that references another model.\nInternally, Ohm will keep a pointer to the model (its ID), but you\nget accessors that give you real instances. You can think of it as\nthe model containing the foreign key to another model.\n\n### collection\n\nProvides an accessor to search for all models that `reference` the\ncurrent model.\n\nTracked keys\n------------\n\nBesides the provided attribute types, it is possible to instruct\nOhm to track arbitrary keys and tie them to the object's lifecycle.\n\nFor example:\n\n```crystal\nclass Log \u003c Ohm::Model\n  track :text\n\n  def append(msg)\n    key[\"text\"].call(\"APPEND\", msg)\n  end\n\n  def tail(n = 100)\n    key[\"text\"].call(\"GETRANGE\", -n.to_s, \"-1\")\n  end\nend\n\nlog = Log.create\nlog.append(\"hello\\n\")\n\nassert_equal \"hello\\n\", log.tail\n\nlog.append(\"world\\n\")\n\nassert_equal \"world\\n\", log.tail(6)\n```\n\nWhen the `log` object is deleted, the `:text` key will be deleted\ntoo. Note that the key is scoped to that particular instance of\n`Log`, so if `log.id` is `42` then the key will be `Log:42:text`.\n\nPersistence strategy\n--------------------\n\nThe attributes declared with `attribute` are only persisted after\ncalling `save`.\n\nOperations on attributes of type `list`, `set` and `counter` are\npossible only after the object is created (when it has an assigned\n`id`). Any operation on these kinds of attributes is performed\nimmediately.  This design yields better performance than buffering\nthe operations and waiting for a call to `save`.\n\nFor most use cases, this pattern doesn't represent a problem.\nIf you are saving the object, this will suffice:\n\n```crystal\nif party.save\n  party.comments.add(Comment.create({\"body\" =\u003e \"Wonderful party!\"}))\nend\n```\n\nWorking with Sets\n-----------------\n\nGiven the following model declaration:\n\n```crystal\nclass Party \u003c Ohm::Model\n  attribute :name\n  set :attendees, Person\nend\n```\n\nYou can add instances of `Person` to the set of attendees with the\n`add` method:\n\n```crystal\nparty.attendees.add(Person.create({\"name\" =\u003e \"Albert\"}))\n\n# And now...\nparty.attendees.each do |person|\n  # ...do what you want with this person.\nend\n```\n\nWorking with Lists\n------------------\n\nGiven the following model declaration:\n\n```crystal\nclass Queue \u003c Ohm::Model\n  attribute :name\n  list :people, Person\nend\n```\n\nYou can add instances of `Person` to the list of people with the\n`push` method:\n\n```crystal\nqueue.people.push(Person.create({\"name\" =\u003e \"Albert\"}))\n\n# And now...\nqueue.people.each do |person|\n  # ...do what you want with this person.\nend\n```\n\nWorking with Counters\n---------------------\n\nGiven the following model declaration:\n\n```crystal\nclass Site \u003c Ohm::Model\n  attribute :url\n  counter :visits\nend\n```\n\nYou can increment or decrement the visits:\n\n```crystal\nsite.visits     #=\u003e 0\nsite.visits(+1) #=\u003e 1\nsite.visits(+1) #=\u003e 2\nsite.visits(+5) #=\u003e 7\nsite.visits(-4) #=\u003e 3\nsite.visits     #=\u003e 3\n```\n\nAssociations\n------------\n\nOhm lets you declare `references` and `collections` to represent\nassociations.\n\n```crystal\nclass Post \u003c Ohm::Model\n  attribute :title\n  attribute :body\n  collection :comments, Comment, :post_id\nend\n\nclass Comment \u003c Ohm::Model\n  attribute :body\n  reference :post, Post\nend\n```\n\nAfter this, every time you refer to `post.comments` you will be\ntalking about instances of the model `Comment`. If you want to get\na list of IDs you can use `post.comments.ids`.\n\n### References explained\n\nDoing a `Ohm::Model.reference` is actually just a shortcut for\nthe following:\n\n```crystal\n# Redefining our model above\nclass Comment \u003c Ohm::Model\n  attribute :body\n  attribute :post_id\n  index :post_id\n\n  def post=(post)\n    self.post_id = post.id\n  end\n\n  def post\n    Post[post_id]\n  end\nend\n```\n\nThe net effect here is we can conveniently set and retrieve `Post` objects,\nand also search comments using the `post_id` index.\n\n```crystal\nComment.find({\"post_id\" =\u003e \"1\"})\n```\n\n### Collections explained\n\nThe reason a `Ohm::Model.reference` and a\n`Ohm::Model.collection` go hand in hand, is that a collection is\njust a macro that defines a finder for you, and we know that to find a model\nby a field requires an `Ohm::Model.index` to be defined for the field\nyou want to search.\n\nHere's again the `collection` macro in use:\n\n```crystal\ncollection :comments, Comment, :post_id\n```\n\nWhen it expands, what you get is this method definition:\n\n```crystal\ndef comments\n  Comment.find({\"post_id\" =\u003e self.id })\nend\n```\n\nBoth examples are equivalent.\n\nIndices\n-------\n\nAn `Ohm::Model.index` is a set that's handled automatically by Ohm. For\nany index declared, Ohm maintains different sets of objects IDs for quick\nlookups.\n\nIn the `Party` example, the index on the name attribute will\nallow for searches like `Party.find({\"name\" =\u003e \"some value\"})`.\n\nNote that the methods `Ohm::Model::Set#find` and\n`Ohm::Model::Set#except` need a corresponding index in order to work.\n\n### Finding records\n\nYou can find a collection of records with the `find` method:\n\n```crystal\n# This returns a collection of users with the username \"Albert\"\nUser.find({\"username\" =\u003e \"Albert\"})\n```\n\n### Filtering results\n\n```crystal\n# Find all users from Argentina\nUser.find({\"country\" =\u003e \"Argentina\"})\n\n# Find all active users from Argentina\nUser.find({\"country\" =\u003e \"Argentina\", \"status\" =\u003e \"active\"})\n\n# Find all active users from Argentina and Uruguay\nUser.find({\"status\" =\u003e \"active\"}).combine({\"country\" =\u003e [\"Argentina\", \"Uruguay\"] })\n\n# Find all users from Argentina, except those with a suspended account.\nUser.find({\"country\" =\u003e \"Argentina\"}).except({\"status\" =\u003e \"suspended\"})\n\n# Find all users both from Argentina and Uruguay\nUser.find({\"country\" =\u003e \"Argentina\"}).union({\"country\" =\u003e \"Uruguay\"})\n```\n\nNote that calling these methods results in new sets being created\non the fly. This is important so that you can perform further operations\nbefore reading the items to the client.\n\nFor more information, see [SINTERSTORE](http://redis.io/commands/sinterstore),\n[SDIFFSTORE](http://redis.io/commands/sdiffstore) and\n[SUNIONSTORE](http://redis.io/commands/sunionstore).\n\nUniques\n-------\n\nUniques are similar to indices except that there can only be one record per\nentry. The canonical example of course would be the email of your user, e.g.\n\n```crystal\nclass User \u003c Ohm::Model\n  attribute :email\n  unique :email\nend\n\nu = User.create({\"email\" =\u003e \"foo@bar.com\"})\nu == User.with(\"email\", \"foo@bar.com\")\n# =\u003e true\n\nUser.create({\"email\" =\u003e \"foo@bar.com\"})\n# =\u003e raises Ohm::UniqueIndexViolation\n```\n\n[redis]: http://redis.io\n[ohm]: http://github.com/soveran/ohm-crystal\n[resp]: https://github.com/soveran/resp-crystal\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Fohm-crystal","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsoveran%2Fohm-crystal","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsoveran%2Fohm-crystal/lists"}