Rails: Polymorphic User Table a good idea with AuthLogic? - ruby-on-rails

I have a system where I need to login three user types: customers, companies, and vendors from one login form on the home page.
I have created one User table that works according to AuthLogic's example app at http://github.com/binarylogic/authlogic_example. I have added a field called "User Type" that currently contains either 'Customer', 'Company', or 'Vendor'.
Note: each user type contains many disparate fields so I'm not sure if Single Table Inheritance is the best way to go (would welcome corrections if this conclusion is invalid).
Is this a polymorphic association where each of the three types is 'tagged' with a User record? How should my models look so I have the right relationships between my User table and my user types Customer, Company, Vendor?
Thanks very much!
------UPDATE------
Current setup:
#User model
class User < ActiveRecord::Base
acts_as_authentic
belongs_to :authenticable, :polymorphic => true
end
#Customer model (Company and Vendor models are similar)
class Customer < ActiveRecord::Base
has_many :users, :as => :authenticable
acts_as_authentic
end
Is this a valid way to set them up?

This is what it sounds like you are trying to do:
You have users that can be in one of three groups. If that isn't quite right a little clarification would help.
Since AuthLogic only cares about it's special fields, making and using other fields in your user model is no biggy.
Each user might be made up something like this:
#The Authlogic Fields
t.string :username, :null => false
t.string :crypted_password, :null => false
t.string :password_salt, :null => false
t.string :persistence_token, :null => false
#Your field to identify user type
t.integer :user_type
#If your user is a customer, you would populate these fields
t.integer :customer_name
...
#If your user is a company, you would populate these fields
t.integer :company_name
...
#If your user is a vendor, you would populate these fields
t.integer :vendor_name
...
I'm not sure what you mean by "single table inheritance" but if you want to keep information about a vendor in a table separate from the users table (really no reason to unless you are REALLY concerned about performance) you can just link up your models like this:
class User < ActiveRecord::Base
has_many :customers, :companies, :vendors
end
class Customer < ActiveRecord::Base
belongs_to :user
end
class Company < ActiveRecord::Base
belongs_to :user
end
class Vendor < ActiveRecord::Base
belongs_to :user
end
In this case, since a user would have no associations to 2 of the 3 user types, you are pushed into using the has_many association which works nicely with the 0 association case. You would just have to do some checks in your code to make sure you don't double up.

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.

Associating multiple existing records

I'm creating an application where one user becomes the account_manager of an account. What I want to do is to add other users to the account. A user can only have one account but an account can have many users.
class Account < ActiveRecord::Base
has_many :users
belongs_to :account_manager, :class_name => 'User', :foreign_key => 'account_manager_id'
end
class User < ActiveRecord::Base
has_one :account
What I'm totally stuck on is having a place where the account manager can either select the user from a dropdown, type in their name, or use some other type of selection. If I try to do this in console each new user I add replaces the last instead of adding to it. here is my schema for accounts:
create_table "accounts", force: :cascade do |t|
t.integer "user_id"
t.integer "account_manager_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
I've tried using collection_select but I think that is only for :though associations. I'm also thinking I probably need a join table but I don't know how to set it up. The thing that is tripping me up most is that I won't be creating new objects, I only want to add existing users to existing accounts. I'm just looking for someone who can talk through this with me.
In your User model:
class User < ActiveRecord::Base
belongs_to :account
end
While in your question, you are writing has_one. You can't write has_many in one model, and has_one in other model. There needs to be belongs_to in one model.
Edit:
The model that belongs_to, always saves the foreign keys in its table. So users would save account_id in it. In order to get all the users of an account, you would simply do:
Account.first.users # As an account `has_many` users

Seeding an ActiveRecord Database with objects from another table

