Rails associations using an array as the foreign_id - ruby-on-rails

I am looking to create an association where a model can be created and owned by three different entities but also references the other two entities.
For example, I have a performance model where three different types of models can create a performance: venue, artist, band. However, the performance also needs to reference the other two e.g if a venue creates a performance, it needs to list an artist or a band that will be performing. And if an artist creates a performance, then the artist needs to put a venue where he/she will be performing.
So I am starting with something like this:
class CreatePerformances < ActiveRecord::Migration[6.0]
def change
create_table :performances, id: :uuid do |t|
t.belongs_to :venue, index: true
t.belongs_to :artist, index: true
t.belongs_to :band, index: true
t.timestamps
end
end
end
However, if a venue owner creates a performance and has two separate bands performing then I would need to have an array of bands in the band_id column. But when I do that (t.belongs_to :band, type: :uuid, array: true, default: [], index: true) and add a band to the band_id array and then do band.performances I get: ActiveRecord::StatementInvalid (PG::InvalidTextRepresentation: ERROR: malformed array literal:
Can I make an association column an array and still be able to use the Rails association features or is that not possible or even bad practice, and if so how?
Also, I am using postgresql and if you have a more elegant ways of doing the above that would also be appreciated.

I guess it's better to use a has_many relationship.
If a performance can have many bands playing then it does not "belongs to" a band, it "has many" bands.
So you may use a "has and belongs to many" or a "has many :through" relationship between performances and bands. Check the differences here https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many
The easiest of the two to configure is HABTM:
class Band
has_and_belongs_to_many :performances
class Performance
has_and_belongs_to_many :bands
You need a table, so add a miration that does this:
create_table :bands_performances, id: false do |t|
t.references :band, index: true
t.references :performance, index: true
end
https://guides.rubyonrails.org/association_basics.html#creating-join-tables-for-has-and-belongs-to-many-associations
Check the guide, if you need extra fields you may need a join model and use has_many :through. You know the context better than anyone.

Related

Referencing a column on a table to a column on another table Ruby on Rails

I was reading another question on here regarding referencing columns from two separate tables but was a little confused if it addressed my issue. What's going on is I have two tables, Destination and Booking. The Destination table has a column for location_id, and the Booking has a column for location, and I am trying to reference location in Booking table from location_id column in Destination table.
Here is my table for Booking(migration)
class CreateBookings < ActiveRecord::Migration[6.1]
def change
create_table :bookings do |t|
t.string :name
t.string :start_date
t.string :end_date
t.string :email
t.integer :location
t.timestamps
end
end
end
and here is my table(Migration) for Destination
class CreateDestinations < ActiveRecord::Migration[6.1]
def change
create_table :destinations do |t|
t.string :address
t.string :city
t.string :state
t.string :zip
t.integer :location_id
t.timestamps
end
end
end
My Models are setup currently as
class Booking < ApplicationRecord
# belongs_to :reservation, optional: true
has_many :destinations, :class_name => 'Destination', :foreign_key=> 'location_id'
validates :name, :start_date, :end_date, :email, presence: true
end
and
class Destination < ApplicationRecord
has_many :bookings, :class_name => 'Booking', :foreign_key=> 'location'
end
Am I currently referencing the columns correctly, or is there something else I should be doing?
How you should write your migrations depends on the association between your models. Foreign keys go onto tables that have a belongs_to association.
Can a single Booking have multiple Destinations? If the answer is no, you need to change the association in your Booking model to belongs_to :destination and then put a :destination_id on your bookings table (you can give it a custom name like :location_id if you want but the convention is to use the model name).
If a single Booking can have multiple Destinations, and surely a single Destination can have multiple Bookings, then you have a many-to-many relationship. In that case you will not put foreign keys on the destinations table, nor the bookings table. Instead you will need a join table between them and that's where the foreign keys go.
Rails gives 2 different ways to declare many-to-many relationships. See https://guides.rubyonrails.org/association_basics.html#choosing-between-has-many-through-and-has-and-belongs-to-many.
If you want to use has_and_belongs_to_many, your models would look like this:
class Booking < ApplicationRecord
has_and_belongs_to_many :destinations
end
class Destination < ApplicationRecord
has_and_belongs_to_many :bookings
end
And the migration would look like this:
class CreateBookingsAndDestinations < ActiveRecord::Migration[6.0]
def change
create_table :bookings do |t|
# ...
end
create_table :destinations do |t|
# ...
end
create_table :bookings_destinations, id: false do |t|
t.belongs_to :booking
t.belongs_to :destination
end
end
end
Caveat: Based on your question I'm assuming you want a booking to have a destination. If you want a destination to many bookings and vise-versa, Sean's answer is great.
I think you're misunderstanding how foreign keys / associations work in databases.
It sounds like you want a column in the bookings table to "reference" a value column in the destinations table (or maybe the opposite), as in:
bookings.location -> destinations.location_id or maybe destinations.location_id -> bookings.location.
That's not typically what we mean by "reference" in a relational database. Instead, when you say that a table (for example, a 'comments' table) references another table (for example, a comments table references a user table), what we typically mean is that we're storing the primary key column of the referenced table (e.g. the user's id) in a column in the first table (e.g. comments.user_id --> users.id).
From an english language standpoint I expect that you want a booking to refer to a destination, so I'm going to assuming we want a the booking table to reference/refer to the destinations table, like this:
booking.location -> destinations.id
In Ruby on Rails, the convention is to name a column that stores an association with the same as the table it references, plus _id, like so the convention would be this:
booking.destination_id -> destinations.id
A common way to create this in a migration would be with:
add_reference :bookings, :destination
When adding a reference in a database you almost always want to index by that value (so that you can do Bookings.where(destination_id: #destination.id) and not kill your database). I am also a strong advocate for letting your database enforce referential integrity for you, so (if your database supports it) i'd recommend the following:
add_reference :destinations, :booking, index: true, foreign_key: true
This would prevent someone from deleting a destination that has a booking associated with it.

Creating a one to many relation between a model and strings in rails

I want to create a join table between an ActiveRecord model and strings such that the model can have many strings. My join table is:
create_table :project_structures_sku_prefixes, id:false do |t|
t.belongs_to :project_structure, foreign_key: true
t.string :sku_prefix
end
I'm not sure how I would declare this in my model. Something like:
has_many_and_belongs_to :sku_prefixes, class_name:'String'
Can this be accomplished in Rails without having to create another simple model that just stores a string?

Invalid DB operation going to DB constraint _before_ data model validation? (HABTM uniqueness)

I have a model (Company) with a HABTM self-join relationship (the join table calls associated companies suppliers and purchasers).
I want to prevent companies from ever having duplicate suppliers or purchasers, so I'm adding a uniqueness constraint on supplier-purchaser pairs. I'm adding this constraint at both the database level:
create_table :supply_link, id: false do |t|
t.belongs_to :supplier, null: false, index: true
t.belongs_to :purchaser, null: false, index: true
end
add_index :supply_link, [:supplier_id, :purchaser_id], unique: true
and the data model level:
class Company < ApplicationRecord
has_and_belongs_to_many :purchasers, -> { distinct },
join_table: :supply_link,
class_name: :Company,
foreign_key: :supplier_id,
association_foreign_key: :purchaser_id
has_and_belongs_to_many :suppliers, -> { distinct },
join_table: :supply_link,
class_name: :Company,
foreign_key: :purchaser_id,
association_foreign_key: :supplier_id
end
But in trying to write a spec for this arrangement, I noticed something strange: typically, data model validations preempt database constraints — but not here.
That is, if you set both a data model validation (validates :email, presence: true) and a DB constraint (t.string :email, null: false), then the data model catches invalid operations (like User.create(email: nil)) before the DB ever even sees them.
But in this case, when I try to add the same supplier to a Company multiple times, it goes to the database first, and raises an error. If I remove the database constraint altogether, then the data model handles it the right way, and the duplicate entries never get added to the suppliers attribute. two things happen: 1) the duplicate associations all appear in the join table, but 2) the model doesn't show any duplicates for company.suppliers (thanks #Pavel Mihailyuk). I've tested this manually in the console, and with RSpec/FactoryGirl.
What's going on here, and what's the right way to write the specs? Should I keep both validations and expect invalid operations to raise an ActiveRecord error? Should I remove the database constraint and rely solely on the data model's -> { distinct } scope? Should I report this as a bug in Rails?

Add custom fields to object in ROR application

I'm working on CRM platform.
I would like my users to add, edit and delete custom fields in Client, Contact and Lead objects. Those fields may be plain textfield, list, checkbox, tag etc. Those fields may be required or not. Those fields may have custom validation (that user will define).
Say one company from financials would like to add income to Client object, another would add order configuration to Lead object.
Is there any "enterprise-level" solution (ROR gem) for my problem.
Of cause I know about Custom configuration and config gem, but it doesn't look extensible enough.
Hard question, but this is how I would try to deal with it: I would make all the objects to be derived from a CustomField object, then I would create a one to many relationship between it and a Field model. Something like this:
create_table :field_types do |t|
t.string :name # This would identify the fields: checkbox, plain text, etc
end
create_table :fields do |t|
t.belongs_to :custom_field, null: false, index: true
t.belongs_to :field_type, null: false, index: true
t.string :name
end
class Field < ApplicationRecord
belongs_to :custom_field
belongs_to :field_type
end
class CustomField < ApplicationRecord
has_many :fields
end
This way you could just look into the specified fields on the database and mount it at the view.
Then I would create a table for each type of field that could be used by the users to save the data from the CustomField objects. For instance, I would check the Client field specifier, mount a view with checkboxes A and B. Then, I would get the data from the checkboxes and save each of them at the table Checkboxes with an identifier, so that I could tell that it came from clients.
Depending on what you need to do, another idea that pops to my head is to save the data as a JSON string into the database. This way you could have different fields with different values, all you would need to do is serialize and deserialize to save and load it from the database, respectively.
Sorry if it was a little confusing. Hope it helps.
Assuming your database is relational:
I would suggest to use Entity-Attribute-Value pattern:
https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model.
Here is a gem for it:
https://github.com/iostat/eav_hashes
Also document-oriented database like MongoDB would be an option, if you ever consider changing database. It is schemaless, so you can have different attributes for different instance.
I'm not aware of any out of the box options available, but you might be better off rolling your own on something like this anyway. It will allow you more flexibility, and shouldn't be terrible to implement. In terms of models, I'd probably go with a single-table inheritance table for the fields, probably using a jsonb column for customization options (assuming postgres):
create_table :fields do |t|
t.string :type, null: false # TextField, ListField, etc.
t.jsonb :config, default: {}, null: false
t.belongs_to :contact
end
You can then subclass as necessary for different use-cases:
class Field < ApplicationRecord
belongs_to :contact
end
class TextField < Field
def required=(required)
config[:required] = required
end
end
class CheckboxField < Field
def default_checked=(default_checked)
config[:default_checked] = default_checked
end
end
You can look into something like jsonb_accessor to make for a cleaner interface to the jsonb column.
Likewise, single-table inheritance looks like it may also make sense for the contacts as well, not sure what the base table should be, but maybe something like:
create_table :contacts do |t|
t.string :type, null: false # Contact, Lead, Client
end
class Contact < ApplicationRecord
end
class Lead < Contact
end
Here are some examples I found helpful for custom fields:
http://railscasts.com/episodes/403-dynamic-forms?view=asciicast
And:
https://github.com/lab2023/postgresql_jsonb_ransack_rails_5
https://gist.github.com/ismailakbudak/2ca1feac945999ec3e7d9cf0a373497a

index: true vs foreign_key: true (Rails)

Following a guide, I ran the following command:
rails g migration CreateSnippetsUsers snippet:belongs_to user:belongs_to
This created the following migration:
class CreateSnippetsUsers < ActiveRecord::Migration[5.0]
def change
create_table :snippets_users do |t|
t.belongs_to :snippet, foreign_key: true
t.belongs_to :user, foreign_key: true
end
end
end
In the past I've seen the same thing, but with index: true instead of foreign_key: true. What's the difference between the two?
Indexes, foreign keys and foreign keys constraints are strictly related concepts in databases that are often confused or misunderstood.
REFERENCES
When you declare a reference, you're simply saying to include a column whose values should match those of another table (and in Rails you also get some useful methods to navigate through the associated models). In the example:
create_table :appointments do |t|
t.references :student
end
the appointments table will have a column named student_id whose values should be in the pool of students' id values.
INDEXES
Since when you add a reference you will probably use that column often, you may (and probably should!) also tell you database to boost the look up speed using the reference column. You can do this with the option index: true (which by the way is a default option in the reference method since Rails 5). Indexes have few drawbacks, the main being a larger memory consumption.
FOREIGN KEY CONSTRAINTS
From what said so far, reference column and foreign column are synonyms. But do you remember when I said that a reference column's values should match those of another table? If you simply declare a reference, it's your responsibility to ensure that a matching row on the referenced table exists, or someone will end up doing nonsense actions like creating appointments for non-existing students. This is an example of database integrity, and fortunately there are some mechanisms that will grant a stronger level of integrity. These mechanisms are called ' database constraints'. What the option foreign_key: true does is exactly to add this kind of constraint on the reference column, to reject any entry whose foreign key values are not in the referenced table.
Database integrity is a complex task, growing in difficulty with the database's complexity. You probably should add also other kind of constraints, like using they keywords dependent: :destroy in your class to ensure that when you delete a student, all of its existing appointments are also destroyed.
As usual, here's a RTFM link: https://guides.rubyonrails.org/association_basics.html
Index improve speed of data retrieval operations on database tables. When we write index: true to any column, it adds a database index to this column. For example I was creating a table:
create_table :appointments do |t|
t.references :student, index: true
end
It will create student_id column in appointments table.
A foreign key have different use case, it is a relationship between tables. It allow us to declare an index in one table that is related to an index in another table and also some constraints are placed.The database enforces the rules of this relationship to maintain referential integrity. For example we have two table profiles and educations, and a profile may have many educations.
create_table :educations do |t|
t.belongs_to :profile, index: true, foreign_key: true
end
Now we have profile_id column in educations table which is foreign key of profiles table. It prevents a record from being entered into the educations table unless it contains a profile_id value that exists in the profiles table. So referential integrity will be maintained.

Resources