Rails - Joining 4 tables - ruby-on-rails

Let say we have 4 tables
Client has many Campaign
Campaign has many Adgroup
Adgroup has many Ad
Ad
The relationship is the same, one to many. Let say we have known Ad.id, and we want to get Client.id from that Ad.id.
Could anybody give an elegant solution (by elegant i mean without writing sql statement, instead use active records procedure)?

Make sure that Ad belongs_to an Adgroup, and has a reference to it in it's db structure! Adgroups should belongs_to Campaigns and so on, all the way up. Remember, the dude with belongs_to is the one that needs to hold a reference in his db table to the guy that has_one him, not the other way round!
If the resources are created OK to begin with, you should be able to call #ad.adgroup.campaign.client.id and get a particular ad's (obtained, say, by #ad = Ad.find(some_id)) id.
Hope this helps!

With Rails associations, you have to remember to specify belongs_to for the other side of the relationship, so an ad will most likely has_one Adgroup and so on and so forth up the chain. Once you've coupled the associations, you can use ActiveRecord to method chain these models starting from the bottom, going up to the top of the hierarchy. So you would start with ad and chain it like:
#ad = Ad.find(an_id_or_name_or_whatever).Adgroup.Campaign.Client.id
Looking at the above, you can chain the Adgroup onto an Ad because of the associative relationship which gives you access to the methods of that parent model, all the way up to the Client model, of which .id is a method, and you can call it.
Take a look at some association basics from Rails here:
http://guides.rubyonrails.org/association_basics.html

First, ensure you have your relationships setup as follows:
class Ad < ActiveRecord::Base
belongs_to :ad_group, inverse_of: :ads
end
class AdGroup < ActiveRecord::Base
belongs_to :campaign, inverse_of: :ad_groups
has_many :ads, inverse_of :ad_group
end
class Campaign < ActiveRecord::Base
belongs_to :client, inverse_of: :campaigns
has_many :ad_groups, inverse_of :campaign
end
class Client < ActiveRecord::Base
has_many :campaigns, inverse_of :client
end
Use joins and pluck if all you want is the client id and efficient SQL:
Client.joins(campaigns: {ad_groups: :ad}).where(
ads: { id: some_id }).pluck('clients.id').first
If you want the entire client and efficient SQL then just:
Client.joins(campaigns: {ad_groups: :ad}).where(ads: { id: some_id }).first

Related

Rails field on join entity in has_many through relationship

I may be going about this the wrong way but after reading various SO articles and the Rails docs on associations and scopes, I'm not much wiser.
I have a many-to-may relationship expressed like so:
class User < ActiveRecord::Base
has_many :user_program_records
has_many :programs, through: :user_program_records
end
class Program < ActiveRecord::Base
has_many :user_program_records
has_many :users, through: :user_program_records
end
class UserProgramRecord < ActiveRecord::Base
belongs_to :user
belongs_to :program
# has a field "role"
end
The idea is that there are many users in the system and many programs. Programs have many users in them and users may belong to multiple programs. However - within a given program, a user can only have one role.
What I'd really like to be able to write is:
Program.first.users.first.role
and have that return me the role (which is just a String).
What's the cleanest way to do this? Basically, once I've scoped a user to a given program, how do I cleanly access fields on the relevant join table?
You are thinking about it slightly wrong:
user.role
Would be very ambiguous as a user can have different roles in different programs. Instead you need to think of the join entity as a thing of its own.
The easiest way is to select the join model directly:
program = Program.includes(:user_program_records, :users).first
role = program.user_program_records
.find_by(user: program.users.first)
.role
You can use stuff like association extensions and helper methods to make this a bit sexier.

Are indirect cyclic dependencies ok?