I'm new to Rails, I'm making my first app and I have (another) question:
I'm adding a "decks of cards" feature to my flashcard app, but having trouble with the modeling to generate a "deck" that is populated by card objects from my card table.
Here are my associations:
User has_many :decks
Deck belongs_to :user
Deck has_many :cards
Card belongs_to :deck
class Card < ActiveRecord::Base
belongs_to :deck
validates :front, presence: true
validates :back, presence: true
validates :deck_id, presence: true
end
And here's my decks migration/table:
class Decks < ActiveRecord::Migration
def change
create_table :decks do |t|
t.string :name, null: false
t.string :card
t.integer :user_id, null: false
end
end
end
My issue is that I would like for the "card" column in the decks table to consist of Card objects, so that I can access/manipulate their methods, but I'm not sure how exactly to do this. I tried populating the table with t.string of ":card" in the hopes that this would work but it only comes up blank. I am wondering if this is even possible or advisable or is there a better way?
If anyone can point me to resources/offer advice on this, thank you. I checked the docs/SO and can't seem to find anything.
What you're describing can be easily accomplished.
Provided that User, Deck and Card are ActiveRecord models, you can associate them by setting foreign keys to connect the tables. A foreign key is an integer column that contains the id of the associated model (its table's primary key)
The Rails convention is to use belongs_to and has_many to declare "one to many" associations (docs). These methods will add to your model objects the required methods to represent and interact with the association.
On the DB schema side of things, you'll need to set the foreign keys on the models' tables that declare the belongs_to associations.
So, if you have these models:
class User < ActiveRecord::Base
has_many :decks
end
class Deck < ActiveRecord::Base
belongs_to :user
has_many :cards
end
class Card < ActiveRecord::Base
belongs_to :deck
end
You'll need these migrations:
class CreateDecks < ActiveRecord::Migration
def change
create_table :decks do |t|
# your other columns...
t.integer :user_id
end
end
end
class CreateCards < ActiveRecord::Migration
def change
create_table :cards do |t|
# your other columns...
t.integer :deck_id
end
end
end
With a has_many relation you don't store the foreign key on the owning table. Instead you have a deck_id column in the cards table.
An example of using an association:
# Load a deck and include the cards in the query
#deck = Deck.joins(:cards).last
#deck.cards.each do |card|
puts card.front
end
# create a new card
#deck.cards.new(front: 'foo', back: 'bar')
#deck.save # will save the card as well.
http://guides.rubyonrails.org/association_basics.html
You don't want the card column in your decks table. The has_many :cards and belongs_to :deck lines in your models provide that functionality for you to be able to do things like #deck.cards.
You just have to be sure to assign a deck_id when you create a new Card object.
You should read through the Rails Guides on associations, and also on database seeding.

Token access to edit function of multiple models

I'm working on an application where a guest will be provided a short Base64 token that they could in turn use to access the edit function of one of several different models via one "search form" on the application homepage.
I have already created the token functionality and included it in the schema for the models I need. My question is, how would one best search for and access the edit function using the access token from the home page?
I'm having a hard time finding a good way to do this and while I'm finding a lot about access tokens, most of it doesn't seem to pertain to my use case.
Rails provides the ability for model classes to be inherited from a parent model class. Then the models can have shared attributes, but also unique ones. In the database all of these model objects are stored in the same table for all classes, so this is called Single Table Inheritance or STI. (Documented here but there are better docs in blog posts out there.)
If you use this approach, then you could search the parent class for all instances to find matching objects/records.
class AccessToken < ActiveRecord::Base
# has attribute access_token, and maybe others
end
class OneAccessibleKind < AccessToken
# may have other attributes
end
class AnotherAccessibleKind < AccessToken
# may have other attributes
end
Your migration would look something like this:
create_table :access_token do |t|
t.string "access_token"
t.string "type"
# add any additional attributes of subclasses
t.timestamps
end
You can then query against the parent class. Note
all_models = AccessToken.where(access_token: 'a-token')
Note that these will all come back as AccessToken objects (i.e. the parent class), but you can inspect the type attribute to see what their base class is.
This may not be the best solution, however, if your classes are mostly different fields because you'll have lots of unused columns. Depending on your backing database (assuming row-oriented SQL) and number of objects this could be a performance problem.
Another option would be to use a one-to-one relationship and have an AccessToken model for each of your other models. Here you can use an STI association.
class AccessToken < ActiveRecord::Base
belongs_to :owner, :polymorphic => true
end
class OneAccessibleKind < ActiveRecord::Base
has_one :access_token, :as => :owner
end
class AnotherAccessibleKind < ActiveRecord::Base
has_one :access_token, :as => :owner
end
With migrations something like this:
create_table :access_token do |t|
t.string :access_token
t.integer :owner_id, null: false
t.string :owner_type, null: false
t.timestamps
end
create_table :one_accessible_kind do |t|
# any attributes for this type
t.timestamps
end
Then you can find an access token and access each owner to get the objects.
AccessToken.where(access_token: 'a-token').map(&:owner)

Scaffolding ActiveRecord: two columns of the same data type

Another basic Rails question:
I have a database table that needs to contain references to exactly two different records of a specific data type.
Hypothetical example: I'm making a video game database. I have a table for "Companies." I want to have exactly one developer and exactly one publisher for each "Videogame" entry.
I know that if I want to have one company, I can just do something like:
script/generate Videogame company:references
But I need to have both companies. I'd rather not use a join table, as there can only be exactly two of the given data type, and I need them to be distinct.
It seems like the answer should be pretty obvious, but I can't find it anywhere on the Internet.
Just to tidy things up a bit, in your migration you can now also do:
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
And since you're calling the keys developer_id and publisher_id, the model should probably be:
belongs_to :developer, :class_name => "Company"
belongs_to :publisher, :class_name => "Company"
It's not a major problem, but I find that as the number of associations with extra arguments get added, the less clear things become, so it's best to stick to the defaults whenever possible.
I have no idea how to do this with script/generate.
The underlying idea is easier to show without using script/generate anyway. You want two fields in your videogames table/model that hold the foreign keys to the companies table/model.
I'll show you what I think the code would look like, but I haven't tested it, so I could be wrong.
Your migration file has:
create_table :videogames do |t|
# all your other fields
t.int :developer_id
t.int :publisher_id
end
Then in your model:
belongs_to :developer, class_name: "Company", foreign_key: "developer_id"
belongs_to :publisher, class_name: "Company", foreign_key: "publisher_id"
You also mention wanting the two companies to be distinct, which you could handle in a validation in the model that checks that developer_id != publisher_id.
If there are any methods or validation you want specific to a certain company type, you could sub class the company model. This employs a technique called single table inheritance. For more information check out this article: http://wiki.rubyonrails.org/rails/pages/singletableinheritance
You would then have:
#db/migrate/###_create_companies
class CreateCompanies < ActiveRecord::Migration
def self.up
create_table :companies do |t|
t.string :type # required so rails know what type of company a record is
t.timestamps
end
end
def self.down
drop_table :companies
end
end
#db/migrate/###_create_videogames
class CreateVideogames < ActiveRecord::Migration
create_table :videogames do |t|
t.belongs_to :developer
t.belongs_to :publisher
end
def self.down
drop_table :videogames
end
end
#app/models/company.rb
class Company < ActiveRecord::Base
has_many :videogames
common validations and methods
end
#app/models/developer.rb
class Developer < Company
developer specific code
end
#app/models/publisher.rb
class Publisher < Company
publisher specific code
end
#app/models/videogame.rb
class Videogame < ActiveRecord::Base
belongs_to :developer, :publisher
end
As a result, you would have Company, Developer and Publisher models to use.
Company.find(:all)
Developer.find(:all)
Publisher.find(:all)

Resources