I have 2 models: Link and User such as:
class Link < ActiveRecord::Base
belongs_to :src_user
belongs_to :dst_user
end
class User < ActiveRecord::Base
has_many :links
end
A schema could looking like:
+----------+ +------+
| Link | | User |
+----------+ |------+
| src_user |---->| |
| dst_user |---->| |
+----------+ +------+
My question is: how can I edit User model do in order to do this
#user.links # => [list of links]
(...which should query #user.src_users + #users.dst_users, with unicity if possible.)
Can we do this only using SQL inside ActiveRecord?
Many thanks.
(note: I'm on Rails 3.1.1)
You have to specify multiple relations inside the user model so it knows which specific association it will attach to.
class Link < ActiveRecord::Base
belongs_to :src_user, class_name: 'User'
belongs_to :dst_user, class_name: 'User'
end
class User < ActiveRecord::Base
has_many :src_links, class_name: 'Link', inverse_of: :src_user
has_many :dst_links, class_name: 'Link', inverse_of: :dst_user
end
The :class_name option must be specified since the association name is not simply :links. You may also need to specify the :inverse_of option in the Link model, but I can't be sure of that. It wouldn't hurt if you did, though.
In order to do your #user.links call, you'll have to do something like this:
class User < ActiveRecord::Base
def links
Link.where(['src_user = ? OR dst_user = ?', self.id, self.id])
end
end
… since ActiveRecord doesn't provide a way to merge two associations on the same model into one.
This sounds very similar to the child/mother/father problem, which has a solution. That is, a problem where an object of one type belongs_to more than one object of the same type (not necessarily the same type as the child, but the objects it belongs to are themselves the same type).
This solution is quite old, and may not be up to date with the latest style schemes in Rails3, but it should work fine, or with very little modification.
http://railsforum.com/viewtopic.php?id=1248
Related
I have a tag system on multiple models that are linked together.
The system works like this:
A Top has many Middles
A Middle has many Lows
Tops, Middles and Lows have many Tags
A tag associated to Top level is supposed to qualify every Middle and Low associated to it.
Same goes for a tag that would be associated to a Middle, every Low associated to it would 'inherit' from the tags.
This mechanic is not on a database level, in the end in what concerns the database, Tops, Middles and Lows all have their own tag collection, and i initially implemented instance methods on each model so that when you call, for example, low_instance.all_tags, it concatenates the tag collections of it's parent Middles, and the one of its Top.
Here is what the models look like:
# ______________________________
# / \
# (1) (*)
# [Top] (1) __ (*) [Middle] (*) __ (*) [Low]
# (*) (*) (*)
# \_______________ | ______________/
# |
# *
# [Tags]
class Low < ApplicationRecord
has_many :low_tags, dependent: :destroy
has_many :tags, through: :low_tags
has_many :middle_foos, dependent: :destroy
has_many :middles, through: :middle_foos
end
class Middle < ApplicationRecord
belongs_to :top
has_many :middle_tags, dependent: :destroy
has_many :tags, through: :middle_tags
has_many :middle_lows, dependent: :destroy
has_many :lows, through: :middle_lows
end
class Top < ApplicationRecord
has_many :middles, dependent: :destroy
has_many :lows, dependent: :destroy
has_many :top_tags, dependent: :destroy
has_many :tags, through: :top_tags
end
### Join tables
class MiddleLow < ApplicationRecord
belongs_to :middle
belongs_to :low
end
class LowTag < ApplicationRecord
belongs_to :low
belongs_to :tag
end
class MiddleTag < ApplicationRecord
belongs_to :middle
belongs_to :tag
end
class TopTag < ApplicationRecord
belongs_to :top
belongs_to :tag
end
That actually works like a charm. The issue is that i want to be able to search my Lows with the awesome Ransack gem and using the full tag collection of a Low (its self tags, plus the ones inherited from the parent Middles and Top)
Problem: Ransack only works with ActiveRecord::Relations. So from Ransack's point of view, i can only search my Lows using their self-tags and not the full inherited collection as this does not exist on the database level.
The initial solution to this problem i wanted to implement is to add a "copy" full tag collection on the database level that updates with the rest and that i could use to search with Ransack.
But I'm sure i don't have to add anything to the database as all the info is already here in the join tables and i kind of don't want to duplicate that info which is not super cool i think and would make the code base less understandable.
I have seen potential solutions using has many with scopes like so:
has_many :all_tags, ->(low) {
unscope(.........).
left_joins(..........).
where(.........)
# Returs self tags (Low) + tags from associated Middles + tags from the Top
}
I'm pretty sure this would be the best solution, but I'm really not good when it comes to database querying especially with so much models and join tables. I get confused and can't seem to find what to put in that scope so that i get this full collection of tags.
So if anybody has a clue about that, any help would be greatly appreciated!
By the way, using Rails 6.1 and Ruby 2.7
So found the solution to the query I wanted to construct in the end.
The scope looks like following:
has_many :full_tags, lambda { |low|
where_clause = 'top_tags.top_id = ? or low_tags.low_id = ?'
where_args = [low.top_id, low.id]
if low.middles.any?
where_clause += ' or middle_tags.zone_id IN ?'
where_args << low.middle_ids
end
unscope(where: :low_id)
.left_joins(:middle_tags, :top_tags, :low_tags)
.where(where_clause, *where_args).distinct
}, class_name: 'Tag'
Calling xxx.full_tags on any instance of low returns the whole collection of tags from every middle it belongs to , plus those from the top it belongs to, plus its own tags, and the distinct makes it a unique collection.
That being said, that didn't fully fixed my problem because the whole purpose was to pass this scoped has_many relation as an attribute used by the Ransack gem to filter out my Low models, out of their full inherited collection of tags.
Big disappointment it was when i discovered that Ransack performs eager loading when it comes to search on associations:
Rails does not support Eager Loading on scoped associations
So i ended up implementing a whole other solution for my tagging system. But hey, i learned a lot.
I am establishing an associated relationship between models for house buying and selling. User will have plural Housing agencies and plural Houses, and conversely, House will also have plural Housing agencies and plural Users. The specific models are as follows:
model User
has_many :agencies
has_many :houses through: :agencies
model Agency
belongs_to :user
belongs_to :house
model House
has_many :agencies
has_many :users through: :agencies
I want to know, if I want to query the users who have joined the agency, can I do this
House.where(id: 10).agency_users
That is, will the agency_users method be generated? If yes, where is the official teaching document written? If not, how should it be fixed?
thanks
To start with you have an error in the definition of your assocation - through is a keyword argument so you need to add a colon:
has_many :houses through: :agencies
What new method does Rails' "has_many :through" generate?
It generates a whole bunch of methods:
| | | has_many
generated methods | habtm | has_many | :through
----------------------------------+-------+----------+----------
others | X | X | X
others=(other,other,...) | X | X | X
other_ids | X | X | X
other_ids=(id,id,...) | X | X | X
others<< | X | X | X
# ...
I don't know where you got agency_users from but its definately not going to be generated from any of the code in your question as its based on the name argument (the first one).
You're also making a classic rookie misstake and calling an assocation method on an ActiveRecord assocation. where returns multiple records while the assocations create instance methods. Use find(10) or find_by(id: 10) (if it should be allowed to be nil).
If yes, where is the official teaching document written? If not, how should it be fixed?
I'm not sure what you're actually asking here or what "official teaching document" is supposed to mean. Rails has both API documentation and guides.
If you're talking about documenting your own code it depends on if you're using RDoc or Yard. Neither will actually automatically pick up the methods generated by the assocation macros as far as I know but you can do it manually in Yard with:
# #!method foo
I don't know if RDoc has a similiar feature so you might to just document the assocations in the overall class description.
These assocations are not what you really want
The main problem here is that you're putting house_id on the agencies table which means that it can only ever record a single value per agency! Thats not going to be good for buisness.
What you actually want is something more like
class Listing
belongs_to :user
belongs_to :agency
end
class Agency
has_many :listings
has_many :clients,
through: :listings,
source: :user
end
class User
has_many :listings
has_many :agencies,
through: :listings
end
I would generally avoid the term House and go with the domain specific listing which can cover many different kinds of properties.
How can you create a has_many association but also be able to refer to the various instances by a term that isn't the name? Sort of like an attribute of the class? (dog1 in this case.)
class User < ActiveRecord::Base
has_many :dogs
dogs: dog1, dog2, dog3, dog4 # I know this won't work but just to give the idea
end
So you can do:
barry = User.create(name: 'Barry')
barry.dog1.create(name: 'rover', weight: 12)
barry.dog1.name #=> rover
barry.dog1.weight #=> 12
There is no clean way to do this in Rails, a workaround is to use has_one:
class User
has_many :dogs
has_one :favorite_dog, class_name: 'Dog', -> { where(favorite: true).order(updated_at: :desc) }
But, the thing is, Rails' has_one is just a has_many with a SQL LIMIT 1. So in fact, the relation some_user.favorite_dog will return only one record even though there could be several "favorited dogs". The has_one :favorite_dog I used as an example should be latest_favorited_dog (because of the order on updated_at).
has_one documentation: https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/has_one
I'm having a bit of difficulty figuring out how to do this in the "Rails" way, if it is even possible at all.
Background: I have a model Client, which has a has_many relationship called :users_and_managers, which is defined like so:
has_many :users_and_managers, -> do
Spree::User.joins(:roles).where( {spree_roles: {name: ["manager", "client"]}})
end, class_name: "Spree::User"
The model Users have a has_many relationship called credit_cards which is merely a simple has_many - belongs_to relationship (it is defined in the framework).
So in short, clients ---has many---> users ---has many---> credit_cards
The Goal: I would like to get all the credit cards created by users (as defined in the above relationship) that belong to this client.
The Problem: I thought I could achieve this using a has_many ... :through, which I defined like this:
has_many :credit_cards, through: :users_and_managers
Unfortunately, this generated an error in relation to the join with the roles table:
SQLite3::SQLException: no such column: spree_roles.name:
SELECT "spree_credit_cards".*
FROM "spree_credit_cards"
INNER JOIN "spree_users" ON "spree_credit_cards"."user_id" = "spree_users"."id"
WHERE "spree_users"."client_id" = 9 AND "spree_roles"."name" IN ('manager', 'client')
(Emphasis and formatting mine)
As you can see in the generated query, Rails seems to be ignoring the join(:roles) portion of the query I defined in the block of :users_and_managers, while still maintaining the where clause portion.
Current Solution: I can, of course, solve the problem by defining a plain 'ol method like so:
def credit_cards
Spree::CreditCard.where(user_id: self.users_and_managers.joins(:credit_cards))
end
But I feel there must be a more concise way of doing this, and I am rather confused about the source of the error message.
The Question: Does anyone know why the AR / Rails seems to be "selective" about which AR methods it will include in the query, and how can I get a collection of credit cards for all users and managers of this client using a has_many relationship, assuming it is possible at all?
The joins(:roles) is being ignored because that can't be appended to the ActiveRecord::Relation. You need to use direct AR methods in the block. Also, let's clean things up a bit:
class Spree::Role < ActiveRecord::Base
scope :clients_and_managers, -> { where(name: %w{client manager}) }
# a better scope name would be nice :)
end
class Client < ActiveRecord::Base
has_many :users,
class_name: "Spree::User",
foreign_key: :client_id
has_many :clients_and_managers_roles,
-> { merge(Spree::Role.clients_and_managers) },
through: :users,
source: :roles
has_many :clients_and_managers_credit_cards,
-> { joins(:clients_and_managers_roles) },
through: :users,
source: :credit_cards
end
With that setup, you should be able to do the following:
client = # find client according to your criteria
credit_card_ids = Client.
clients_and_managers_credit_cards.
where(clients: {id: client.id}).
pluck("DISTINCT spree_credit_cards.id")
credit_cards = Spree::CreditCard.where(id: credit_card_ids)
As you can see, that'll query the database twice. For querying it once, check out the following:
class Spree::CreditCard < ActiveRecord::Base
belongs_to :user # with Spree::User conditions, if necessary
end
credit_cards = Spree::CreditCard.
where(spree_users: {id: client.id}).
joins(user: :roles).
merge(Spree::Role.clients_and_managers)
I am basically trying to create an app which allows a user to track what they are working on. So each user will have many projects.
Now each project can have a certain number of types e.g. videos, websites, articles, books etc. with each of these types of projects having many objects (i.e. a videos project would list all the videos they were working on, books project may have a list of books they read).
Each of these objects would have different variables (books might have author and length and websites may have URL, rank etc.)
How would I set up these models in rails?
So basically would I separate my project model from my type model (so project has_one type and type belongs_to project) and then have 4 separate object models (i.e. objectsite) for each of the different types?
Or is there a better way to design this.
Modelling Many Rails Associations
I read this question and I think this may be similar to what I want to do but am not entirely sure as I don't full understand it.
UPDATE**
So far I have:
Projects
has_one type
Type
belongs_to projects
has_many objects
object
belongs_to type
But I am not to sure if this is the best way to go about it because like I said each object for each type will be different
From what I could bring myself to read, I'd recommend this:
Models
So each user will have many projects.
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
end
#app/models/user.rb
Class User < ActiveRecord::Base
has_many :projects
end
Vars
Each of these objects would have different variables (books might have
author and length and websites may have URL, rank etc.)
There are two ways to interpret this
The first is if you know what details your different objects will require, you could include them as attributes in their respective datatables. If you don't know, you'll have to use another table to populate them.
I'll detail both approaches for you:
--
Known Attributes
As Pavan mentioned in the comments, you'll likely benefit from an STI (Single Table Inheritance) for this:
#app/models/project.rb
Class Project < ActiveRecord::Base
belongs_to :user
has_many :books, class_name: "Project::Books"
has_many :folders, class_name: "Project::Folders"
end
#app/models/object.rb
Class Object < ActiveRecord::Base
#fields - id | type | project_id | field1 | field2 | field3 | field4 | created_at | updated_at
belongs_to :project
end
#app/models/project/book.rb
Class Book < Object
... stuff in here
end
This will allow you to call:
project = Project.find(params[:id])
project.books.each do |book|
book.field1
end
--
Unknown Attributes
#app/models/project.rb
Class Project < ActiveRecord::Base
has_many :objects
end
#app/models/object.rb
Class Object < ActiveRecord::Base
belongs_to :project
has_many :options
end
#app/models/option.rb
Class Option < ActiveRecord::Base
#fields - id | object_id | name | value | created_at | updated_at
belongs_to :object
end
This will allow you to call:
project = Project.find(params[:id])
project.objects.first.options.each do |option|
option.name #-> outputs "name" of attribute (EG "length")
option.value #-> outputs "value" of attribute (EG "144")
end
This means you can use option to populate the various attributes your objects may require.