Skip to content
Browse files

update_attributes -> update

  • Loading branch information...
1 parent 833ccfd commit 409db3b08ebb05464bea5e2e6ee162b1ca9e48bd @norman norman committed
Showing with 305 additions and 62 deletions.
  1. +272 −36 Guide.md
  2. +28 −25 README.md
  3. +4 −0 lib/prequel/active_model.rb
  4. +1 −1 lib/prequel/model.rb
View
308 Guide.md
@@ -4,76 +4,312 @@ By [Norman Clarke](http://njclarke.com)
## What is Prequel?
-Prequel is a database and ORM replacement for small, mostly static models. In
-many applications or libraries, you need models for datasets like the 50 US
-states, the world's countries with associated TLDs, or a list of phone number
+Prequel is a database and ORM alternative for small, mostly static models. Use
+it to replace database-persisted seed data and ad-hoc structures in your app or
+library with plain old Ruby objects that are searchable via a fast, simple
+database-like API.
+
+Many applications and libraries need models for datasets like the 50 US states,
+the world's countries indexed by top level domain, or a list of phone number
prefixes and their associated state, province or city.
Creating a model with Active Record, DataMapper or another ORM and storing this
data in an RDBMS is usually overkill for small and/or static datasets. On the
-other hand, storing the data in ad-hoc strutures can offer little flexibility
-when it comes to filtering, or establishing relations with models.
-
-Prequel offers a middle ground: it makes use of Ruby's Enumerable module to
-expose a powerful, ORM-like query interface to Ruby's Hash. It can operate
-entirely in-memory, or persist data in a file or a compressed, signed string
-suitable for passing as a cookie.
-
-Prequel loads your dataset from the file and keeps it in memory. It's not a
-"NoSQL", it's a "NoDB". There's not only no schema or migrations to set up,
-there's also no server or anything else to install since its only dependencies
-are Ruby's standard library. Whenever possible, it behaves like the Ruby
-standard library as opposed to introducing quirky behavior. It was inspired in
-part by [Rubinius's](http://rubini.us/) short but meaningful tagline "Use Ruby."
-
-But just a word of warning - don't even dare think about using it for more than
-a couple megabytes of data. For that you need a real database of some sort, like
-Postgres, MySQL, Redis, Mongo, etc.
-
-## An example
-
-class Country
- field :tld, :name, :region, :population, :area
- filters do
- def african
- find {|c| c.region == :africa}
- end
+other hand, keeping it in ad-hoc strutures can offer little flexibility
+when it comes to filtering, or establishing searchable relations with other
+models.
- def large
- find {|c| c.area >= 100_000}
- end
- end
-end
+Prequel offers a middle ground: it loads your dataset from a script or file,
+keeps it in memory as a hash, and makes use of Ruby's Enumerable module to
+expose a powerful, ORM-like query interface to your data.
+
+Bu just one word of warning: Prequel is not like Redis or Membase. It's not a
+real database of any kind - SQL or NoSQL. Think of it as a "NoDB." Don't use it
+for more than a few megabytes of data: for that you'll want something like
+Redis, Postgres, or whatever kind of database makes sense for your needs.
## Creating Models
+Almost any Ruby class can be stored as a Prequel Model, simply by extending
+the {#Prequel::Model} module:
+
+ class Person
+ extend Prequel::Model
+ end
+
+You can also extend the {Prequel::ActiveModel} module to add an Active
+Record/Rails compatible API. This will be discussed in more detail later.
+
### Setting up a simple model class
+As shown above, simply extend (**not** include) `Prequel::Model` to create a
+model class. In your class, you can add persistable/searchable fields using the
+{Prequel::Model::ClassMethods#field field} method. This adds accessor methods,
+similar to those created by `attr_accessor`, but marks them for internal use by
+Prequel.
+
+ class Person
+ extend Prequel::Model
+ field :email, :name, :birthday, :favorite_color
+ end
+
+All PrequelModels require at least one unique field to use as a hash key. By
+convention, the first field you add will be used as the key; `:email` in the
+example above. You can also use the {Prequel::Model::ClassMethods#id_field
+id\_field} method to specify which field to use as the key.
+
### Basic operations on models
+New instances of Prequel Models can be {Prequel::Model::InstanceMethods#initialize initialized}
+with an optional hash of attributes, or a block.
+
+ person = Person.new :name => "Moe"
+
+ person = Person.new
+ person.name = "Moe"
+
+ person = Person.new do |p|
+ p.name = "moe"
+ end
+
+When initializing with both a hash and a block, the block is called last, so
+accessor calls in the block take precedence:
+
+ person = Person.new(:name => "Larry") do |p|
+ p.name = "Moe"
+ end
+ p.name #=> "Moe"
+
+Prequel exposes methods for model creation and storage which should look quite
+familiar to anyone acquantied with ORM's, but the searching, indexing and
+filtering methods are a little different.
+
#### CRUD
+{Prequel::Model::ClassMethods#create Create},
+{Prequel::AbstractKeySet#find Read},
+{Prequel::Model::InstanceMethods#update Update},
+{Prequel::Model::InstanceMethods#delete Delete}
+methods are fairly standard:
+
+ # create
+ Person.create :name => "Moe Howard", :email => "moe@3stooges.com"
+
+ # read
+ moe = Person.get "moe@3stooges.com" # or...
+ moe = Person.find "moe@3stooges"
+
+ # update
+ moe.name = "Mo' Howard"
+ moe.save # or...
+ moe.update :name => "Mo' Howard" # or...
+
+ # delete
+ moe.delete # or...
+ Person.delete "moe@3stooges.com"
+
+#### Searching
+
+Finds in Prequel are done via the find class method. If a single argument is passed,
+that is treated as a key and Prequel looks for the matching record:
+
+ Person.find "moe@3stooges" # returns instance of Person
+ Person.find "cdsafdfds" # raises Prequel::NotFoundError
+
+If a block is passed, then Prequel looks for records that return true for the
+conditions in the block, and returns an iterator that you can use to step through
+the results:
+
+ people = Person.find {|p| p.city =~ /Seattle|Portland|London/}
+ people.each do |person|
+ puts "#{person.name} probably wishes it was sunny right now."
+ end
+
+There are two important things to note here. First, in the `find` block, it appears
+that an instance of person is yielded. However, this is actually an instance of
+{Prequel::HashProxy}, which allows you to invoke model attributes either as symbols,
+strings, or methods. You could also have written the example these two ways:
+
+ people = Person.find {|p| p[:city] =~ /Seattle|Portland|London/}
+ people = Person.find {|p| p["city"] =~ /Seattle|Portland|London/}
+
+Second, the result of the find is not an array, but rather an enumerator that
+allows you to iterate over results while instantiating only the model objects
+that you use, in order to improve performance. This enumerator will be an
+instance of a subclass of {Prequel::AbstractKeySet}.
+
+Models' `find` methods are actually implemented directly on key sets: when you
+do `Person.find` you're simply doing a find on a key set that includes all keys
+for the Person class. This is important because it allows finds to be refined:
+
+ londoners = Person.find {|p| p.city == "London"}
+
+ londoners.find {|p| p.country == "CA"}.each do |person|
+ puts "#{person.name} lives in Ontario"
+ end
+
+ londoners.find {|p| p.country == "GB"}.each do |person|
+ puts "#{person.name} lives in Europe"
+ end
+
+Key sets can also be manipulated with set arithmetic functions:
+
+ european = Country.find {|c| c.continent == "Europe"}
+ spanish_speaking = Country.find {|c| c.language == :es}
+ portuguese_speaking = Country.find {|c| c.language == :pt}
+ speak_an_iberian_language = spanish_speaking + portuguese_speaking
+ non_european_iberian_speaking = speak_an_iberian_language - european
+
+An important implementation detail is that the return value of `Person.find` is
+actually an instance of a subclass of {Prequel::AbstractKeySet}. When you
+{Prequel::Model.extended extend Prequel::Model}, Prequel creates
+{Prequel::Model::ClassMethods#key_class an anonymous subclass} of
+Prequel::AbstractKeySet, which facilitates customized finders on a per-model
+basis, such as the filters described below.
+
#### Filters
+Filters in Prequal are saved finds that can be chained together, conceptually
+similar to [Active Record
+scopes](http://api.rubyonrails.org/classes/ActiveRecord/NamedScope/ClassMethods.html#method-i-scope).
+
+You define them with the {Prequel::Model::ClassMethods#filters filters} class
+method:
+
+ class Person
+ extend Prequel::Model
+ field :email, :gender, :city, :age
+
+ filters do
+ def men
+ find {|p| p.gender == "male"}
+ end
+
+ def who_live_in(city)
+ find {|p| p.city == city}
+ end
+
+ def between_ages(min, max)
+ find {|p| p.age >= min && p.age <= max}
+ end
+ end
+ end
+
+The filters are then available both as class methods on Person, and instance
+methods on key sets resulting from `Person.find`. This allows them to be
+chained:
+
+ Person.men.who_live_in("Seattle").between_ages(35, 40)
+
+#### Relations
+
+Prequel doesn't include any special methods for creating relations as in Active
+Record, because this can easily be accomplished by definiing an instance method
+in your model:
+
+ class Book
+ extend Prequel::Model
+ field :isbn, :title, :author_id, :genre, :year
+
+ def author
+ Author.get(author_id)
+ end
+
+ filters
+ def by_genre(genre)
+ find {|b| b.genre == genre}
+ end
+
+ def from_year(year)
+ find {|b| b.year == year}
+ end
+ end
+ end
+
+ class Author
+ extend Prequel::Model
+ field :email, :name
+
+ def books
+ Book.find {|b| b.author_id == email}
+ end
+ end
+
+Assuming for a moment that books can only have one author, the above example
+demonstrates how simple it is to set up `has_many` / `belongs_to` relationships
+in Prequel. Since the results of these finds are key sets, you can also chain
+any filters you want with them too:
+
+ Author.get("stevenking@writers.com").books.by_genre("horror").from_year(1975)
+
+
#### Indexes
+If your dataset is on the larger side of what's suitable for Prequel (a few
+thousand records or so) then you can use wrap your search with the
+{Prequel::Model::ClassMethods#with_index} method to memoize the results and
+improve the performance of frequently accessed queries:
+
+ class Book
+ extend Prequel::Model
+ field :isbn, :title, :author_id, :genre, :year
+
+ def self.horror
+ with_index do
+ find {|b| b.genre == "horror"}
+ end
+ end
+ end
+
+The argument to `with_index` is simply a name for the index, which needs to be
+unique to the model. You can optionally pass a name to with_index, which is
+a good idea when indexing methods that take arguments:
+
+ def self.by_genre(genre)
+ with_index("genre_#{genre}") do
+ find {|b| b.genre == genre}
+ end
+ end
+
### Active Model
+Prequel implements Active Model: read more about it
+[here](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/).
+
+TODO: write me
+
## Mappers and Adapters
+TODO: write me
+
### Bundled adapters
+TODO: write me
+
#### Prequel::Adapter
+TODO: write me
+
#### Prequel::Adapters::File
+TODO: write me
+
#### Prequel::Adapters::YAML
-#### Prequel::Adapters::Cookie
+TODO: write me
+
+#### Prequel::Adapters::SignedString
+
+TODO: write me
## Extending Prequel
+TODO: write me
+
### Adding functionality to Prequel::Model
+TODO: write me
+
### Creating your own adapter
+TODO: write me
View
53 README.md
@@ -6,8 +6,12 @@ library with plain old Ruby objects that are searchable via a fast, simple
database-like API.
It implements Active Model and has generators to integrate nicely with Rails.
+You can store your data in a file, a signed string suitable for storage in a
+cookie, or easily write your own IO adapter.
-For more info, take a peek at the {Guide}, or read on for some quick samples.
+For more info, take a peek at the [Prequel
+Guide](http://norman.github.com/prequel/file.Guide.html), or read on for some
+quick samples.
## A quick tour
@@ -42,17 +46,11 @@ For more info, take a peek at the {Guide}, or read on for some quick samples.
Country.create :tld => "CA", :name => "Canada", :region => :america, :population => 34_000_000
Country.create :tld => "JP", :name => "Japan", :region => :asia, :population => 127_000_000
Country.create :tld => "CN", :name => "China", :region => :asia, :population => 1_300_000_000
-
- # Save the database. Prequel is oriented towards reads, so you only write
- # when you want to save the whole database.
- adapter.save_database
+ # etc.
# Do some searches
- big_asian_countries = Country.big.in_region(:asia)
- start_with_c = Country.find {|c| c.name =~ /^C/}
-## Find out more
-
-A guide for Prequel is in the works, but for now you can read the API docs.
+ big_asian_countries = Country.big.in_region(:asia)
+ countries_that_start_with_c = Country.find {|c| c.name =~ /^C/}
## Installation
@@ -66,18 +64,22 @@ with others. Note that 1.8.6 is not supported.
* Ruby 1.8.7 - 1.9.2
* Rubinius 1.2.3
-* JRuby 1.5.6
+* JRuby 1.5.6+
## Author
- Norman Clarke (norman@njclarke.com)
+ [Norman Clarke](mailto:norman@njclarke.com)
+
+## Contributors
+
+Many thanks to Adrián Mugnolo for code review, feedback and the
+inspiration for the name.
-## Thanks
+## The name
-Thanks to Adrián Mugnolo for ideas, code review and the idea for the name.
-Thanks to the authors of [Sequel](http://sequel.rubyforge.org/) for the
-inspiration for this library's name, and the the idea behind how the filters and
-relations should work.
+Why "Prequel?" Because for some models, before you try SQL, you should try
+this. Also, because [Sequel](http://sequel.rubyforge.org/) is a fantastic
+library, and it inspired this library's use of Enumerable.
## License
@@ -86,16 +88,17 @@ Copyright (c) 2011 Norman Clarke
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
-the Software, and to permit persons to whom the Software is furnished to do so,
-subject to the following conditions:
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
-FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
-COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
-CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
View
4 lib/prequel/active_model.rb
@@ -113,6 +113,10 @@ def model_name
def destroy
run_callbacks(:destroy) { delete }
end
+
+ def update_attributes
+ run_callbacks(:save) { update }
+ end
end
end
end
View
2 lib/prequel/model.rb
@@ -136,7 +136,7 @@ def save
end
# Update this instance's attributes and invoke #save.
- def update_attributes(attributes)
+ def update(attributes)
self.class.attribute_names.each do |name|
value = attributes[name] || attributes[name.to_s]
send("#{name}=", value) if value

0 comments on commit 409db3b

Please sign in to comment.
Something went wrong with that request. Please try again.