ActiveRecord and use of has_one & has_many - ruby

Consider this simple model, where a Project has one ProjectType and, naturally many Projects can be of that type.
So a Project has_one :project_type (called type) and a ProjectType has_many :projects.
In my migration I put (simplified for this example)
create_table :projects do |t|
t.string :name, :null => false
t.integer :type
end
create_table :project_types do |t|
t.string :name, :null => false
end
My Project class looks like this (again simplified for this example)
#!usr/bin/ruby
require 'active_record'
class Project < ActiveRecord::Base
has_one :type, :class_name => 'ProjectType'
end
And my ProjectType looks like
#!usr/bin/ruby
require 'active_record'
class ProjectType < ActiveRecord::Base
has_many :projects
end
I've written a simple Unit Test to check this works
#test creation of Projects and related objects.
def test_projects_and_etc
pt = ProjectType.create(:name => 'Test PT')
project = Project.create(:name => 'Test Project', :type => pt)
assert project.type.name == 'Test PT', "Wrong Project Type Name, expected 'Test PT' but got '#{project.type.name}'."
# clean up
project.destroy
pt.destroy
end
This test throws an error at the assert, saying
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: project_types.project_id: SELECT "project_types".* FROM "project_types" WHERE ("project_types".project_id = 1) LIMIT 1
The SQL seems to be assuming that there is a project_id field in the project_types table but that makes no sense if a ProjectType can be associated with many Projects. I suspect that my problem is something to do with my wanting to be able to refer to the ProjectType as project.type not project.project_type, but I am not sure how I'd fix this.

You need to use belongs_to instead of has_one on the project model.
You also need to add a project_type_id column on to the projects table.

Related

avoiding destroy with foreign key

I'm new in Ruby on Rails. I don't understand how rails behave using foreign Key, I've researched it for some days but I didn't get the answer.
Simple sample:
I created two tables:
class CreatePosts < ActiveRecord::Migration
def change
create_table :posts do |t|
t.string :title
t.text :content
t.timestamps null: false
end
end
end
class CreateComments < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :author
t.text :content
t.references :post, index: true, foreign_key: true
t.timestamps null: false
end
end
end
My models are:
class Post < ActiveRecord::Base
has_many :comments
end
class Comments < ActiveRecord::Base
belongs_to :post
end
My doubt is: As I have a Foreign Key in my table COMMENTS (.references :post, index: true, foreign_key: true) I guess that I wouldn't be able to destroy any post which has any COMMENTS associated to them, isn't it ?
I did as above but I am still able to destroy the posts, even when I have the comments associated. How can I treat it? What am I doing wrong?
Cheers
I'd refine your migrations to use the :on_delete options on your foreign keys. It can take one of those values : :nullify, :cascade, :restrict
From what I understand, you need to set this value to :restrict on your post_id column in your comments table, so that posts with associated comments can't be deleted.
Update:
Or, you could also directly set it on the association in your Post model:
has_many :comment, dependent: :restrict_With_error
Please take a look at:
http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many -> See the Options: Section
From what i understand, you dont want to destroy a post if there are associated comments?
Why not put a if statement encapsulating the delete button for a post
So something like:
psudo code
if #post.comments exists
cant delete post
else
delete post
end

Rails Associations: with self through second model

I have two models Classification and ClassificationRelationships. I want to create a hierarchy of classifications using supperclass and subclass so that each classification can have many subclasses but only one superclass.
my migrations look like this
class CreateClassifications < ActiveRecord::Migration[5.0]
def change
create_table :classifications do |t|
t.string :symbol
t.string :title
t.integer :level
t.timestamps
end
add_index :classifications, :symbol
add_index :classifications, :level
end
end
class CreateClassificationRelationships < ActiveRecord::Migration[5.0]
def change
create_table :classification_relationships do |t|
t.integer :superclass_id
t.integer :subclass_id
t.timestamps
end
add_index :classification_relationships, :superclass_id
add_index :classification_relationships, :subclass_id
add_index :classification_relationships, [:superclass_id, :subclass_id], unique: true, name: 'unique_relationship'
end
end
so far with my models I have
class ClassificationRelationship < ApplicationRecord
belongs_to :superclass, :class_name => "Classification"
belongs_to :subclass, :class_name => "Classification"
end
class Classification < ApplicationRecord
has_many :classification_relationships
has_many :subclasses, through => :classification_relationships
has_one :superclass, through => :classification_relationships
end
I read quite a few other posts but am still unsure how to finish the associations. I am pretty sure I need to specify the foreign keys but am not clear on how I should do that. Thanks for the help!
Get rid of ClassificationRelationship.
All you need is for Classification to have a parent_id which, in the root instances, is allowed to be null.
Add:
belongs_to :parent, class_name: 'Classification', foreign_key: :parent_id
def children
Classification.where(:parent_id => self.id)
end
Some operations will not be optimal. e.g. Find all descendants. That's because this will require repeated queries to find children, their children, etc...
This may not be a concern for you.
If it is, I recommend storing a path as such:
after_create :set_path
def set_path
path = parent ? "#{parent.path}#{self.id}/" : "#{self.id}/"
self.update_attributes!(:path => path)
end
Then you can do things like:
def descendants
Classification.where("classifications.path LIKE '#{self.path}%' AND classifications.path <> '#{self.path}'")
end
Of course, make sure path is indexed if you'll be doing queries like that.

