Moving a many to many to a one to one in rails - ruby-on-rails

I have a relationship between two models that is currently a many to many. I'd like to move it to a one to one relationship, because that's what all of the data has turned out to be.
Is there a seamless way to do this? Via a migration and model change combo? My models would be simple to just change as shown below:
Now
class App < ActiveRecord::Base
has_and_belongs_to_many :owners
end
class Owner < ActiveRecord::Base
has_and_belongs_to_many :apps
end
Changing to
class App < ActiveRecord::Base
belongs_to :owner
end
class Owner < ActiveRecord::Base
has_one :app
end
But how do I update the database to reflect these changes without losing any of the data I currently have?

Add an owner_id field to your apps table and then iterate through your apps_owners table and set the owner_id field in apps to the owner_id field in apps_owners for the apps record whose id is equal to app_id.
Assuming you're correct and there aren't multiple apps_owners entries for the same app_id following SQL should do it (not tested), although I'm not familiar with how to incorporate raw SQL into migrations:
update apps set owner_id=(select owner_id from apps_owners where apps.id = apps_owners.app_id)

You won't be able to do this simply. The main problem is that has_and_belongs_to_many relationships use a tertiary database table (apps_owners in your case) to hold the relationship information.
After you create the appropriate migration to add owner_id to your App model, you'll need to create a rake task that reads the app_owner table and re-creates the relationships.

Related

act_as_tenant with child records

I am working on a Rails project with act_as_tenant. At the moment, I have a model that looks like this:
class Portfolio < ApplicationRecord
acts_as_tenant :organization
has_many :holdings
end
Additionally, I have this Holding model:
class Holding < ApplicationRecord
belongs_to :portfolio
end
act_as_tenant is working correctly for the Portfolio model. That is, if I am logged in as an organization that does not match the Portfolio's organization, that Portfolio won't exist in the database.
However, as I am logged in as a different organization, I can still access the Holding. But when I try to do holding.portfolio, I get nil.
Is there a way to extend this sort of act_as_tenant logic so that all child models of a parent model will automatically be scoped by tenant as well?
Reading the documentation for acts_as_tenant:
Row-level multitenancy each model must have a tenant ID column on it. This makes it easy to filter records for each tenant using your standard database columns and indexes. ActsAsTenant uses row-level multitenancy.
What this implies that also the holding model should have acts_as_tenant :organization and have a foreign key to organization. This should be the case also for other records you plan to add that are dependent on acts_as_tenant

Rails associations logical vs code

I am currently building my first application with Ruby on Rails (version 5.2.4.2 with ruby 2.6.3) and am having an issue with one of the associations. In my model there are applications in which you supply a single company that you are applying to. Logically I would like to be able to get an application and say application.company = Company.find_by(...).
To make this work, I have
# app/models/Application.rb
class Application < ApplicationRecord
has_one: company
end
# app/models/Company.rb
class Company < ApplicationRecord
end
# database migration
Class AddCompanyToApplication
def change
add_reference :companies, :application, foreign_key: true
end
end
Doing this allows me to use the desired syntax but has one issue. When I create a second application for the same company, the row in the companies table is changed that removes the company from the first application.
The list of companies is predetermined and I just want to refer to a company from within the application. Is there a way to be able to use the assignment syntax while having the foreign key placed in application rather than companies? From a database perspective I feel like application should hold the foreign key, but this breaks the assignment syntax. It seems like Ruby wants me to make Company have has_many :applications, but this breaks the semantics of what I am trying to accomplish. Is there a way to change the foreign key placement or should I forgo the assignment syntax and stick to SQL and assigning directly to id's?
Right now, you have a 1 to 1 relationship. An application has_one :company and a company belongs_to :application. That's why when you try to create a second application for the same company, the application_id in the companies table gets overridden. If you change your company association to has_many :applications, you should be able to add more than one application to a company. Then you can call application.company to return the application's current company or application.company = ... to assign a company to the current application.
class Application < ApplicationRecord
has_one :company
end
class Company < ApplicationRecord
has_many :applications
end
To make this work, you have to create a new migration:
Class AddCompanyIdToApplication
def change
add_reference :application, :company, foreign_key: true
end
end
Before you do that, you have to rollback your existing AddCompanyToApplication migration (which is named wrong, by the way, because you're not adding the company to the application, you're adding the application to the company) and delete the AddCompanyToApplication migration.
Read more about associations
Read more about add_reference

setting up a belongs_to relation when the foreign key is stored in metadata

