I'm looking for some suggestions on how to deal with "Regions" in my system.
Almost all other models in the system (news, events, projects, and others) need to have a region that they can be sorted on.
So far, I've considered a Region model with has_many :through on a RegionLink table. I've never had a model joined to so many others and wonder if this route has any negatives.
I've also considered using the acts_as_taggable_on gem and just tag regions to models. This seems ok but I'll have to write more cleanup type code to handle the customer renaming or removing a region.
Whatever I choose I need to handle renaming and, more importantly, deleting regions. If a region gets deleted I will probably just give the user a choice on another region to replace the association.
Any advice on handling this is greatly appreciated.
If each News, Event, etc. will belong to only 1 Region, tags don't seem the most natural fit IMO. This leaves you with 2 options:
Add a region_id field to each model
This is simplest, but has the drawback that you will not be able to look at all the "regioned" items at once - you'll have to query the news, events, etc. tables separately (or use a UNION, which ActiveRecord doesn't support).
Use RegionLink model with polymorphic associations
This is only slightly more complicated, and is in fact similar to how acts_as_taggable_on works. Look at the Rails docs on *belongs_to* for a fuller description of polymorphic relationships if you are unfamiliar
class Region < ActiveRecord::Base
has_many :region_links
has_many :things, :through => :region_links
end
# This table with have region_id, thing_id and thing_type
class RegionLink < ActiveRecord::Base
belongs_to :region
belongs_to :thing, :polymorphic => true
end
class Event < ActiveRecord::Base
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
end
# Get all "things" (Events, Projects, etc.) from Region #1
things = Region.find(1).things
Renaming is quite simple - just rename the Region. Deleting/reassigning regions is also simple - just delete the RegionLink record, or replace it's region_id.
If you find yourself duplicating a lot of region-related code in your Event, etc. models, you may want to put it into a module in lib or app/models:
module Regioned
def self.inluded(base)
base.class_eval do
has_one :region_link, :as => :thing
has_one :region, :through => :region_link
...
end
end
end
class Event < ActiveRecord::Base
include Regioned
end
class Project < ActiveRecord::Base
include Regioned
end
Checkout the cast about polymorphic associations. They did change a bit in rails 3 though: http://railscasts.com/episodes/154-polymorphic-association?view=asciicast
Related
There are a few post on this subject but the light has not turned on yet.
I'm trying to extend a rails gem/engine Plutus to use acts_as_tenant
Plutus provides a double entry accounting system for an application. One of the limitations is that the design allows for only one customer or one set of books. What I am trying to do is add multi-tenancy using acts_as_tenant with as little as possible modifications to the Plutus engine. The goal is not to significantly alter Plutus with a different fork, but to add a few optional methods or attributes to Plutus that are only used if you want multiple accounts.
I have it semi-working, but need help in finding where to put stuff and help in clear up what is not working. The condensed Plutus models are:
class Account < ActiveRecord::Base
has_many :credit_amounts, :extend => AmountsExtension
has_many :debit_amounts, :extend => AmountsExtension
has_many :credit_transactions, :through => :credit_amounts, :source => :transaction
has_many :debit_transactions, :through => :debit_amounts, :source => :transaction
end
class Amount < ActiveRecord::Base
belongs_to :transaction
belongs_to :account
end
class Transaction < ActiveRecord::Base
belongs_to :commercial_document, :polymorphic => true
has_many :credit_amounts, :extend => AmountsExtension
has_many :debit_amounts, :extend => AmountsExtension
has_many :credit_accounts, :through => :credit_amounts, :source => :account
has_many :debit_accounts, :through => :debit_amounts, :source => :account
end
Then sti classes on Account: Asset, Equity, Expense, Liability, Revenue and sti classes on Amount: DebitAmount, CreditAmount. This is a little beyond my rails knowledge but this may be on of the most compact double entry schemes I've ever seen (I'm not an accountant, but I have had to add accounting features on apps in the past).
Semi-working means that the only thing I've modified in Plutus is adding a tenant_id to the three models and getting acts_as_tenant to extend two of the three models. From the console on the main application I've found that:
Plutus::Account.acts_as_tenant(:tenant)
Plutus::Amount.acts_as_tenant(:tenant)
Plutus::Transaction.acts_as_tenant(:tenant)
works for Account and Transaction, but errors on Amount with uninitialized constant Transaction, and I'm not sure why. Any ideas?
I've read the rails guide on engines and extending with decorators or concerns, but have not figured out how to send acts_as_tenant(:tenant) to the model using those approaches. Where would I put those three lines of code in the main application (providing I figure out how to get Amount to work!)?
Is there a better approach?
I still have a few class methods that I will have to extend or modify, but no use trying that until I get over this first hurdle.
The problem was that Plutus is a name-spaced engine, and while it works fine within the engine, calling it from outside the engine (main app) could raise conflicts.
To fix it, a class_name option was added to the associations.
module Plutus
class Amount < ActiveRecord::Base
belongs_to :transaction, class_name:"Plutus::Transaction"
belongs_to :account, class_name:"Plutus::Account"
validates_presence_of :type, :amount, :transaction, :account
end
end
Still never figured out the best place to stick the ActsAsTenant calls. I stuck them in the Concerns directory and they didn't get called. Ended up putting them in my Tenant model and all is fine.
So I have the following models in my Ruby on Rails setup: users and courses
The courses need to have content_managers and those content_managers are made up of several individuals in the users model.
I'm a newbie, so bear with me. I was thinking of creating a new model called content_managers that has a user_id and a course_id that links the two tables. It makes sense to me that courses HAVE content_managers. However from the users model, it doesn't make sense that users HAVE content_managers. Some of them ARE content_managers.
From that point of view I believe I'm thinking about it incorrectly and need to set up my ActiveRecord in a different manner from what I'm envisioning. Any help is appreciated.
Thanks!
There's no "have" or "are" in ActiveRecord, only "has_many", "has_one" and "belongs_to". With those tools you should be able to do what you want.
An example:
class Course < ActiveRecord::Base
has_many :content_managers
end
class ContentManager < ActiveRecord::Base
has_many :content_manager_members
has_many :users,
:through => :content_manager_members,
:source => :user
end
class ContentManagerMember < ActiveRecord::Base
belongs_to :course_manager
belongs_to :user
end
class User < ActiveRecord::Base
has_many :content_manager_members
has_many :content_managers,
:through => :content_manager_members
end
Be sure to index these correctly and you should be fine, though navigating from User to Course will be slow. You may need to cache some of this in order to find the level of performance you want, but that's a separate issue that will be uncovered during testing.
Whenever implementing something like this, be sure to load it up with a sufficient amount of test data that will represent about 10x the anticipated usage level to know where the ceiling is. Some structures perform very well only at trivial dataset sizes, but melt down when exposed to real-world conditions.
In my Rails 3.1 project, I have some models with lots of associations. Using ActiveRecord association declarations, I end up with model files that look like this:
# app/models/some_model.rb
class SomeModel < ActiveRecord::Base
belongs_to :other_model
has_many :more_models
has_many :yet_more_models, :through => :more_models
has_one :another_model, :dependent => :destroy
# ... these declarations continue,
# and continue,
# and continue,
# all the way down to line 32
end
This quickly becomes exceedingly ugly and dampens my comprehension/motivation/happiness. What can I do to mitigate?
[a] Format/group/indent them in a particular way?
[b] Re-think my data model, as this may be a symptom of poor design
[c] Live with it -- everyone's model files look this way.
is it possible to group them, by different aspects / functionality of your SomeModel ? do these group of associations tend to have quite a lot of accompanying methods in your SomeModel class? if so, dividing your model into a few modules (like traits), one for every aspect, bundling everything including class methods and association declarations, may help.
e.g.
class SomeModel
include SomeModel::ThisBehavior
include SomeModel::ThatFeature
end
and
module SomeModel::ThisBehavior
extend ActiveSupport::Concern
included do
has_many :this
has_many :that
belongs_to :those
attr_protected :a, :b
attr_accessor :c, :d
end
def do_this
end
...
module ClassMethods
...
end
end
The next step could be trying to make those modules quite agnostic, and group your tests accordingly.
The general rule of thumb is to align related assignments vertically. That carries through to related declarations too.
class SomeModel < ActiveRecord::Base
belongs_to :other_model
has_many :more_models
has_many :yet_more_models, :through => :more_models
has_one :another_model, :dependent => :destroy
end
If you think this is verbose, you haven't seen DataMapper models :P
You can have a model with lots of associations and it's fine for me. If there is a complex logic behind, it will result on a complex bunch of associations. For example, I have an Account class that has over 60 associations: users, companies, centers, products, documents, routes, vehicles .....
This question is more about readability. First of all, decide a convention and follow the same rule all over the project (belongs_to first, has_one second, has_many third, habtm last)
Second advice: If some relations are clearly related with a well separated functionality, you can split your class into some modules keeping each relation in the modules it concerns. But this is a general rule.
class Account < ActiveRecord::Base
include Account::CRM
include Account::Plans
include Account::Finances
end
Maybe you can distribute parent models into others...
Example, I have an app that uses three different instances of User:
class User < ActiveRecord::Base
has_one :social_profile
has_one :tasks_profile
has_one :bank_account
end
And other models that represents user in other project scopes:
class SocialProfile < ActiveRecord::Base
belongs_to :user
has_many :many_things
...
end
Same for TasksProfile and BankAccount.
I have the following setup:
class Publication < ActiveRecord::Base
has_and_belongs_to_many :authors, :class_name=>'Person', :join_table => 'authors_publications'
has_and_belongs_to_many :editors, :class_name=>'Person', :join_table => 'editors_publications'
end
class Person < ActiveRecord::Base
has_and_belongs_to_many :publications
end
With this setup I can do stuff like Publication.first.authors. But if I want to list all publications in which a person is involved Person.first.publications, an error about a missing join table people_publications it thrown. How could I fix that?
Should I maybe switch to separate models for authors and editors? It would however introduce some redundancy to the database, since a person can be an author of one publication and an editor of another.
The other end of your associations should probably be called something like authored_publications and edited_publications with an extra read-only publications accessor that returns the union of the two.
Otherwise, you'll run in to sticky situations if you try to do stuff like
person.publications << Publication.new
because you'll never know whether the person was an author or an editor. Not that this couldn't be solved differently by slightly changing your object model.
There's also hacks you can do in ActiveRecord to change the SQL queries or change the behavior of the association, but maybe just keep it simple?
I believe you should have another association on person model
class Person < ActiveRecord::Base
# I'm assuming you're using this names for your foreign keys
has_and_belongs_to_many :author_publications, :foreign_key => :author_id
has_and_belongs_to_many :editor_publications, :foreign_key => :editor_id
end
See comments for updates.
I've been struggling to get a clear and straight-forward answer on this one, I'm hoping this time I'll get it! :D
I definitely have a lot to learn still with Rails, however I do understand the problem I'm facing and would really appreciate additional help.
I have a model called "Task".
I have an abstract model called "Target".
I would like to relate multiple instances of subclasses of Target to Task.
I am not using single table inheritance.
I would like to query the polymorphic relationship to return a mixed result set of subclasses of Target.
I would like to query individual instances of subclasses of Target to obtain tasks that they are in a relationship with.
So, I figure a polymorphic many to many relationship between Tasks and subclasses of Targets is in order.
In more detail, I will be able to do things like this in the console (and of course elsewhere):
task = Task.find(1)
task.targets
[...array of all the subclasses of Target here...]
But! Assuming models "Store", "Software", "Office", "Vehicle", which are all subclasses of "Target" exist, it would be nice to also traverse the relationship in the other direction:
store = Store.find(1)
store.tasks
[...array of all the Tasks this Store is related to...]
software = Software.find(18)
software.tasks
[...array of all the Tasks this Software is related to...]
The database tables implied by polymorphic relationships appears to be capable of doing this traversal, but I see some recurring themes in trying to find an answer which to me defeat the spirit of polymorphic relationships:
Using my example still, people appear to want to define Store, Software, Office, Vehicle in Task, which we can tell right away isn't a polymorphic relationship as it only returns one type of model.
Similar to the last point, people still want to define Store, Software, Office and Vehicle in Task in one way shape or form. The important bit here is that the relationship is blind to the subclassing. My polymorphs will initially only be interacted with as Targets, not as their individual subclass types. Defining each subclass in Task again starts to eat away at the purpose of the polymorphic relationship.
I see that a model for the join table might be in order, that seems somewhat correct to me except that it adds some complexity I assumed Rails would be willing to do away with. I plea inexperience on this one.
It seems to be a small hole in either rails functionality or the collective community knowledge. So hopefully stackoverflow can chronicle my search for the answer!
Thanks to everyone who help!
You can combine polymorphism and has_many :through to get a flexible mapping:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :targets, :through => :assignment
end
class Store < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :through => :assignment, :as => :target
end
...And so forth.
Although the answer proposed by by SFEley is great, there a some flaws:
The retrieval of tasks from target (Store/Vehicle) works, but the backwards wont. That is basically because you can't traverse a :through association to a polymorphic data type because the SQL can't tell what table it's in.
Every model with a :through association need a direct association with the intermediate table
The :through Assignment association should be in plural
The :as statement wont work together with :through, you need to specify it first with the direct association needed with the intermediate table
With that in mind, my simplest solution would be:
class Assignment < ActiveRecord::Base
belongs_to :task
belongs_to :target, :polymorphic => true
end
class Task < ActiveRecord::Base
has_many :assignments
# acts as the the 'has_many targets' needed
def targets
assignments.map {|x| x.target}
end
end
class Store < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment
end
class Vehicle < ActiveRecord::Base
has_many :assignments, as: :target
has_many :tasks, :through => :assignment, :as => :target
end
References:
http://blog.hasmanythrough.com/2006/4/3/polymorphic-through
The has_many_polymorphs solution you mention isn't that bad.
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => [:store, :software, :office, :vehicle]
end
Seems to do everything you want.
It provides the following methods:
to Task:
t = Task.first
t.targets # Mixed collection of all targets associated with task t
t.stores # Collection of stores associated with task t
t.softwares # same but for software
t.offices # same but for office
t.vehicles # same but for vehicles
to Software, Store, Office, Vehicle:
s = Software.first # works for any of the subtargets.
s.tasks # lists tasks associated with s
If I'm following the comments correctly, the only remaining problem is that you don't want to have to modify app/models/task.rb every time you create a new type of Subtarget. The Rails way seems to require you to modify two files to create a bidirectional association. has_many_polymorphs only requires you to change the Tasks file. Seems like a win to me. Or at least it would if you didn't have to edit the new Model file anyway.
There are a few ways around this, but they seem like way too much work to avoid changing one file every once in a while. But if you're that dead set against modifying Task yourself to add to the polymorphic relationship, here's my suggestion:
Keep a list of subtargets, I'm going to suggest in lib/subtargets formatted one entry per line that is essentially the table_name.underscore. (Capital letters have an underscore prefixed and then everything is made lowercase)
store
software
office
vehicle
Create config/initializers/subtargets.rb and fill it with this:
SubtargetList = File.open("#{RAILS_ROOT}/lib/subtargets").read.split.reject(&:match(/#/)).map(&:to_sym)
Next you're going to want to either create a custom generator or a new rake task. To generate your new subtarget and add the model name to the subtarget list file, defined above. You'll probably end up doing something bare bones that makes the change and passes the arguments to the standard generator.
Sorry, I don't really feel like walking you through that right now, but here are some resources
Finally replace the list in the has_many_polymorphs declaration with SubtargetList
class Task < ActiveRecord::Base
has_many_polymorphs :targets, :from => SubtargetList
end
From this point on you could add a new subtarget with
$ script/generate subtarget_model home
And this will automatically update your polymorphic list once you reload your console or restart the production server.
As I said it's a lot of work to automatically update the subtargets list. However, if you do go this route you can tweak the custom generator ensure all the required parts of the subtarget model are there when you generate it.
Using STI:
class Task < ActiveRecord::Base
end
class StoreTask < Task
belongs_to :store, :foreign_key => "target_id"
end
class VehicleTask < Task
belongs_to :vehicle, :foreign_key => "target_id"
end
class Store < ActiveRecord::Base
has_many :tasks, :class_name => "StoreTask", :foreign_key => "target_id"
end
class Vehicle < ActiveRecord::Base
has_many :tasks, :class_name => "VehicleTask", :foreign_key => "target_id"
end
In your databse you'll need:
Task type:string and Task target_id:integer
The advantage is that now you have a through model for each task type which can be specific.
See also STI and polymorphic model together
Cheers!
This may not be an especially helpful answer, but stated simply, I don't think there is an easy or automagic way to do this. At least, not as easy as with simpler to-one or to-many associations.
I think that creating an ActiveRecord model for the join table is the right way to approach the problem. A normal has_and_belongs_to_many relationship assumes a join between two specified tables, whereas in your case it sounds like you want to join between tasks and any one of stores, softwares, offices, or vehicles (by the way, is there a reason not to use STI here? It seems like it would help reduce complexity by limiting the number of tables you have). So in your case, the join table would also need to know the name of the Target subclass involved. Something like
create_table :targets_tasks do |t|
t.integer :target_id
t.string :target_type
t.integer :task_id
end
Then, in your Task class, your Target subclasses, and the TargetsTask class, you could set up has_many associations using the :through keyword as documented on the ActiveRecord::Associations::ClassMethods rdoc pages.
But still, that only gets you part of the way, because :through won't know to use the target_type field as the Target subclass name. For that, you might be able to write some custom select/finder SQL fragments, also documented in ActiveRecord::Associations::ClassMethods.
Hopefully this gets you moving in the right direction. If you find a complete solution, I'd love to see it!
I agree with the others I would go for a solution that uses a mixture of STI and delegation would be much easier to implement.
At the heart of your problem is where to store a record of all the subclasses of Target. ActiveRecord chooses the database via the STI model.
You could store them in a class variable in the Target and use the inherited callback to add new ones to it. Then you can dynamically generate the code you'll need from the contents of that array and leverage method_missing.
Have you pursued that brute force approach:
class Task
has_many :stores
has_many :softwares
has_many :offices
has_many :vehicles
def targets
stores + softwares + offices + vehicles
end
...
It may not be that elegant, but to be honest it's not that verbose, and there is nothing inherently inefficient about the code.