Configuring the proper join column in Rails Admin

I have two models, which associate with each other through a has_and_belongs_to_many relationship.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands
end
Assume both tables have an id field, and that stage has a stage_name field.
They're related to each other through a table called bands_stages, with a schema that looks similar to this:
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
My intention is to use Rails Admin to allow us to modify certain fields on the Stage, but every time that runs, I get an SQL error doing so:
column stages.id does not exist
It seems that Rails Admin is picking the wrong column by default to join on. How would I inform Rails Admin that I want it to join on a column that actually exists in my join table?
Note that I can't actually make use of the ID in the stages table. The intention is that only ten stages exist at any given time, denoted by their stage number, but every band can visit each stage. Since an ID would automatically increment, it seems to be safer and more explicit to its intent to leverage the more concrete :stage_number field instead.
I'm sure that it's not a problem of rails admin but habtm association.
To make habtm use the right column in sql primary key must be specified for stage model and foreign key for association.
And it is the only way to make it works right.
class Stage < ActiveRecord::Base
self.primary_key = "stage_number"
has_and_belongs_to_many :bands, foreign_key: :stage_number
end
But I think the best way is to use joint model and has_many/belongs_to because for has_many/belongs_to it's possible to set any column to be used as primary key via :primary_key option.
class BandStageLink < ActiveRecord::Base
self.table_name = "bands_stages"
belongs_to :band
belongs_to :stage, foreign_key: :stage_number, primary_key: :stage_number
end
class Band < ActiveRecord::Base
has_many :band_stage_links
has_many :stages, through: :band_stage_links, foreign_key: :stage_number
end
class Stage < ActiveRecord::Base
has_many :band_stage_links, primary_key: :stage_number, foreign_key: :stage_number
has_many :bands, through: :band_stage_links
end
Update: Note that in this case there is still no need to specify any primary keys for stage table. For instance my migration is:
class CreateStageBandTables < ActiveRecord::Migration
def change
create_table :bands_stages, id: false do |t|
t.integer :band_id
t.integer :stage_number
end
create_table :bands do |t|
t.string :name
end
create_table :stages, id: false do |t|
t.integer :stage_number
t.string :name
end
end
end
I tested both cases for rails 4.2.5 and everything works just fine.
Edit - I did mis-understand the primary key bit, I think the desire was to tell Rails to use different attribute as PK, which should be less problematic than re-purposing the auto-increment-by-default PK ID. In that case, the Stage model should include self.primary_key = "stage_number", and the rest of the details at the bottom of this answer relating to HABTM alongside that. Of course has-many-through would still be my preferred solution here.
I think there's a bigger problem with the models and approach, than Rails Admin.
If I understand what you're trying to do, then you'd also need to turn off auto-increment for the primary key in stages table, to hold arbitrary numbers (representing stage numbers) as primary key IDs. It could end badly very quickly, so I'd advise against it.
If the data is genuinely static (10 stages ever), you could even keep it as a constant in the Band model and scrap Stage completely (unless there's more there), e.g.
class Band < ActiveRecord::Base
POSSIBLE_STAGES = [1, 2, ...]
validates :stage, inclusion: { in: POSSIBLE_STAGES, message: "%{value} is not a stage we know of!" }
end
For a table-based approach, I would suggest has-many-through, it'll save you a lot of pain in the future (even if you don't need additional attributes on the join table, things like nested forms are a little easier to work with than in HABTM). Something like this:
class Band < ActiveRecord::Base
has_many :events
has_many :stages, through :events
# band details go into this model
end
class Event < ActiveRecord::Base
belongs_to :band
belongs_to :stage
# you could later add attributes here, such as date/time of event, used_capacity, attendee rating, and
# even validations such as no more than X bands on any given stage at the same time etc.
end
class Stage < ActiveRecord::Base
has_many :events
has_many :bands, through :events
# stage number/details go into this model
end
The migration for that could look something like this:
create_table :bands do |t|
t.string :bandname
# whatever else
end
create_table :events do |t|
t.belongs_to :band
t.belongs_to :stage
# you could add attributes here as well, e.g. t.integer :used_capacity
end
create_table :stages do |t|
t.integer :number
t.integer :total_capacity
# whatever else
end
As you can see primary key IDs are not touched here at all, and I would always avoid storing business data in Rails' and databases' plumbing of any sort (which is what I consider IDs to be, they're there to ensure relation/integrity of the data in a relational database, as well as nice and consistent mapping to ActiveRecord - all business data should be beside that, in actual attributes, not plumbing used to connect models).
If you still want HABTM and re-purposing of primary ID, then I think Stage should include a foreign_key statement to "advertise" itself to the bands_stages join table as having a custom key name (in bands_stages only), while keeping the association_foreign_key on the Band end to show what you want to query in the join table to reach the other side. The stages table would still utilise id though as its primary key, you'd just want to turn off auto-increment with something like t.integer :id, :options => 'PRIMARY KEY' (might be dependent on the database flavour - and again, I would advise against this).
Your models would look like this:
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: "stage_number"
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: "stage_number"
end
The connection between bands and bands_stages would be bands.id = bands_stages.band_id, for which many bands_stages.stage_number would be found, and each would be connected to stage via bands_stages.stage_number = stages.id (where stages.id has been re-purposed to represent business data at a likely future peril).
Change the association_foreign_key value to be a string instead of symbol.
class Band < ActiveRecord::Base
has_and_belongs_to_many :stages, association_foreign_key: 'stage_number'
end
class Stage < ActiveRecord::Base
has_and_belongs_to_many :bands, foreign_key: 'stage_number'
end

Active Record Retrieve belongs_to

I have the two following models associated:
class Post < ActiveRecord::Base
belongs_to :language
def self.are_visible
self.where(:visible => true)
end
end
class Language < ActiveRecord::Base
has_many :posts
end
Schema.rb
create_table "languages", force: true do |t|
t.string "name_de"
t.string "name_en"
end
create_table "posts", force: true do |t|
t.string "title"
t.text "description"
t.integer "language_id"
end
add_index "posts", ["language_id"], name: "index_posts_on_language_id"
How can I list all languages of all visible stores without duplicates?
I want something like this:
#languages = Post.are_visible.select(:language).uniq
But this leads to the following error
PG::UndefinedColumn: ERROR: column "language" does not exist
Of course this column does not exist, only the column language_id exists on the table.
I am wondering why this is so complicated because in C# Linq I would just write:
Repository.Posts.Where(p => p.Visible).Select(p => p.Language).Distinct()
And I would get all Locations of matching posts. But somehow I think I need to change my approach fundamentally to get this in active record.
Update: Got it working the following way:
#languages = Post.joins(:language).are_visible.uniq.pluck(:name_de)
The way you have it set up, you have only one language per post record. You don't have them related in a way you can do this with ActiveRecord; however, you can get posts by language.
#posts = #language.posts
This might make more sense to have a has and belongs to many relationship between these models.
Have you tried with scope? Then you should be able to use select or get languages of visible posts:
class Post < ActiveRecord::Base
belongs_to :language
scope -> :are_visible { where(visible: true) }
end
of course if you have a column visible in Post table which takes only boolean values.
Edit:
Try add join:
#languages = Post.joins(:language).are_visible.select(:language).uniq
Okay, so I did not get this to work so I changed my approach a bit.
#languages = Post.joins(:language).are_visible.uniq.pluck(:name_de)

has_many or join - what's the 'rails way' to use my table?

I have a database that keeps track of accidents. Each accident can have multiple causes. Each cause has a friendly name in a 3rd table. What's the 'rails way' to create this association?
Here's what I have:
create_table "accidents", force: true do |t|
t.text "notes"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "causes", force: true do |t|
t.integer "accident_id"
t.integer "cause_name_id"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "cause_names", force: true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
CauseName.create :name => "DUI"
CauseName.create :name => "Speeding"
Accident:
class Accident ActiveRecord::Base
has_many :causes
end
Causes:
class Cause < ActiveRecord::Base
belongs_to :accidents
has_one :cause_name
end
cause names:
class CauseName < ActiveRecord::Base
belongs_to :causes
end
It seems like to be properly "ORM"'d, I'd use it like this:
Accident.causes.first.cause_name.name #speeding
a = CauseName.find(1)
Accident.causes.first.cause_name = a #set and saved successfully
I've been trying a variety of things, but I can't seem to get my associations to work the way I'd expect. I know I'm not using it right.
I'm very new to rails and activerecord, and horrible with databases... the schema I'm working with was designed by our dba who will be doing reporting on the table, but knows nothing about Ruby or ActiveRecord.
What's the best approach in my situation? Am I even using this thing right?
I think you have your belongs_to and has_one methods placed incorrectly in your Cause - CauseName association.
Quoting the official guide:
If you want to set up a one-to-one relationship between two models,
you'll need to add belongs_to to one, and has_one to the other. How do
you know which is which?
The distinction is in where you place the foreign key (it goes on the
table for the class declaring the belongs_to association), but you
should give some thought to the actual meaning of the data as well.
The has_one relationship says that one of something is yours - that
is, that something points back to you.
So, in your case, it should be like this:
class CauseName < ActiveRecord::Base
has_one :cause # Note that I have changed :causes to singular too
end
class Cause < ActiveRecord::Base
belongs_to :accident # <- Singularized too
belongs_to :cause_name
end
In your case, I'd suggest not splitting causes into two tables. Just cram name into causes table and call it a day.
Then you can easily query like #accident.causes.first.name

Resources