diff --git a/README.md b/README.md index a59b934..a82792a 100644 --- a/README.md +++ b/README.md @@ -4,33 +4,30 @@ ## Synopsis -Persistence isolation framework for long term ruby projects. - -Or, "Half, not Half-Assed" approach to ORM. +ORM with a functional twist, or persistence isolation framework, choose one that +appeals to you more. ## Caution -ORMivore is highly opinionated and does not quite follow conventional -rules of wisdom in ruby/rails community. +ORMivore is highly opinionated and does not follow +conventions typically found in Rails projects. ## Audience People that value ability to maintain app after it is couple years old, or want to get legacy app back under control. -On the other hand, if quick throw-away projects that require no -extension/maintenance in the future are the story of your life, +On the other hand, if quick-and-dirty projects that require no +extension/maintenance in the future are what you do, probably ORMivore is not for you. ## Stability ORMivore is in the R&D 'alpha' stage. In other words, it is playground -for experimentation. Changes are highly likely to be not backward +for experimentation. Changes are highly likely to not be backward compatible. If you decide to use it, prepare to roll up your sleeves, and I disclaim any potential indirect harm to kittens. -Just to state the obvious - ORMivore is not production ready just yet. - ## Motivation If you have seen legacy Rails app, chances are you noticed: @@ -40,25 +37,29 @@ If you have seen legacy Rails app, chances are you noticed: - copious amounts of logged SQL that is hard to relate back to application code - bad performance and high database load caused by runaway (and inefficient) SQL queries +- hard to find bugs caused by multiple instances of object with same +identity ORMivore is designed to help in either avoiding those issues on new project, or slowly eliminating them on a legacy app. -## Philosophy summary +## Inspirational ideas - Everything the system does FOR you, the system also does TO you -- Actions that will lead to long term pain better be painful +- Actions that lead to long term pain better be painful immediately, and never sugar-coated with short term gain - Long term maintenability trumps supersonic speed of initial development -- There is much to be learned from functional programming +- Functional programming is good and you can mix and match it with OO +- Immutability promotes better sleep at night - So much complexity in software comes from trying to make one thing do - two things (Ryan Singer) + two things -## Philosophy explained +## Driving principles -OOD is great, but have to be applied with care. ORMivore uses plain data -structures (functional style) to communicate between different layers, -solving problem of messy circular dependencies. +OOD is great, but have to be applied with care, and is not universal. +ORMivore uses plain data structures (functional style) to communicate +between different layers, solving problem of messy circular +dependencies. Mutable entities have state, and that makes reasoning about them more difficult. Immutable entities are easy to reason about and discourage @@ -67,55 +68,61 @@ placing any "how to" behavior on them, making them a good place for multi-threaded environments. Many ORM make it too easy to conflate business logic and persistence. -Ports and adapters pattern (hexagonal architecture) are core of -ORMivore, and it is great for isolating persistance logic from entities. -It also allows for some degree of substitutability of different storages. +Ports and adapters pattern (hexagonal architecture) is core of +ORMivore, and it helps to isolate persistance logic from entities. +It also allows for pluggable storage mechanisms (to some degree). Putting responsibility of managing data and managing association in one -same object leads to many lies inside of code that quickly relult in a -lot of complexity. It is much simpler to divide those responsibilities -(not easier, but much simpler). +same object results in a tangled ball of complexity. Dividing those +responsibilities allows for a much simpler system. -While STI and polymorphic relations are bad for your health, your legacy +While STI and polymorphic relations are not such a good idea, your legacy database is probably littered with them, and ORMivore provides means to -map those to domain objects in a 'class-less' way. -That makes it possible to introduce ORMivore entities in the same -project/process as legacy ActiveRecord models. +map those to domain objects of different classes. +That makes it possible to introduce ORMivore entities in the legacy +project side-to-side with old ActiveRecord models. ## Installation -Just add 'gem "ormivore"' to Gemfile, or run 'gem install ormivore'. +Just add 'gem "ormivore"' to your Gemfile, or run 'gem install ormivore'. ## Basic Code Example Typical setup would include a bit of boiler plate code that would look a little ridiculous for such simple example heere. This example is using shortcuts that generate boiler plate classes automatically, but for real -production code spelling things out probably is better, it will come +production code spelling details out is better, those extra classes come handy when real functionality and tests are added. -This is complete example that can be copy/pasted in the console, and -will work. It uses memory adapter to avoid having to configure database -access, but code change to get it to work with SQL database is trivial. +This is complete working example that can be copy/pasted in irb session. +It uses memory adapter to avoid having to configure database +access, but it is trivial to change to it to work with SQL database. + +Oh, and here is convenient way to run irb session when you are in ORMivore project: + + irb -r ./app/console.rb ```ruby Sample = Module.new -ORMivore::create_entity_skeleton(Sample, :post, port: true, repo: true, memory_adapter: true) do +ORMivore::create_entity_skeleton(Sample, :post, + port: true, repo: true, memory_adapter: true) do attributes do string :title string :body end end -ORMivore::create_entity_skeleton(Sample, :tag, port: true, repo: true, memory_adapter: true) do +ORMivore::create_entity_skeleton(Sample, :tag, + port: true, repo: true, memory_adapter: true) do attributes do string :name end end -ORMivore::create_entity_skeleton(Sample, :tagging, port: true, repo: true, memory_adapter: true) do +ORMivore::create_entity_skeleton(Sample, :tagging, + port: true, repo: true, memory_adapter: true) do attributes do integer :post_id integer :tag_id @@ -152,26 +159,41 @@ module Sample extend ORMivore::RepoFamily end - Post::Repo.new(Post::Entity, Post::StoragePort.new(Post::StorageMemoryAdapter.new), family: Repos) - Tag::Repo.new(Tag::Entity, Tag::StoragePort.new(Tag::StorageMemoryAdapter.new), family: Repos) - Tagging::Repo.new(Tagging::Entity, Tagging::StoragePort.new(Tagging::StorageMemoryAdapter.new), family: Repos) + Post::Repo.new(Post::Entity, + Post::StoragePort.new(Post::StorageMemoryAdapter.new), family: Repos) + Tag::Repo.new(Tag::Entity, + Tag::StoragePort.new(Tag::StorageMemoryAdapter.new), family: Repos) + Tagging::Repo.new(Tagging::Entity, + Tagging::StoragePort.new(Tagging::StorageMemoryAdapter.new), family: Repos) + Repos.freeze end -session = ORMivore::Session.new(Sample::Repos, Sample::Associations) # # -post = session.repo.post.create(title: 'foo', body: 'bar') # #"foo", :body=>"bar"}> -post = post.apply(body: 'baz') # #"foo", :body=>"baz"}> -session.association(post, :tags).values # [] -t1 = session.repo.tag.create(name: 't1') # #"t1"}> -session.association(post, :tags).add(t1) # [#-1, :tag_id=>-1}>] -session.association(post, :tags).values # [#"t1"}>] -session.association(post, :taggings).values # [#-1, :tag_id=>-1}>] -session.commit # [Sample::Post::Entity, Sample::Tag::Entity, Sample::Tagging::Entity] - -session = ORMivore::Session.new(Sample::Repos, Sample::Associations) # # -post = session.repo.post.find_by_id(1) # #"foo", :body=>"baz"}> -session.association(post, :tags).values # [#"t1"}>] -session.association(post, :taggings).values # [#1, :tag_id=>1}>] +session = ORMivore::Session.new(Sample::Repos, Sample::Associations) + # => # +post = session.repo.post.create(title: 'foo', body: 'bar') + # => #"foo", :body=>"bar"}> +post.apply(body: 'baz') + # => #"foo", :body=>"baz"}> +session.association(post, :tags).values + # => [] +t1 = session.repo.tag.create(name: 't1') + # => #"t1"}> +session.association(post, :tags).add(t1) + # => [#-1, :tag_id=>-1}>] +session.association(post, :tags).values + # => [#"t1"}>] +session.association(post, :taggings).values + # => [#-1, :tag_id=>-1}>] +session.commit_and_reset + # => [Sample::Post::Entity, Sample::Tag::Entity, Sample::Tagging::Entity] + +post = session.repo.post.find_by_id(post.identity) + # => #"foo", :body=>"baz"}> +session.association(post, :tags).values + # => [#"t1"}>] +session.association(post, :taggings).values + # => [#1, :tag_id=>1}>] ``` @@ -180,7 +202,7 @@ session.association(post, :taggings).values # [#