I'm running into an issue when trying to associate two records via a belongs_to:
class Enrollment < ActiveRecord::Base
belongs_to :offering,
foreign_key: [:term_id, :class_number]
end
#enrollment = Enrollment.new
#enrollment.offering = Offering.last
This throws:
ActiveModel::MissingAttributeError: can't write unknown attribute `[:term_id, :class_number]'
What am I doing wrong?
Rails unfortunately does not support composite keys. If you need something like this, you'd better go with a custom :has_many like #yannick's comment cites.
On the other side, you could do it with a model that's backed by a view, using a table (say my_offerings) which has a rails-friendly ID, and the two PK columns from the other table (term_id, class_number) and join it with the other table in code.
Then, have a small process that parses the offerings table, and builds any missing my_offering record.
In the end the view will have an ID, and the join in the view (that's transparent for rails) will deal with the composite key.
Related
What are my options for molding existing database table(s) to my model in rails? I have a has_one and belongs_to relation between two tables, but I'd like to join them and use that as a model (and select only the fields relevant). As this is an external table I'd also like to minimize the amount of queries.
I am inheriting an existing app and would like to not touch anything from the existing environment and slowly migrate. The existing database seems to have been made different from the rails way. I have a model of IdCard and IdCardRequest. One would assume that one IdCard hasmany IdCardRequests, however the IdCard has a property to the last IdCardRequest. It seems that the basic info such as applicant_name is a property of the IdCardRequest rather than IdCard. Luckily they both have a common property id_card_number and I could join it based on that by specifying foreign_key and primary_key to id_card_number. However for now I'd like a model IdCard with the rest of the fields of the IdCardRequest as property.
class IdCard < ExternalTable
self.table_name = 'id_cards'
belongs_to :security_id_request, :foreign_key => 'request_id'
default_scope { includes(:id_request) }
end
class IdRequest < ExternalTable
self.table_name = 'id_request'
has_one :id_card, :foreign_key => 'request_id'
end
# I would like IdCard.first.applicant_lastname
# I have to call IdCard.first.id_request.applicant_lastname
# I have to call IdCard.first.id_request.applicant_firstname
# I could write a delegate_to for every property, but this seems cumbersome and inefficient.
Do you have the option of creating a database view that encapsulates both tables, and renames columns to rails conventions?
e.g.
create view id_card_requests as
select
existing_column as desired_rails_column_name,
...
from id_cards
join id_card_requests on <whatever the join is>
You can then make a rails model IdCardRequests that will work as normal. You can make one of the columns a primary key in the view, or tell the model to use one of the columns with self.primary_key = :my_key_column
Kind of new to Ruby/Rails, coming from c/c++, so I'm doing my baby steps.
I'm trying to find the most elegant solution to the following problem.
Table A, among others has a foreign key to table B (let's call it b_id), and table B contains a name field and a primary (id).
I wish to get a list of object from A, based on some criteria, use this list's b_id to access Table B, and retrieve the names (name field).
I've been trying many things which fail. I guess I'm missing something fundamental here.
I tried:
curr_users = A.Where(condition)
curr_names = B.where(id: curr_users.b_id) # fails
Also tried:
curr_names = B.where(id: curr_users.all().b_id) # fails, doesn't recognize b_id
The following works, but it only handles a single user...
curr_names = B.where(id: curr_users.first().b_id) # ok
I can iterate the curr_users and build an array of foreign keys and use them to access B, but it seems there must be more elegant way to do this.
What do I miss here?
Cheers.
Assuming you have following models:
class Employee
belongs_to :department
end
class Department
has_many :employees
end
Now you can departments based on some employee filter
# departments with employees from California
Department.include(:employees).where(:employees => {:state => "CA"}).pluck(:name)
For simplicity, let's take an example of Article and Comments, instead of A and B.
A Comment has a foreign key article_id pointing at Article, so we can setup a has_many relationship from Article to Comment and a belongs_to relationship from Comment to Article like so:
class Article < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :article
end
Once you have that, you will be able do <article>.comments and Rails will spit out an array of all comments that have that article's foreign key. No need to use conditionals unless you are trying to set up a more complicated query (like all comments that were created before a certain date, for example).
To get all the comment titles (names in your example), you can do <article>.comments.map(&:title).
Does anyone know the way, or a place where I can find out how to do this?
Basically, all I want to do is connect a foreign key between two tables.
Is it true, that all I have to do is write the "belongs_to" and "has many" ?
You also need to ensure that a column exists for the foreign key in the database table associated with the Class that says it "belongs_to" the other one. So for the classes...
Class Tree
belongs_to :forest
end
Class Forest
has_many :trees
end
...Rails assumes that your trees table has a forest_id column. You can then do, for example,
my_tree = Tree.find(1)
my_trees_forest = my_tree.forest
Here's a great place to get the info you need: http://guides.rubyonrails.org/association_basics.html
I have two models: Show and Venue. Show has one venue while each venue belongs to show. This condition is defined in both model files with has_one & belongs_to statements properly. However, I'm not able to access the venue by doing show.venue. Consider the following code where s is a Show instance:
logger.info("*********************")
logger.info("#{s.inspect}")
logger.info("#{Venue.find(s.venue_id)}") # Works
logger.info("#{s.venue}") # Causes a MySQL Error
logger.info("*********************")
I feel like the line that causes the MySQL error should work. This is the error:
ActiveRecord::StatementInvalid (Mysql::Error: Unknown column 'venues.show_id' in 'where clause': SELECT * FROM `venues` WHERE (`venues`.show_id = 95) LIMIT 1)
I have no idea why it is trying to access venues.show_id. Any ideas?
You have the foreign keys reversed. In ActiveRecord's conventions, the class with the belongs_to should map to the database table with the foreign key. See the ActiveRecord API: "The belongs_to association is always used in the model that has the foreign key." This makes some sense, if you think about the way that belongs_to interacts with both has_one and has_many (as you obviously can't put the foreign keys in the has_many model).
Why can you not have a foreign key in a polymorphic association, such as the one represented below as a Rails model?
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
class Event < ActiveRecord::Base
has_many :comments, :as => :commentable
end
A foreign key must reference only one parent table. This is fundamental to both SQL syntax, and relational theory.
A Polymorphic Association is when a given column may reference either of two or more parent tables. There's no way you can declare that constraint in SQL.
The Polymorphic Associations design breaks rules of relational database design. I don't recommend using it.
There are several alternatives:
Exclusive Arcs: Create multiple foreign key columns, each referencing one parent. Enforce that exactly one of these foreign keys can be non-NULL.
Reverse the Relationship: Use three many-to-many tables, each references Comments and a respective parent.
Concrete Supertable: Instead of the implicit "commentable" superclass, create a real table that each of your parent tables references. Then link your Comments to that supertable. Pseudo-rails code would be something like the following (I'm not a Rails user, so treat this as a guideline, not literal code):
class Commentable < ActiveRecord::Base
has_many :comments
end
class Comment < ActiveRecord::Base
belongs_to :commentable
end
class Article < ActiveRecord::Base
belongs_to :commentable
end
class Photo < ActiveRecord::Base
belongs_to :commentable
end
class Event < ActiveRecord::Base
belongs_to :commentable
end
I also cover polymorphic associations in my presentation Practical Object-Oriented Models in SQL, and my book SQL Antipatterns Volume 1: Avoiding the Pitfalls of Database Programming.
Re your comment: Yes, I do know that there's another column that notes the name of the table that the foreign key supposedly points to. This design is not supported by foreign keys in SQL.
What happens, for instance, if you insert a Comment and name "Video" as the name of the parent table for that Comment? No table named "Video" exists. Should the insert be aborted with an error? What constraint is being violated? How does the RDBMS know that this column is supposed to name an existing table? How does it handle case-insensitive table names?
Likewise, if you drop the Events table, but you have rows in Comments that indicate Events as their parent, what should be the result? Should the drop table be aborted? Should rows in Comments be orphaned? Should they change to refer to another existing table such as Articles? Do the id values that used to point to Events make any sense when pointing to Articles?
These dilemmas are all due to the fact that Polymorphic Associations depends on using data (i.e. a string value) to refer to metadata (a table name). This is not supported by SQL. Data and metadata are separate.
I'm having a hard time wrapping my head around your "Concrete Supertable" proposal.
Define Commentable as a real SQL table, not just an adjective in your Rails model definition. No other columns are necessary.
CREATE TABLE Commentable (
id INT AUTO_INCREMENT PRIMARY KEY
) TYPE=InnoDB;
Define the tables Articles, Photos, and Events as "subclasses" of Commentable, by making their primary key be also a foreign key referencing Commentable.
CREATE TABLE Articles (
id INT PRIMARY KEY, -- not auto-increment
FOREIGN KEY (id) REFERENCES Commentable(id)
) TYPE=InnoDB;
-- similar for Photos and Events.
Define the Comments table with a foreign key to Commentable.
CREATE TABLE Comments (
id INT PRIMARY KEY AUTO_INCREMENT,
commentable_id INT NOT NULL,
FOREIGN KEY (commentable_id) REFERENCES Commentable(id)
) TYPE=InnoDB;
When you want to create an Article (for instance), you must create a new row in Commentable too. So too for Photos and Events.
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 1
INSERT INTO Articles (id, ...) VALUES ( LAST_INSERT_ID(), ... );
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 2
INSERT INTO Photos (id, ...) VALUES ( LAST_INSERT_ID(), ... );
INSERT INTO Commentable (id) VALUES (DEFAULT); -- generate a new id 3
INSERT INTO Events (id, ...) VALUES ( LAST_INSERT_ID(), ... );
When you want to create a Comment, use a value that exists in Commentable.
INSERT INTO Comments (id, commentable_id, ...)
VALUES (DEFAULT, 2, ...);
When you want to query comments of a given Photo, do some joins:
SELECT * FROM Photos p JOIN Commentable t ON (p.id = t.id)
LEFT OUTER JOIN Comments c ON (t.id = c.commentable_id)
WHERE p.id = 2;
When you have only the id of a comment and you want to find what commentable resource it's a comment for. For this, you may find that it's helpful for the Commentable table to designate which resource it references.
SELECT commentable_id, commentable_type FROM Commentable t
JOIN Comments c ON (t.id = c.commentable_id)
WHERE c.id = 42;
Then you'd need to run a second query to get data from the respective resource table (Photos, Articles, etc.), after discovering from commentable_type which table to join to. You can't do it in the same query, because SQL requires that tables be named explicitly; you can't join to a table determined by data results in the same query.
Admittedly, some of these steps break the conventions used by Rails. But the Rails conventions are wrong with respect to proper relational database design.
Bill Karwin is correct that foreign keys cannot be used with polymorphic relationships due to SQL not really having a native concept polymorphic relationships. But if your goal of having a foreign key is to enforce referential integrity you can simulate it via triggers. This gets DB specific but below is some recent triggers I created to simulate the cascading delete behavior of a foreign key on a polymorphic relationship:
CREATE FUNCTION delete_related_brokerage_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Brokerage' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_brokerage_subscriber_delete
AFTER DELETE ON brokerages
FOR EACH ROW EXECUTE PROCEDURE delete_related_brokerage_subscribers();
CREATE FUNCTION delete_related_agent_subscribers() RETURNS trigger AS $$
BEGIN
DELETE FROM subscribers
WHERE referrer_type = 'Agent' AND referrer_id = OLD.id;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cascade_agent_subscriber_delete
AFTER DELETE ON agents
FOR EACH ROW EXECUTE PROCEDURE delete_related_agent_subscribers();
In my code a record in the brokerages table or a record in the agents table can relate to a record in the subscribers table.
Let's say you want to create a like system in SQL. in polymorphic association you create a LIKE table with those columns:
id_of_like user_id liked_id liked_type
1 12 3 image
2 3 5 video
liked_id refers to id of image or id of video.
Normally when you create a table, you set a foreign key and tell the SQL database which table will this foreign key reference. Before you insert the row, your database will check if the current foreign key value is valid or not. for example if in a table where foreign key references USER table, your database will check if the current foreign key exists in the USER table. This verification makes sure your database is consistent.
Let's say in the above table, we somehow set the liked_id as a FOREIGN KEY, how the database would know which table (IMAGE or VIDEO) to visit to validate the current key value? liked_type is not for the database manager it is for humans to read it, database manager does not read it to check which table to go.
Imagine I inserted a new row with liked_id=333333
id_of_like user_id liked_id liked_type
1 12 333333 image
your database manager will not be able to validate if id=333333 exist in IMAGE table or not.