TL;DR - saving models innit. Skip down to usage.
It is not currently possible to call .to_json on an object that inherits from Storm::BaseModel (i.e. League, Team, etc). Doing so will cause a stack level too deep error, so be aware of that during development until it is fixed.
This is what should be used throughout Steak for object persistance (instead of ActiveRecord).
It follows the Data Mapper pattern (rather than the ActiveRecord pattern), putting a layer between our domain models, and their persistence. The major outworkings of this are:
- Models can nolonger persist themselves, thus each of them has a
Managerto do it for them. egUserManager.save(user) - As models aren't modeling a database row, they must explicitly define their attributes. eg.
attributes :id, :my_attr, :my_other_attr - You can't access related objects by doing
division.teams, instead doTeamManager.find_by_division_id(division.id)
The reason for using a data mapper pattern at all is to remove all circular dependencies between our models. This allows us to split our models into different engines that do not depend on eachother, giving us much greater flexibility (at the cost of a little more typing) as our product grows very large.
This forced separate means that once we get to large scale we will not be dealing with a web of dependencies, and ultimatley will easily be able to split out apps that provide a subset of our main app's functionality if (when) we're forced to do so as we scale up.
There's a lot of good and very clever stuff in ActiveRecord that it makes sense to make use of (eg. SQL generation), so we've built storm as a layer on top of that.
Much like ActiveRecord::Base does for Rails models, we've written two base classes that provide core functionality for our domain models and our managers. These are Storm::BaseModel and Storm::BaseManager respectively.
The bulk of the complexity is in Storm::BaseManager. In this implimentation it pretty much just turns domain objects into record (ie. rails) objects and, then calls appropriate persistence related methods on them, or builds domain objects from records objects and sets some internal state.
When creating a new model, you now need to create three classes
- Domain Model - The model used throughout steak.
- Record Model (plus migrations) - Used by storm to actually persist your model.
- Manager - The class you use to load/persist your domain model.
Pretty simple stuff:
class User < Storm::BaseModel
# define our attrs
attributes :id, :email, :name
# add methods as normal
def surname
name.split(" ").last
end
end
user_one = User.new
user_one.name = "Timothy Sherratt"
# set attrs with a hash
user_two = User.new(name: "Timothy Sherratt")
We're essentially using ActiveRecord as our SQL generation engine, because of this record models are normal ActiveRecord::Base models, however they inherit from Storm::BaseRecord as this allows us to add extra functionality to that which ActiveRecord::Base provides.
As we're only concerned only with getting stuff into the database, the only stuff that should be in these models is stuff todo with that. As AR models get all the config from DB structure, all we're left with are validations to make sure our data is in the correct format.
When writing your migrations, make sure that your column names exactly match your domain model attribute names or errors will ensue!
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
end
end
end
class UserRecord < Storm::BaseRecord
validates presence: true, :name
end
Each model needs a manager. This class essentially handles the translation between the domain models, and our record models used for saving. Manager method names have been borrowed heavily from ActiveRecord, so should feel familiar.
Basic persistence/loading functionality is provided entirely by BaseManager, so there is very little you need to do...
class UserManager < Storm::BaseManager; end
um = UserManager.new
user = User.new(name: "Timothy Sherratt")
Saving:
user.id
# => nil
um.save(user)
# => true
user.id
# => 1
# :save, and :save! as you would expect
user.name = nil
um.save(user)
# => false
um.save!(user)
# => Storm::RecordInvalidError "validation failed..."
Updating
user.email = "tim@mitoo.co"
um.save(user)
# => true
Loading
# by id
user = um.find(1)
# => #<User id: 1, ...
# by an attr
user = um.find_by_email("tim@mitoo.co")
# => #<User id: 1, ...
# first, second, third, fourth
user = um.first
# => #<User id: 1, ...
# where
um.where(email: "tim@mitoo.co")
# => [#<User id: 1, ... ] # returns array
Deleting Soft delete is built into Storm, and is what you should use when you do not have a specific reason to do otherwise
# soft delete
um.destroy(user)
# => true
# real delete
um.real_delete!(user)
# => true
#####Generate Your Files! I only went a wrote a bloody generator.
$ cd components/users
$ rails g storm_model my_model
# => create app/models/users/my_model.rb
# => create app/models/users/my_model_manager.rb
# => create app/models/users/my_model_record.rb
#####Custom Manager Functionality
You can define your own methods on your managers. See the code for a few (protected) methods exposed to allow for more complex stuff (but use with caution).
class TeamManager < Storm::BaseManager
# contrived example, obvs
def teams_in_my_fave_league
self.where(league_: 2)
end
end
find_by_sql is one of the protected methods that is exposed for use within managers. This is good because it allows you to setup queries with joins etc. But you must remember:
- Caching, especially if you're writing complex/long-running queries.
- To check
trashed_at IS NULL
module LeagueModels
class LeagueManager < Storm::BaseManager
def find_by_user_enquiries(user_id)
q = "SELECT l.* \
FROM league_models_leagues l, league_models_league_enquiries le \
WHERE l.id=le.league_id AND le.user_id=? \
AND le.trashed_at IS NULL \
AND l.trashed_at IS NULL"
self.find_by_sql [q, user_id]
end
end
end
#####Non-Standard Naming
class CupRecords < Storm::BaseRecord; end
class LeagueManager < Storm::BaseManager
set_domain_class Comps
set_record_class CupRecords
end
class Comps < Storm::BaseModel
set_manager_class LeagueManager
end
#####Saving an instance of a mystery model You can get from the model to the manager, if you need to. It is not recommended to use this functionality outside of when you're dealing with polymorphic relationships etc. as it could cover up bugs, and make code less obvious/readable.
mngr = my_isnt.manager
mngr.save! my_inst
#####Callbacks
In Storm lifecycle callbacks are on the Manager rather than the Model, as it is the Manager rather than the model that knows about these events.
The major difference between this and ActiveRecord callbacks is that as we're dealing with a manager, the methods we define to be called must accept one argument, which will be the instance of the domain model being saved.
We currently have :create, :update, :save and :desctroy callbacks, and before_xxx/after_xxx expect :method_name.
class UserManager < Storm::BaseManager
before_save :my_before_method
after_save :my_after_method
def my_before_method(obj)
put "about to save user id #{obj.id}"
end
def my_after_method(obj)
put "finished saving user id #{obj.id}"
end
end
UserManager.new.save(my_user)
# => "about to save user id 1"
# => "finished saving user id 1"
Allow different models that inherit from eachother to be persisted to a single db table.
In simple terms, this works by saving the class of the instance into its db row in a column called :sti_type (lolz), and then just letting the Manager know that it has to take this into account. So...
- Create the column
class AddStiTypeToResults < ActiveRecord::Migration
def change
add_column :results, :sti_type, :string
end
end- Let the manager know
class ResultsManager < BaseManager
single_table_inheritance
end- That's it...
manager = ResultManager.new
result = Result.new(id: 1)
hw_result = HomeWalkoverResult.new(id: 2)
manager.save! result
manager.save! hw_result
manager.find(1).class.name
# => 'Result'
manager.find(2).class.name
# => 'HomeWalkoverResult'P.S. If you want to convert an instance from one of your STI types to another, you can use the :becomes method (which works very similarly to its AR namesake).
result = manager.find(1)
result = result.becomes(HomeWalkoverResult)
result.id
# => 1
result.class.name
# => 'HomeWalkoverResult'Save a few keystrokes by getting storm to instantiate our desired manager and put it in @mngr before each request.
module LeagueApi
class V1::LeaguesController < ApplicationController
mngr LeagueModels::LeagueManager
def show
@league = @mngr.find(params[:id])
render 'leagues/league'
end
end
end
Add this line to your application's Gemfile:
gem 'storm'And then execute:
$ bundle
Or install it yourself as:
$ gem install storm
TODO: Write usage instructions here
- Fork it ( https://github.com/[my-github-username]/storm/fork )
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request