I'm the middle of creating a RoR application which needs a Many-to-many association between the same table (at least in theory).
How so? Well, I'd need a User table which contains two kind of users: Server, and client, more or less like the idea of a Teacher and a student (with private lessons, but with multiple teachers), or a Doctor and a Patient
My first idea was to simply make a User table (you know, login, email, and personal info) and assign it a Role (Server, or client), but then I thought that making such association with a third-table would troublesome
USER <-----> USER_USER
But the idea of creating two "login" tables that represent each role, and a third-table for the association sounds wrong.
Client_Login <-----thru---> Client_Server <---thru---> Server
For simplicity sake, a client cannot be a server to another clients, and a server cannot be a client for another server.
Obviously, a server can have multiple clients, and a client has multiple servers
How would recommend modeling this relationship?
If you need to explicitly have different methods between the two, Server and Client, which I am assuming since you want different classes. Then you might want to look into Single Table Inheritance(STI). This will allow you to use one User table, but have two different models that use it.
class User < ActiveRecord::Base
belongs_to :another_model #example association that will exist for all user types
self.inheritance_column = :role
# if you need to be able to tell what role are available
def self.roles
%w(Client Server)
end
end
class Client < User
has_many :server_clients
has_many :servers, through: :server_clients
end
class Server < User
has_many :server_clients
has_many :clients, through: :server_clients
end
You then have to just setup a simple server_client.rb model for the bridge.
example from here: http://samurails.com/tutorial/single-table-inheritance-with-rails-4-part-1/
This will allow you to put common functionality for all Users in the User class, and specific functionality in the respective classes of Server and Client.
It's done all the time. It's quite common to have a many-to-many back to yourself. It's common in hierarchies dealing with people's relations to each other, (dependency, managers, children, etc... )
class User
has_many :user_relations, dependent: destroy, inverse_of: :user
has_many :dependent_users, through: :user_relations
has_many :dependent_upon_users, through: user_relations, source:
:dependent_upon
end
class UserRelation < ActiveRecord::Base
belongs_to :user
belongs_to :dependent_upon, class_name: User
validates_presence_of :user, :dependent_upon
end

Include a record by default in any association

I have a Client Model as below:
class Client < ActiveRecord::Base
has_many :custodians,:dependent => :destroy
I have a Custodian Model as below:
class Custodian < ActiveRecord::Base
belongs_to :client
In my custodians table I have record with id = 0 , name = 'N/A' that I want to include in all my collection_selects irrespective of the client_id.
e.g for client_id = 10 I want the following in collection_select
Custodian.where('client_id = 10 or client_id = 0')
I know I can do it in my views but I have too many views so it is not practical. Plus I want a more DRY method on either Custodian model or associations. I tried default_scope on Custodian model but could not get it to work.
Basically I am looking for way to always include custodian with id=0 in each association and collection_select.
You can't do what you want using a has_many and belongs_to approach. To implement a belongs_to relationship, the Custodian record has to have a single client_id field. Your logic requires that the custodian_id=0 record belong to many Client records, so it would have to have many client_id fields, but it can only have one. See the Rails Guides-Active Record Associations-The belongs_to Association (http://guides.rubyonrails.org/association_basics.html)
You can accomplish what you want by using a has_and_belongs_to_many relationship. By making both the Custodian and Client models has_and_belongs_to_many to each other, you will be able to have the custodian_id=0 record belong to many Client records and all the other Custodian records will only belong to one client (even though they could belong to many, your program logic must only allow them to belong to one.) See the has_and_belongs_to_many section of the above Rails Guide. To be clear, here is how your models would look:
class Client < ActiveRecord::Base
has_many_and_belongs_to_many :custodians
end
class Custodian < ActiveRecord::Base
has_many_and_belongs_to_many :client
end
Also, because of your special case on custodian_id=0, you will need to establish the look-up table record for the custodian_id=0 record relationship using an active_record callback (probably before_validation or before_create) when you create a new Client record.
Similarly, you will need to implement your own :dependent => :destroy functionality using the before_destroy callback to preserve the custodian_id=0 record and delete all the other associated Custodian records. You'll also have to destroy the corresponding look-up table entries.
This sounds like a lot of work, but if you absolutely must have the custodian_id=0 record associated with every Client, this is the only way I can see it being done. You may want to evaluate it this is really necessary. There may be other program logic that could allow you to get to similar results without going through this process.
You could use an instance or class method:
#app/models/client.rb
Class Client < ActiveRecord::Base
has_many :custodians,:dependent => :destroy
def inc_zero(id)
where("client_id = ? OR client_id = 0", id)
end
def self.inc_zero_custodians(id)
joins(:custodians).where("client_id = ? OR client_id = 0", id)
end
end
#-> Client.custodians.inc_zero(10)
#-> Client.inc_zero_custodians(10)

RoR: "belongs_to_many"? Association headache

