Ruby Foreign Key Constraints in Database vs Model - sql

I have the following models in a Rails app. This setup has worked fine since normally when I would like to add a new VendorPromo to a vendor, there's a dropdown in the view of available Promos to choose from. Now, I'm enabling the creation of new VendorPromos via an API. However, this current set up will allow the creation of a VendorPromo with any promo_id, even if it doesn't exist in Promo. I've seen the belongs_to :promo, foreign_key: :promo, and I know you can add a foreign key constraint via the DB as well. What I'd like to know is the difference between these two approaches and if one is better?
Edit: I recognize that application constraints are different than DB constraints, but when I apply the constraint in the app, i.e. the belongs_to :promo, foreign_key: :promo it doesn't actually seem to enforce the constraint at all. Specifically, I can create a new VendorPromo with a promo_id of, say, 13, even though Promo only has IDs between 1 and 8.
class Vendor
has_many :vendor_promos
end
class VendorPromo
belongs_to :vendor
belongs_to :promo
end
class Promo
end

The foreign_key attribute in the belongs_to method is not actually defining a foreign key, it's specifying for rails's internals which field holds the foreign key.
The Ruby on Rails Guide warns you about it here: https://guides.rubyonrails.org/association_basics.html#options-for-has-many-foreign-key
In any case, Rails will not create foreign key columns for you. You need to explicitly define them as part of your migrations.

Juan Carlos's answer was part of the way there. It's true that the foreign_key attribute in belongs_to does not enforce any database or application level foreign key. What I was looking for was the belongs_to :promo, optional: false flag does in fact enforces an application level foreign key. That flag, in addition to a DB level foreign key constraint resolved my issue.

Related

Opinions on Best Practice for a Relation that is linked to many other types of Relations

I have a Rails database for reviewing things at a school. My tables are School, Major, Course, Instructor, Review. The way my team currently has the Review table set up, there are non-nullable foreign keys to each one of the other tables.
My issue with this is that to submit a new review the user would want to fill in only 1 of those foreign keys. Is there a way to do this with Rails? Even if there is, it seems like this would be a better use case for Instructor_Review, Course_Review, etc tables. That also has the (very nice) benefit of being able to customize table attributes for each review.
However, if we were to break up Review into multiple tables, is there a mechanism in Rails for having common columns? The overall_rating attribute would need to be included for every type of review, should the attributes just have the same name or is there a way for Rails to have table Subclasses (I know there is in SQL...)
I wouldn't create different models for the different review types if they are similar (apart from the thing the review is about). This is probably a source of duplication which should be avoided.
Instead, you could use a polymorphic association. Your review model has one thing the review belongs to (be it a school, a teacher, or an instructor). So lets model it like that. Give it a reference to that thing and the type of that thing (so that Rails knows which class it belongs to).
With polymorphic associations, your classes can be linked like this:
class Review < ActiveRecord::Base
belongs_to :reviewable, polymorphic: true
belongs_to :user
# ...
end
class School < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
class Teacher < ActiveRecord::Base
has_many :reviews, as: :reviewable
end
On the database-level this means that your review table just needs one foreign key to point at the reviewable thing (plus one type column). The review-migration would look like this:
class CreateReviews < ActiveRecord::Migration
def change
create_table :reviews do |t|
t.references :user, index: true
t.references :reviewable, polymorphic: true, index: true, null: false
t.timestamps null: false
end
end
end
polymorphic: true lets Rails create the id- and type-column.
For details, please refer to the ActiveRecord documentation.
PS: There is also a RailsCast covering this issue. But beware: It's from 2009 - pretty old, but (from a quick glimpse at it) it should still work.
You are looking for polymophic relationships. Basically add a type column on the review and then you have to wire it up correctly. Here is a great article for that.
http://www.gotealeaf.com/blog/understanding-polymorphic-associations-in-rails
The review table will then have your common reviewable columns that will be shared. The next part is a little more complicated. You would then setup a review to have an extension.
To explain the extension, this is normally done when you have an entity that can either be a group or user (similar to how your InstructoreReview and CourseReview will by types of reviews). Those groups/users will have a profile that is similar to the extension of the review.
The ReviewExtension will have it's own table per review that you are extending and most of the time those extensions in their own review_extensions folder under models.
Once that is done you will delegate the common getter and setter methods you are wanting to the extension.
Hopefully this will at least give you enough to get going.

Make a foreign key also a primary key in a Rails migration

