How to add methods to a has_many collection - ruby-on-rails

Assuming a typical has_many association
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
How can I add a method to the collection of orders? For the sake of code organization, I'm trying to reactor this method (this is a made-up example) inside of my Customer class:
def update_orders
ThirdPartyAPI.look_up(self.orders) do |order|
# Do stuff to the orders
# May need to access 'self', the Customer...
end
end
I don't like this because it puts a lot of knowledge about the Order class inside my Customer class. I can't use an instance method off of an order, since the ThirdPartyAPI can do a batch lookup on multiple orders. I could make a static method off of Order and pass in the array of orders, and their parent customer, but this feels clunky.
I found this in the rails docs, but I couldn't find any good examples of how to use this in practice. Are there any other ways?

I think this should do it
has_many :entities do
def custom_function here
end
def custom_function here
end
end

Related

Consistently using associated model subclasses

I have a situation where I have basic models that I want to add business logic to. For example, I might have something like this.
class List < ApplicationRecord
has_many :subscriptions
has_many :subscribers, though: :subscriptions
end
class Subscriber < ApplicationRecord
has_many :subscriptions
has_many :lists, through: :subscriptions
end
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
end
Subscribing and unsubscribing is easy via the normal association methods.
# Subscribe
list.subscriptions.create(
subscriber: subscriber
)
# Unsubscribe
list.subscriptions.destroy(subscription)
# Unsub from all lists
subscriber.subscriptions.destroy_all
But there's logging and tracking and metrics and hooks and other business logic. I could do this with callbacks. However I'd like to keep the basic models simple and flexible. My desire is to separate the core functionality from the extra business logic. Right now this is to simplify testing. Eventually I'll need to add two different sets of business logic on top of the same core.
Currently I'm using a service object to wrap common actions with all the current business logic. Here's a simple example, there's a lot more.
class SubscriptionManager
def subscribe(list, subscriber)
list.subscriptions.create( subscriber: subscriber )
log_sub(subscription)
end
def unsubscribe(subscription)
subscription.list.subscriptions.destroy(subscription)
log_unsub_reason(subscription)
end
def unsubscribe_all(subscriber)
subscriber.subscriptions.each do |subscription|
unsubscribe(subscription)
end
subscriber.lists.reset
subscriber.subscriptions.reset
end
end
But I'm finding it increasingly awkward. I can't use the natural subscriber.subscriptions.destroy_all, for example, but must be careful to go through the SubscriptionManager methods instead. Here's another example where this system caused a hard to find bug.
I'm thinking about eliminating the SubscriptionManager and instead writing subclasses of the models which have the extra logic in hooks.
class ManagedList < List
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :subscribers, though: :subscriptions, class_name: "ManagedSubscriber"
end
class ManagedSubscriber < Subscriber
has_many :subscriptions, class_name: "ManagedSubscription"
has_many :lists, through: :subscriptions, class_Name: "ManagedList"
end
class ManagedSubscription < Subscription
belongs_to :list, class_name: "ManagedList"
belongs_to :subscriber, class_name: "ManagedSubscriber"
after_create: :log_sub
after_destroy: :log_unsub
end
The problem is I'm finding I have to duplicate all the associations to guarantee that Managed objects are associated to other Managed objects.
Is there a better and less redundant way?
I don't really understand why do you need to define the associations again in the subclasses. However, I have a tip that you could use directly in your Subscription model.
If you want to keep your model simple, and don't overload it with callbacks logic, you can create a callback class to wrap all the logic that will be used by the model.
In order to do that, you need to create a class, for example:
class SubscriptionCallbacks
def self.after_create(subscription)
log_sub(subscription)
end
def self.after_destroy(subscription)
log_unsub_reason(subscription)
end
end
Then in Subscription model:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_destroy SubscriptionCallbacks
after_create SubscriptionCallbacks
end
That way, your model stand clean and you can destroy a subscription and apply all custom logic without using a service.
UPDATE
Specifically, what I don't understand is why are you making Single Table Inheritance on three models just to add callbacks to one of them. The way you wrote your question, for the three subclasses you override the associations to use the subclasses created. Is that really necessary? I think that no, because what you want to achieve is just refactor your service as callbacks in order to use destroy and destroy_all directly in the Subscription model, I take that from here:
But I'm finding it increasingly awkward. I can't use the natural subscriber.subscriptions.destroy_all, for example, but must be careful to go through the SubscriptionManager methods instead.
Maybe with conditional callbacks is enough, or even just normal callbacks on your Subscription model.
I don't know how the real code is wrote, but I found tricky to use Single Table Inheritance just to add callbacks. That doesn't make your models "simple and flexible".
UPDATE 2
In a callback class, you define methods with the name of the callback that you want to implement, and pass the subscription as a parameter. Inside that methods, you can create all the logic that you want. For example (assuming that you will use different logic given a type attribute):
class SubscriptionCallbacks
def after_create(subscription)
if subscription.type == 'foo'
log_foo_sub(subscription)
elsif subscription.type == 'bar'
log_bar_sub(subscription)
end
end
private
def log_foo_sub(subscription)
# Here will live all the logic of the callback for subscription of foo type
end
def log_bar_sub(subscription)
# Here will live all the logic of the callback for subscription of bar type
end
end
This could be a lot of logic that will not be wrote on Subscription model. You can use destroy and destroy_all as usual, and if a type of subscription is not defined in the if else, then nothing will happen.
All the logic of callbacks will be wrapped in a callback class, and the only peace of code that you will add to the subscription model will be:
class Subscription < ApplicationRecord
belongs_to :list
belongs_to :subscriber
after_create SubscriptionCallbacks.new
end

