Normalizing data in rails - ruby-on-rails

I created a rails model by doing
script/generate model Customer name:string address:string city:string state:string zip:integer [...]
I filled the database with 5000 customers and started building my app. Now I've realized my model isn't normalized: I often have multiple customers at the same address! If I wish to do something per-address, like, say, a mailing, this causes problems. What I'd like to have is a Address model, a Customer model, and a Mailing model.
Is there a rails way to normalize an existing model, splitting it into two models? Or should I just write a script to normalize my existing data, then generate new models accordingly?

You asked about what the migration would look like. Rather than cram this in a comment reply, I created a new answer for you.
script/generate model address customer_id:integer address:string city:string state:string zip:integer
class CreateAddresses < ActiveRecord::Migration
def self.up
create_table :addresses do |t|
t.integer :customer_id
t.string :address
t.string :city
t.string :state
t.integer :zip_code
t.timestamps
end
# move customer address fields to address table
Customer.all.each do |c|
Address.create({
:customer_id => c.id,
:address => c.address,
:city => c.city,
:state => c.state,
:zip => c.zip
})
end
# you'll have to write your own merge script here
# use execute("your sql goes here...") if you can't do it with ActiveRecord methods
# remove old customer address columns
remove_columns(:customers, :address, :city, :state, :zip)
end
def self.down
# here you will define the reverse of the self.up method
# re-add the address columns to the user table
# repopulate the customer table with data from the address table
drop_table :addresses
end
end
Resources
AcitveRecord::Migration
execute

I'm not aware of a built-in Rails way to programmatically split up your model. You'll need to write a new Address model and database update migration to get everything switched over.
Your models will likely look something like this:
class Person < ActiveRecord::Base
has_many :addresses
end
class Address < ActiveRecord::Base
belongs_to :person
end

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.

How to map one user to another (from same table)