EDIT:
I want a column to be a primary key and a foreign key from Rails' point of view. That is to say a foreign key using the references method. I'd like to do something like:
def change
create_table :VipPerson, id: false do |t|
t.primary_key :person_id
t.references :person, :id, column: :person_id
t.timestamps
end
end
I want to have a custom primary key that is also used by the Rails model to point to the Person-model (see below). This has actually nothing to do with foreign-keys that are employed by the sql-database (Rails didn't support the creation of real foreign-keys until Rails 4.2, as pointed out by Demi Magus).
-
I want to realize Multiple Table Interitance (MTI) in my Rails project and therefore realize inheritance like you usually do in a relational database:
table: Person
-------------
(PK) ID
Name
...
table: VipPerson (specalization of Person)
----------------
(PK) (FK) Person_ID
VIP-Status
...
So for this I need to write a migration that declares an attribute (here Person_ID) both as a (custom) primary key and a foreign key at the same time. I do not see how to do this with the methods of Rails' migration DSP.
I do not want to use Rails' polymorphic associations because I want to guarantee data integrity in the database. I know that I will need to customize the Rails model as well, but let's not get to that topic. The main problem is how to write a migration that makes a PK also an FK (or vice versa).
The foreign key relation is only available with the latest version of rails, which is 4.2 as you can see in the release notes, and you can use it like this
Creating a simple foreign key
add_foreign_key :articles, :authors
generates:
ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id")
You also should know that the add_foreign_key will only generate only a foreign key constraint, not a column like the activerecord add_column does
add_column :articles, :author_id, :integer
will generate:
ALTER TABLE "articles" ADD COLUMN author_id INT(11);
You can find more info here
Also if you are not looking just for the edge of rails, then you could find useful this gem that allows you to do what you'r looking for which is composite_primary_keys, the only thing that you have to be cautious is that you will have to use the version compatible with your rails version.
I hope it helps :D
The solution is manually setting the primary key and foreign key in the model rather than fiddling in the migration (note you must tell Rails not to create the default column id by passing "id: false" in the options):
Migration:
def change
create_table :VipPerson, id: false do |t|
t.primary_key :person_id # Normal integer PK, shouldn't auto-increment
t.integer :vip_status
..... etc.
end
end
In the model:
class VipPerson < ActiveRecord::Base
self.primary_key = "person_id"
belongs_to :person, foreign_key: "person_id"
end
Just manually point to the same column in the model. On the database layer you can then additionally enhance the primary key with a real foreign-key contraint (see Demi Magus answer).

Using a different foreign key with nested resources in Rails

When working with nested resources in Rails is it possible to use a value/field other than the primary key of the parent resource as the foreign key for the child resource objects?
e.g. if I have "books" that belong to "authors", I pass the "author"'s primary key to the book when it's created with t.references :author, index: true in app/db/migrate/[timestamp]_create_books.rb (right?).
Is it possible to pass the author's name, instead? (Assuming that the "authors" table has a "name" field...)
I ask because I have a preexisting table of books with various fields (author, title, subject, year, etc.) and it seems simpler to create an authors table with the unique authors from the books table and then join them where authors.name=books.author instead of having to figure out a way of getting the unique primary keys from authors to associate with the correct author in books. (But I am probably totally wrong about this.) (In any case, I am curious if it can be done and/or what the proper way of bringing in the preexisting database that lacks the author-book associations would be.)
(I apologize if my terminology is off.)
So, your models:
class Author < ActiveRecord::Base
has_many :books
end
class Book < ActiveRecord::Base
belongs_to :author, primary_key: "name", foreign_key: "author_name"
end
It should work. But this way breaks one of the Rails cornerstones: "Convention over configuration" and that's why you have chances to end up with total mess in your DB someday.
And what else attributes except 'name' Author model has? If there are few of them (or even only one - 'name') it will be better and easier to have only one model 'Book' with the attribute 'author'.

Ruby on Rails Migration with Primary and Foreign Key

I am just starting ROR and making different tables for the SQLite database and am running into some trouble in relation to automatically generated id's of tables and foreign keys. To create my tables I used the "rails generate scaffold" command followed by the attributes that I needed. I went to the db/migrate directory and looked at what I had just defined. But I don't understand how I am supposed to explicitly reference foreign keys. I'm used to using Oracle so I'd normally do this process in the very first step during creation and be done with it already.
For example, I have a users table with some general attributes such as a username, password, etc. I also have an orders table with attributes transactionID (auto generated), userID (I want to this to be an FK), PartNo (FK key from the Products table). What I don't understand is how to use the auto generated key from the Users table and include it as a foreign key in the Orders table.
Also, I read somewhere else that if I put a line of code such as
t.integer user_id
in my create_orders.rb file then it would automatically know that this is a foreign key!?
I feel like this is probably really easy and I'm missing something. This is my first time using rails and I thought I got to define the foreign keys straight away via SQL. Any help would be greatly appreciated. If I could figure this out it would save me a lot of time.
After you do this, on your Order model, you'd use a belongs_to :user to generate the relationship and utilize the foreign key.
If you want to be able to go from User to Order, you'll likely be a one-to-many, so you'd have to do has_many :orders on User.
Here is the documentation for associations in Rails. And here is the specific documentation for belongs_to vs has_one.

rails3 and the proper way to use associations

I'm doing my first rails(3) application.
Associations don't make sense. First, even the rails guides don't
really explain what they do, they just explain how to use them.
From what I gather, associations do two things:
a) Allow ActiveRecord to optimize the structure of the database.
b) Allow ActiveRecord to offer an alternate ruby syntax for
joins and the like (SQL queries). I want this.
I'm trying to understand associations, and how to properly use them. Based
on the example below, it seems like associations are 'broken' or at least
the documentation is.
Consider a trivial version of my application. A teacher modifying wordlists
for study.
There are 3 relevant tables for this discussion. For clarity, I've simply
included the annotate(1) tool's definition of the table, and removed
unnecessary fields/columns.
A wordlist management table:
Table name: wordlist_mgmnt_records
id :integer not null, primary key
byline_id :integer(8) not null
A table that maps words to a word list:
Table name: wordlists
wordlist_mgmnt_id :integer not null
word_id :integer not null
We don't actually care about the words themselves. But we do care about
the last table, the bylines:
Table name: bylines
id :integer(8) not null, primary key
teacher_id :integer not null
comment :text not null
Bylines record who, what tool was used, where, when, etc. Bylines are
mainly used to trouble shoot what happened so I can explain to users what
they should have done (and/or repair their mistakes).
A teacher may modify one or more word list management records at a time
(aka single byline). Said another way, a single change may update multiple
word lists.
For wordlist_mgmnt_records the associations would be:
has_many :bylines # the same byline id can exist
# in many wordlist_mgmnt_records
But what's the corresponding entry for bylines?
The Beginning Rails 3 (Carneiro, et al) book says:
"Note: For has_one and has_many associations, adding a belongs_to
on the other side of the association is always recommended. The
rule of thumb is that the belongs_to declaration always goes in
the class with the foreign key."
[ Yes, I've also looked at the online rails guide(s) for this. Didn't
help. ]
For the bylines table/class do I really want to say?
belongs_to :wordlist_mgmnt_records
That really doesn't make sense. the bylines table basically belongs_to
every table in the data base with a bylines_id. So would I really say
belongs_to all of them? Wouldn't that set up foreign keys in all of the
other tables? That in turn would make changes more expensive (too many
CPU cycles) than I really want. Some changes hit lots of tables, some of
them very large. I prize speed in normal use, and am willing to wait to
find bylines without foreign keys when using bylines for cleanup/repair.
Which brings us full circle. What are associations really doing in rails,
and how does one use them intelligently?
Just using associations because you can doesn't seem to be the right
answer, but how do you get the added join syntax otherwise?
I'll try to help your confusion....
A byline can have multiple wordlist_mgmnt_records, so defining the has_many there seems to make sense.
I'm not sure I understand your confusion in the other direction. Since you have defined the attribute wordlist_mgmnt_records.byline_id, any given wordlist_mgmnt_record can only 'have' (belong_to) a single byline. You're simply defining the crows foot via ruby (if you like database diagrams):
wordlist_msgmnt_records (many)>>----------(one) byline
Or read in english: "One byline can have many wordlist_mgmnts, and many individual wordlist_mgmnt's can belong to a single byline"
Adding the belongs_to definition to the wordlist_mgmnt model doesn't affect the performance of the queries, it just let's you do things like:
#record = WordlistMgmntRecord.find(8)
#record_byline = #record.byline
Additionally you're able to do joins on tables like:
#records = WordlistMgmntRecord.joins(:byline).where({:byline => {:teacher_id => current_user.id}})
Which will execute this SQL:
SELECT wordlist_mgmnt_records.*
FROM wordlist_mgmnt_records
INNER JOIN bylines
ON wordlist_mgmnt_records.byline_id = bylines.id
WHERE bylines.teacher_id = 25
(Assuming current_user.id returned 25)
This is based off of your current DB design. If you find that there's a way you can implement the functionality you want without having byline_id as a foreign key in the wordlist_mgmnt_records table then you would modify your models to accomodate it. However this seems to be how a normalized database should look, and I'm not really sure what other way you would do it.