How must one implement a has_many alike system for different kind of models?

I have several kinds of Orders; they differ quite a lot in the attributes and behaviour; but they share a common interface (Duck). Roughly like so:
class PackageOrder
has_one :package
def line_items
[package, package.site.first]
end
end
class SiteOrder
has_one :site
def line_items
[site]
end
end
class Customer
has_many :package_orders
has_many :site_orders
end
Is there a way to abstract the various orders on Customer? So that I can say active-recordy things like:
Customer.orders
Customer.orders << PackageOrder.new
And that I don't have to add a has_many line whenever I introduce a new Order variation?
I am not too familiar with all the possible usages of Polymorphic associations, so perhaps that offers a solution? Is this feasible at all?
I could, obviously, do some magic on Customer (pseudocode warning)
class Customer
ORDER_TYPES = [:package_order, :site_order]
class_eval.do
ORDER_TYPES.each {|type| has_many type.pluralize.to_sym}
end
def orders
ORDER_TYPES.map {|type| self.type.pluralize }.flatten
end
end
But I am wondering if ActiveRecord or Rails does not already offer something for me:

Mongoid, complex query, something similar to has_many : through

I have the following models:
class Company
# ...
has_many :invoices
end
class Invoice
# ...
belongs_to :company
has_many :items
field :type, String
scope :expense, where(type: 'expense')
scope :income, where(type: 'income')
end
class Item
# ...
belongs_to :invoice
end
The question is how to fetch all income Items for the given company?
Something similar to company.items.expense
Using embedded relations won't make any difference. Calling company.items.expense would still return an error, since company.items returns an Array.
Try something like this:
class Company
#...
def expenses
self.invoices.where(type: 'expense')
end
def incomes
self.invoices.where(type: 'income')
end
end
Then you can call company.expenses and company.incomes.
Depending on your usage, you may find it better to embed Item inside Invoice or leave it as a separate collection. Also, since you're dealing with invoices, remember to be careful with your callbacks and make them cascade where necessary, so Invoice modified time changes if an Item gets changed.

rails model templates (or instance inheritance) options?