I'd like to know how to solve this problem in my model/migrations, with correct referential integrity/uniqueness constraints.
I have a user table with two types of user: support_worker and service_user (like teacher and pupil). A support_worker can provide support for many service_users. I used to have separate tables for these respective user types, but for simplicity it makes more sense to have both user types in a single 'user' table (for Devise).
I'll have another table called support_allocation which records the relationship between a support_worker and the service_user(s) they support - this support_allocation has other information stored about it (like a budget; time/money). So this table needs to map one user_id to another user_id. I imagine the table structure will look something like this: SupportAllocation (id, support_worker_id, service_user_id)
So far, my migrations look like this (I've used Devise gem to create the user table so this amends it):
class ChangeUsers < ActiveRecord::Migration[5.2]
def change
change_table :users do |t|
t.string :user_type # support_worker or service_user
t.string :given_name
t.string :family_name
t.string :customer_reference # only for service_users
t.date :date_of_birth # only for service_users
t.string :job_roles # only for support_workers
end
end
class CreateSupportAllocations < ActiveRecord::Migration[5.2]
def change
create_table :support_allocations do |t|
t.boolean :active, default: true
# This next bit is guesswork
t.integer support_worker_id # support_worker's user_id
t.integer service_user_id # service_user's user_id
t.timestamps
end
end
end
Here's where I get confused... I need to create a join, but this will only do it on user_id, whereas the relationship is defined by the two user_id columns (as shown and named above). I'm not sure if this a compound key or if a single foreign key (or two) will suffice.
Here's my migration work-in-progress:
class AddJoins < ActiveRecord::Migration[5.2]
def change
change_table :support_allocations do |t|
t.belongs_to :user, index: true
end
end
end
I'd like to know how to achieve this. For the record, I'm using ActiveAdmin for my app. Thank you for your help.
I don't think you need the AddJoins migration. Add 2 associations in your CreateSupportAllocations model like so:
belongs_to :support_worker, :foreign_key => :support_worker_id, :class_name => User
belongs_to :service_user, :foreign_key => :service_user_id, :class_name => User
In your activeadmin form you can set the collections for the select, for example
(in app/admin/support_allocations.rb)
form do |f|
f.inputs do
# your inputs
f.input :support_worker, :as => :select, :collection => User.where(:user_type => 'support_worker')
f.input :service_user, :as => :select, :collection => User.where(:user_type => 'service_user')
end
f.actions
end
# added after comments
index do
selectable_column
column :support_worker
actions
end
Add a to_s method in you user model like so:
def to_s
"#{self.full_name}"
end
Thanks for all your help. I added the suggested associations to my SupportAllocation model. For the record, I also had to add the following associations to my User model to make the join work fully, in both directions:
has_many :occurances_as_support_worker, :class_name => 'SupportAllocation', :foreign_key => 'support_worker_id'
has_many :occurances_as_service_user, :class_name => 'SupportAllocation', :foreign_key => 'service_user_id'
I used the example given here to work this out.
When accessing attributes specific to a type of user (i.e. using the join over support_worker_id OR service_user_id), on the index page. I use code like this:
column 'Service user', :full_name, :sortable => 'service_users.family_name' do |support_allocation|
#ServiceUser.find(support_allocation.service_user_id).full_name
support_allocation.service_user.full_name
end
column 'Support worker', :full_name, :sortable => 'support_workers.family_name' do |support_allocation|
support_allocation.support_worker.full_name
end

Rails 5 - Creating two new Models Inheriting from a base Model

Lets say that I have an User model with its attributes (first_name, last_name, etc) and I want to create two new models Teacher and Student.
They will inherit the User model attributes, and also, they will have specific attributes. For instance, the Student model will have a file attribute, and the Teacher model will have subject attribute.
I was reading about STI (Single Table Inheritance) and Polymorphic relationships.
What should I look for to accomplish this? Do you have any example to show?
If you create an attribute called "type" on your users table, Rails will automatically assume you want to implement STI. Then, creating Teacher and Student models is as simple as extending the User class. The name of the child class will automatically be inserted into the type column and used to filter queries as you would expect.
user.rb
class User < ApplicationRecord
end
teacher.rb
class Teacher < User
end
student.rb
class Student < User
end
With STI, you place all of the columns that either model will use in the same table and simply ignore (default to null) the ones that don't apply in any give situation.
A polymorphic relationship allows two or more tables to fill the same association. If you want to use three different tables but ensure that a User has either a Teacher or a Student, that could be modeled as a polymorphic belongs_to. The downside is that you would need to get back to the User model to access the shared information, i.e. teacher.user.first_name.
I have found this gem that looks like what I am looking for. I have played with it a little and it does work for me.
https://github.com/krautcomputing/active_record-acts_as
So, for my case I have added to the Gemfile:
gem 'active_record-acts_as'
Then:
$ bundle
These are my migrations:
# 20171202142824_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :first_name
t.string :last_name
t.date :birth_date
t.string :dni
t.string :cuil
t.string :email
t.string :phone
t.string :address
t.string :postal_code
t.string :city
t.string :state
t.string :country
t.actable # This is important!
t.timestamps
end
end
end
# 20171202142833_create_students.rb
class CreateStudents < ActiveRecord::Migration[5.1]
def change
create_table :students do |t|
t.string :file
# Look, there is no timestamp.
# The gem ask for it to be removed as it uses the User's timestamp
end
end
end
# 20171202142842_create_teachers.rb
class CreateTeachers < ActiveRecord::Migration[5.1]
def change
create_table :teachers do |t|
# Look, there is no timestamp.
# The gem ask for it to be removed as it uses the User's timestamp
end
end
end
These are my models:
# user.rb
class User < ApplicationRecord
actable
validates_presence_of :first_name, :last_name
def full_name
[last_name.upcase, first_name].join(', ')
end
end
# student.rb
class Student < ApplicationRecord
acts_as :user
validates_presence_of :file
end
# teacher.rb
class Teacher < ApplicationRecord
acts_as :user
end
Now, with all that set, you can simply create a new Student and a new Teacher doing:
Student.create!(first_name: 'John', last_name: 'Doe', file: 'A125')
=> #<Student id: 3, file: "A125">
Teacher.create!(first_name: 'Max', last_name: 'Power')
=> #<Teacher id: 1>
You have access to all the methods and attributes of the User. For instance:
Teacher.last.full_name
=> "POWER, Max"

Relation one to many with 2 foreigns keys of the same table Ruby on rails

I have a table called customers. These table has two addresses . One address of work and One direccion of house
Those 2 addresses belong to a table called addresses
I don't know how to relation those 2 tables
Migrations
class CreateCustomers < ActiveRecord::Migration
def change
create_table :customers do |t|
t.string :name
t.integer :address_id #Address of work
t.integer :address_id_1 #Address of home
t.timestamps
end
end
end
class CreateAdresses < ActiveRecord::Migration
def change
create_table :adresses do |t|
t.string :street
t.timestamps
end
end
end
I do not believe this is a good approach or database design. If you want to proceed this way and not get out of the rails convention just create two columns address_id and address_two_id
and in customer.rb
belongs_to :address, class_name: "Address"
belongs_to :address_two, class_name: "Address"
By default rails takes the name of the foreign key and stores it in a column called "name"+"_id"
The better way is two have a column customer_id in your Address model and create a relation in your customer class
customer.rb
has_many :addresses
And you can also validate that a customer has no more than two addresses by adding this validation to
address.rb
validate :validate_two_addresses
def validate_two_addresses
address_count = Address.where(customer_id: self.customer_id).count
errors.add(:base, "You cannot have more than 2 addresses.") unless address_count < 3
end

How to Nest Models within a Model

Imagine I have two models
Film
-name
-description
-duration
-year_made
-rating
-actors
Actor
-name
-d_o_b
-biography
-films
Actors are nested in a Film and vice versa.
How do I represent this relationship in my Ruby models? Realistically I would have a third table mapping actor_id with film_id.
Whilst adding details to a film I would like to be able to create an actor on the fly(if an actor does not exist create a new one with the name supplied)
Thank you in advance.
ADDITION:
Just found a link to a similar question.
You're looking at a Has and Belongs to Many (HABTM) relationship between the two tables.
Read about HABTM relationship in the Rails guides here: http://edgeguides.rubyonrails.org/association_basics.html#has_and_belongs_to_many-association-reference
First you'll need to generate a migration which will look something like this:
class AddActorFilmTable < ActiveRecord::Migration
def self.up
create_table :actors_films, :id => false do |t|
t.integer :actor_id, :null => :false
t.integer :film_id, :null => :false
end
add_index :actors_films, [:actor_id, :film_id], :unique => true
end
def self.down
drop_table :actors_films
end
end
and then specify in your models:
class Actor < ActiveRecord::Base
has_and_belongs_to_many :films
end
class Film < ActiveRecord::Base
has_and_belongs_to_many :actors
end
This will allow you to use all of the additional Rails methods for this type of relationship. To use this in a form, you could follow RailsCast 17: HABTM Checkboxes - though it's old, it should still apply. Alternatively, you can use a gem like Simple Form to easily generate the associations for you like so:
form_for #actor do |f|
f.collection_check_boxes :film_ids, Film.all, :id, :name
end

Resources