In a Rails 4 application, I have an STI model that stores metadata in a jsonb column.
Base Class:
class Post < ActiveRecord::Base
...
end
Subclass:
class JobPost < Post
# has a jsonb column for metadata
end
One of the data attributes in the metadata column of a JobPost is a foreign_key reference to another table (company_id). I'd like to add a belongs_to :company reference in the JobPost model. It seems like this should be possible by doing something like
class JobPost < Post
belongs_to :company do
Company.find_by_id self.metadata['company_id']
end
end
but that doesn't appear to work. Help?
Note: I am not necessarily intent on using belongs_to rather than writing needed methods like def company by hand, but I do need a way to eager load companies when listing job posts. If there's a way to do that eager loading without a belongs_to relation I'm all ears.
Update1
I have also tried the following, which doesn't appear to work either:
class JobPost < Post
belongs_to :company, foreign_key: "(posts.metadata->>'company_id')::integer".to_sym
end
Update2
To be more clear about my intentions and need:
1) A JobPost belongs_to a Company, but a Post (and other subclasses of Post) does not. I'd prefer not to jankily add the company_id column to the posts table when it won't be used by the other subclasses.
2) A JobPost could justify having it's own table (perhaps the relationship with a Company is enough to justify it). There are reasons why this wouldn't be ideal, but if that's the only answer I'm open to it. I'd, however, like a more definitive "what you're trying to do can't be done" response before going down this road, though.
The primary question is whether you can customize belongs_to so that it uses the metadata column rather than expecting the foreign key to be a column in the table.
The secondary question is whether you can eager load companies alongside job posts without having that belongs_to relation set up.
EDIT
UPD 2
You need to add "company_id" column to the base class of your STI table. If JobPost inherits from Post, and it should have "company_id" then add the "company_id" column to Post (base table).
Remember STI stands for "Single Table Inheritance" so there is only one table on database schema level. Imagine a column of a Post table, where few data records are the foreign key entries for Companies with company_id and what about the other records of this column with non JobPost subclass types, are they null/empty? Hence the relationship is defined with parent STI table and subclass inherits these relations. Additional type column in STI defines the subclass type.
Check here
You may need to dig further on Polymorphic classes instead of STI if both JobPost and Post have relationship with Company, else create two separate model, as they tend do have some unique relationships and column fields.
UPD
Based on updated ask
app/model/company.rb
class Company < ActiveRecord::Base
has_many :posts
delegate :jobposts, to: :posts
end
app/model/post.rb
class Post < ActiveRecord::Base
belongs_to :company
self.inheritance_column = :ptype
scope :job_posts, -> { where(ptype: 'JobPost') }
def self.ptype
%w(JobPost)
end
end
app/models/jobpost.rb
class JobPost < Post; end
Create a company
company = Company.create!(company_params)
Create some posts and add them to the company
company.posts << JobPost.new(jobpost_params)
To fetch jobpost by company relationship
company.job_posts
In case you are storing company_id in jsonb in any which column, just format your jobpost_params hash input accordingly and it should do the deed for you
OLD ASK
To find by primary key
Company.find(id)
In your case, id is self.metadata['company_id']
To find by other keys
Company.find_by(key: value)
Company.find_by_id is no more recommended
Please remove do and end after belongs_to in your model, instead in your controller you can write:
Jobpost.all.each do |x|
# your do
end
regarding foreign key, as rails is convention over configuration, it by default includes company_id reference to Jobpost which you can change in your Company.rb model

has_one not updating attribute id?

So I have a DB relationship where a User has a PreferenceList. I have set up a preference_list_id in the database table, and I have set up a belongs_to :user relationship with PreferenceList and a has_one :preference_list relationship with User.
Here's the weird part. If I do user.preference_list = preference_list, I can access user.preference_list and it will give me the correct instance of preference_list. However, if I do user.preference_list_id (a valid, column, I've checked), it gives me nothing even after I have added preference_list to user.preference_list. I need the id to be updated for various database operations. What am I doing wrong?
The foreign key goes on the table for the class declaring the belongs_to association. So if you have
class PreferenceList < ActiveRecord::Base
belongs_to :user
end
class User < ActiveRecord::Base
has_one :preference_list
end
table preferences_lists should have user_id columns. Rails will handle this columns properly. You can add preference_list_id column to users table as well. But this column has no special meaning, Rails will do nothing with it by default. Try to change belongs_to/has_one sides if you really need to operate preference_list_id. Or use user.preference_list.id

rails model relationship and migration

I have some problem trying to understand when building a rails app with several models and relation ships between them...
If I take a basic example like a model Group, a model User and a model Car
class Group < ActiveRecord::Base
has_many :users
end
class User < ActiveRecord::Base
belongs_to :group
has_many :cars
end
class Car < ActiveRecord::Base
belongs_to :user
end
Will those relation ship statements automatically create the following functions:
group.users
user.group
user.cars
car.user
It seems that we sometimes need to have to create "references" in migration (like adding a reference toward User in Car table) but is this always required ?
In this case, what is the difference of creating the migration and of adding the relationship statement in the models ? I sometimes have the feeling this is used for the same purpose.
Thanks a lot for your help,
Regards,
Luc
The association declarations are there for Rails only. You have to define the foreign keys (references) in the database, so that Rails can properly save the data.
Remember, despite all the magic, it's still backed by a relational database, so good practices there will pay off in the long run.

Resources