I can't seem to wrap my head around this, so I thought I'd post and see if anyone could help me out (please pardon the question if it's insultingly simple: it's complicated to me right now!)
I have these models:
order
service
customer
I think they speak for themselves: a service is what the customer buys when they place an order.
Ok.
So, naturally, I setup these relationships:
# a customer can have many orders
class Customer
has_many :orders
end
# an order belongs to a single customer and can have many services
class Order
belongs_to :customer
has_many :services
end
... but here's where I trip up:
# a service can belong to many orders
class Service
# belongs_to :order ???
end
Because my understanding of belongs_to is that--if I put it there--a service could only belong to one order (it would have only one value in the order_id key field--currently not present--tying it to only one order, where it needs to be able to belong to many orders).
What am I missing here?
There are two ways to handle this. The first is a rails-managed many-to-many relationship. In this case, you use a "has_and_belongs_to_many" relationship in both the Order and Service models. Rails will automatically create a join table which manages the relationships. The relationships look like this:
class Order
has_and_belongs_to_many :services
end
class Service
has_and_belongs_to_many :orders
end
The second way is to manage the join table yourself through an intermediate model. In this case, you might have another model called "LineItem" that represents a Service in the context of an Order. The relationships look like this:
class LineItem
belongs_to :order
belongs_to :service
end
class Order
has_many :line_items
end
class Service
has_many :line_items
end
I prefer the second myself. It's probably just me, but I don't get as confused about what's going on when it's explicit. Plus if I ever want to add some attributes to the relationship itself (like perhaps a quantity in your case) I'm already prepared to do that.
class Customer
has_many :orders
end
class Service
has_many :orders
end
class Order
belongs_to :customer
belongs_to :service
end
The Order should have customer_id and service_id, because it is in a many-to-one relationship with both.
I think this Railscast will help you out - basically you have 2 options. You can use has_and_belongs_to_many or has_many :through.
You will also find that has_and_belongs_to_many has been deprecated in favor of has_many :though => model_name which gives the same (and more) functionality.
I think you have realized this but your order is really a composite domain model, sometimes called an aggregate in DDD speak.
Your Service is a really a listing of some product/service that someone can order. Your order aggregate records what someone ordered.
The aggregate as someone else said is made up of a header, the Order, which includes things like who ordered, the date, does it include taxes, shipping charge, etc. And the Order has_many OrderLineItem's. The OrderLineItem belongs_to Service and contains things like the quantity ordered, belongs_to the product/service, etc.
class Customer < ActiveRecord::Base
has_many :orders
end
class Order < ActiveRecord::Base
belongs_to :customer
end
class OrderLineItem < ActiveRecord::Base
belongs_to :Order
end
I personally use the OrderLineItem model name in deference to LineItem because in a system that needs to ship real products, as opposed to services, you might have allocations that link orders to inventory which would have line items and shipments that get the allocated product to the client, which also have line items. So the line item term can become very overloaded. This likely is not the case in your model because you're only doing services.

Rails has_one :through association

Rails has a has_one :through association that helps set up a one-to-one association with a third model by going through a second model. What is the real use of that besides making a shortcut association, that would otherwise be an extra step away.
Taking this example from the Rails guide:
class Supplier < ActiveRecord::Base
has_one :account
has_one :account_history, :through => :account
end
class Account < ActiveRecord::Base
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ActiveRecord::Base
belongs_to :account
end
might allow us to do something like:
supplier.account_history
which would otherwise be reached as:
supplier.account.history
If it's only for simpler access then technically there could be a one-to-one association that connects a model with some nth model going through n-1 models for easier access. Is there anything else to it that I am missing besides the shortcut?
Logic, OK it might sound a bit weak for this but it would be logical to say that "I have a supplier who has an account with me, I want to see the entire account history of this supplier", so it makes sense for me to be able to access account history from supplier directly.
Efficiency, this for me is the main reason I would use :through, simply because this issues a join statement rather than calling supplier, and then account, and then account_history. noticed the number of database calls?
using :through, 1 call to get the supplier, 1 call to get account_history (rails automatically uses :join to retrieve through account)
using normal association, 1 call to get supplier, 1 call to get account, and 1 call to get account_history
That's what I think =) hope it helps!
I'm surprised no one has touched on Association Objects.
A has_many (or has_one) :through relationship facilitates the use of the association object pattern which is when you have two things related to each other, and that relation itself has attributes (ie a date when the association was made or when it expires).
This is considered by some to be a good alternative to the has_and_belongs_to_many ActiveRecord helper. The reasoning behind this is that it is very likely that you will need to change the nature of the association or add to it, and when you are a couple months into a project, this can be very painful if the relationship were initially set up as a has_and_belongs_to_many (the second link goes into some detail). If it is set up initially using a has_many :through relationship, then a couple months into the project it's easy to rename the join model or add attributes to it, making it easier for devs to respond to changing requirements. Plan for change.
Inverse association: consider the classic situation user-membership-group. If a user can be a member in many groups, then a group has many members or users, and a user has many groups. But if the user can only be a member in one group, the group still has many members: class User has_one :group, :through => :membership but class Group has_many :members, :through => memberships. The intermediate model membership is useful to keep track of the inverse relationship.
Expandability: a has_one :through relationship can easy be expanded and extended to a has_many :through relationship

Resources