I have a situation where I want to make 'parametric' models in rails; for example I'd like to define PrototypeRecipe, and then be able to make multiple DerivedRecipe's; maybe one derived recipe uses more sugar and another uses less eggs or something. The critical point is that I want all the 'derived' instances to inherit properties from a single shared PrototypeRecipe, but be able to make local modifications.
Ideally, I'd like to be able to define methods on the prototype (say, putting together a shopping list), and have these methods respond to local changes in derived instances (so if I specified 3 eggs instead of 2, i could call the prototype's make_shopping_list function and it would reflect that).
Is there an existing method for accomplishing something like this? Here's the best I can come up with so far:
class Ingredient << ActiveRecord::Base
belongs_to :recipe, :polymorphic => true
# uuid => UUID String (for grouping ingredients which change between prototype and derived instances)
end
class PrototypeRecipe << ActiveRecord::Base
has_many :ingredients
def make_ingredient_list(derived_recipe = nil)
self.ingredients.map {|i| derived_recipe.nil? ? i : derived_recipe.ingredients.where(:ingredient_uuid => i.uuid).first }
end
end
class DerivedRecipe << ActiveRecord::Base
belongs_to :prototype_recipe
has_many :ingredients
def method_missing(sym, *args)
self.prototype_recipe.send( sym, *args, self)
end
end
I know this code can be made a lot cleaner, I'm more wondering if the general approach can be improved on. The basic idea is that ingredients would each have a unique ID. To modify a prototype recipe, you simply create an instance of DerivedRecipe, link it to the prototype, and then add an ingredient with the same UUID as one of the prototype's ingredients.
I'm not 100% on what behavior you are looking to have, so here's my attempted solution.
Single-Table Inheritance (STI). Your base class will be PrototypeRecipe and your child class will be DerivedRecipe.
In your prototype_recipes table, specify a type column (text). This signals to Rails you want to use STI. If you put your make_ingredients_list method inside the base class, it will be accessible from your child classes.
# app/models/ingredient.rb
class Ingredient < ActiveRecord::Base
belongs_to :recipe, :class_name => "PrototypeRecipe"
...
end
# app/models/prototype_recipe.rb
class PrototypeRecipe < ActiveRecord::Base
has_many :ingredients
has_many :derived_recipes
def make_ingredient_list
...
end
end
# app/models/derived_recipe.rb
class DerivedRecipe < PrototypeRecipe
belongs_to :prototype_recipe
end
Now you can do something like:
#cupcakes = PrototypeRecipe.create
#cupcakes_with_extra_eggs = #cupcakes.derived_recipes.create
print #cupcakes_with_extra_eggs.make_ingredient_list
Is this what you were looking for?

Can I access the id of the parent object inside a named_scope when fetching associated objects with the 'others' method?

Let's say you have two models: articles and comments.
class Article < ActiveRecord::Base
has_many :comments
end
You know you can fetch associated comments to an article like this:
article = Article.first
article.comments # => SELECT * FROM "comments" WHERE ("comments".article_id = 123)
Is there a way to explicitly access the article_id (123) within a named_scope?
I need this for a complex named_scope that joins another table. Basically the named_scope will depend on to be called from the associated parent object to make sense (article.comments.my_named_scope and not Comments.my_named_scope).
I don't want to pass the id as a parameter for the named_scope. So, instead of passing the article_id to the named scope with ... lambda { |article| ...} and access the id with "... #{article.id} ...", I want to somehow access this article_id that the others method uses, which I get from the has_many association.
Sounds like what you're actually after is an association extension:
http://guides.rubyonrails.org/association_basics.html#association-extensions
In particular, proxy_owner, which will be the #article in question
eg:
class Article < ActiveRecord::Base
has_many :posts do
def sample_extension
puts "Proxy Owner #{proxy_owner}"
end
end
end
#article.posts.sample_extension
Been struggling with the same issue. You can try this, which is a more elegant than using association extensions:
class Article < ActiveRecord::Base
has_many :posts
end
class Post < ActiveRecord::Base
def self.get_article_id
self.new.article_id
end
end
#article = Article.new
#article.posts.get_article_id
Within class methods for Post, you can now just use get_article_id anywhere you need the ID of the parent article. With proxy associations, I wasn't able to do that.
I like #ajkochanowicz's solution but looks like there's a DB hit involved there (Rails 3.2.x), so just a heads up, not really ideal considering the fact that you already have the parent object on hand somewhere.
For Rails 4 and above
The newer way to do it in Rails4+ is:
class Article < ActiveRecord::Base
has_many :comments do
def my_named_scope
puts "Scope Owner = #{#association.owner}"
end
end
end
article = #article.comments.my_named_scope
Inside the scope my_named_scope, #association.owner returns the Article object that .comments was called on. Hence the article returned by the code above is same as the #article object.
Alternative method
If you don't want to use extensions and would rather avoid the "create a new object and get id from there" method (as described by Chanpory's answer), here is how to do it:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
def self.get_article_id
Comment.scope_attributes["article_id"] # scope_attributes returns a hash of all the attributes inherited from the owner of this scope
end
end
#article = Article.find(10)
#article.comments.get_article_id # returns 10

Resources