Create new ruby model instance with associations - mysql

This is perhaps a basic question, but it's currently driving me nuts...Perhaps i'm missing something, since i'm diving so deep in the code, but the question is:
How can I make my object with associations?
I have the following migration scripts:
class CreateConcepts < ActiveRecord::Migration
def self.up
create_table :concepts do |t|
t.integer :language_id, :null => false
t.string :uri
t.integer :isco_code
t.timestamps
end
end
def self.down
drop_table :concepts
end
end
class CreatePrefLabels < ActiveRecord::Migration
def self.up
create_table :pref_labels do |t|
t.integer :language_id
t.integer :concept_id
t.string :value
t.timestamps
end
end
def self.down
drop_table :pref_labels
end
end
class CreateLanguages < ActiveRecord::Migration
def self.up
create_table :languages do |t|
t.string :code
t.timestamps
end
end
def self.down
drop_table :languages
end
end
The classes for these objects look like the following:
class Concept < ActiveRecord::Base
belongs_to :language
has_one :pref_label
validates_uniqueness_of :isco_code
end
class PrefLabel < ActiveRecord::Base
belongs_to :language
belongs_to :concept
validates_uniqueness_of :value
end
class Language < ActiveRecord::Base
has_many :concepts
has_many :pref_labels
validates_uniqueness_of :code
end
So if I remember my Ruby lessons correctly, the following code should be perfectly fine:
concept = Concept.first
concept.pref_label
language = Language.find(:code => "en")
language.pref_labels
language.concepts
So i've written the following line of code. The internals are a bit dark, but I'm 100% sure the hashes generated from the JSON data are correct. Checked this with the debugger:
# This function will retrieve all the top most concepts from the ESCO API in the
# specified language as an array of Ruby objects, each represented by the relevant class.
# If the relevant concept does not exist in the database, the entry is automatically created.
# The function will fall back to English by default if no language is specified
def self.top_concepts(lang = Language.find_by_code('en') || Language.create(:code => 'en'))
concepts = []
json = HTTParty.get "#{Setting[:api]}/getTopmostConcepts?language=#{lang.code}"
collection = JSON.parse json.parsed_response
collection.each do |c|
language = Language.find_by_code c['language'] || Language.create(:code => c['language'])
concept = Concept.create(:language => language, :uri => c['uri']['uri'], :isco_code => c['iscoCode'].to_i)
label = PrefLabel.find_by_concept_id(concept.id) || PrefLabel.create(:language_id => language.id, :concept_id => concept.id, :value => c['prefLabel']['string'])
concept.pref_label= label
concept.save
concepts << concept
end
return concepts
end
The problem I'm having now is that my PrefLabels are :
1) not beeing created all the time
2) are never linked to my concept objects.
What am I missing?

First, I would suggest simplifying this snippet of your code a bit like so:
language = Language.find_or_create_by_code(c['language'])
concept = langauge.concepts.create(:uri => c['uri']['uri'], :isco_code => c['iscoCode'].to_i)
concept.pref_label = PrefLabel.find_or_create_by_concept_id(:concept_id => concept.id, :language_id => language.id, :value => c['prefLabel']['string'])
That's untested so it might not be quite right.
Second, check to see if validation is passing or not. valid? will return false if there is a validation issue of some sort -- for example, if the PrefLabel value isn't unique.
I suspect you need to scope your validations, although that's just a guess of course.

Related

Why isn't Ruby on Rails loading my associated objects with .includes()?

My Problem
I'm attempting to retrieve data through a foreign key association in my Ruby on Rails application. The data from the primary table is loaded correctly, but the associated objects are not being loaded and are always nil.
Background Info (Migrations, Database tables, and Model classes)
I'm currently working with two tables:
eval_forms
user_details
The tables are created through Rails migrations.
The user_details table is created through a single migration:
class CreateUserDetails < ActiveRecord::Migration[5.2]
def change
create_table :user_details do |t|
t.string :eduPersonPrincipalName, unique: true
t.string :DisplayName, default: 'NULL'
t.string :Email, default: 'NULL'
t.string :Role, default: 'Student'
t.boolean :hasAppointment, default: '0'
t.timestamps
end
end
def self.down
drop_table :user_details
end
end
and the eval_forms table has had a few migrations to create and update it:
class CreateEvalForms < ActiveRecord::Migration[6.1]
def change
create_table :eval_forms do |t|
t.belongs_to :form_builder, foreign_key: 'form_builder_id'
t.belongs_to :course, foreign_key: 'course_id'
t.string :Description
t.datetime :OpenDate
t.datetime :CloseDate
t.timestamps
end
end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
def change
add_column :eval_forms, "Author_user_details_id", :bigint, null: false
add_foreign_key :eval_forms, :user_details, column: "Author_user_details_id"
add_column :eval_forms, "Year", :integer
add_column :eval_forms, "Semester", :string
add_column :eval_forms, "IsArchived", :boolean
end
end
I know that the foreign key is set up correctly as it is listed correctly in MySQL. Here's a reference from MySQL of the 2 tables and their relation:
Additionally, I've set up the model classes in my Rails app.
eval_form:
class EvalForm < ApplicationRecord
has_many :eval_forms_roles
has_many :roles, through: :eval_forms_roles
has_many :eval_forms_courses
has_many :courses, through: :eval_forms_courses
has_many :eval_responses
has_many :eval_reminders
belongs_to :user_detail
validates :formName, presence: true
validates :formData, presence: true
end
user_detail:
class UserDetail < ApplicationRecord
has_one :la_detail
has_many :eval_responses
has_many :eval_forms
end
So What's Wrong?
Lastly, here is the code to retrieve the objects from the database and the section where I'm getting my error.
My controller action:
def index
# list *all* existing evaluation forms, with options to filter by OpenDate, CloseDate, etc (todo)
#EvalForms = EvalForm.includes(:user_detail)
end
My view:
<td><%= ef.user_detail.DisplayName %></td>
My error:
NoMethodError in Evaluations::EvalForms#index
undefined method `DisplayName' for nil:NilClass
Extracted Source location: <td><%= ef.user_detail.DisplayName %></td>
Restating the problem
In conclusion, I'm really confused as to why the associated user_detail objects are not being retrieved despite my .includes() statement in the controller action. I'm pretty new to Ruby as well as Rails, but there are other sections in the application that look similar to this and work correctly so I don't see what my issue is.
I would start by using conventional naming which in Rails means snake_case everywhere:
class CreateUserDetails < ActiveRecord::Migration[5.2]
def change
create_table :user_details do |t|
t.string :edu_person_principal_name, unique: true
t.string :display_name
t.string :email
t.string :role, default: 'Student'
t.boolean :has_appointment, default: false # let the driver handle conversion
t.timestamps
end
end
end
class UpdateEvalForms < ActiveRecord::Migration[6.1]
def change
change_table :eval_forms do |t|
t.belongs_to :author_user_details, foreign_key: { to_table: :user_details }
t.integer :year # consider using `YEAR(4)` instead
t.string :semester
t.boolean :is_archived, default: false
end
end
end
If you continue using a strange mix of camelCase and PascalCase you will need to explicitly configure all your associations and you will lose all the advantages of convention over configuration. I would not recommend this at all unless you're stuck with a legacy database as its a surefire recipe for developer confusion and bugs.
You will also get a missing constant error if you call the PascalCase methods without an explicit recipient (self):
class EvalForm < ApplicationRecord
def short_description
# uninitialized constant Description (NameError)
Description.truncate(27, separator: ' ')
end
end
While you can fix this with self.Description.truncate(27, separator: ' ') its still very smelly.
In this case if you want to call the column author_user_details_id instead of user_details_id which is derived from the name you need to configure the assocation to use the non-conventional name:
class EvalForm < ApplicationRecord
belongs_to :user_detail, foreign_key: :author_user_details_id
end
class UserDetail < ApplicationRecord
has_many :eval_forms, foreign_key: :author_user_details_id
end
If the rest of your schema looks like this you'll have to do this across the board.

Trying to relate tables in MySQL and Rails

I'm having problems relating some tables, I have the client table with the fields ("name, age and gender") and another table called personal_documents with the fields "cpf, rg, etc ...), I tried the relationship of personal_documents belongs_to client but when i search for client only the fields of client ("name, age and gender) and "personal_documents_id" appear, the fields for personal documents ("cpf, rg, etc...) should also appear too, thanks for the help!
Code:
In client model:
has_one :personal_documents
in personal_documents model:
belongs_to :client
rails generate model Client
inside migration file you create as follow
class CreateClients < ActiveRecord::Migration[6.0]
def change
create_table :clients do |t|
t.string :user_kind
# your other field here
t.timestamps
end
end
end
rails generate model PersonalDocument
inside migration file you create as follow
class CreatePersonalDocuments < ActiveRecord::Migration[6.0]
def change
create_table :personal_documents do |t|
# this is the one that relate personal document
# to client
t.references :client, index: true
t.string :rg_front
# other field
t.timestamps
end
end
end
inside model you can declare as follow
class Client < ApplicationRecord
# please note personal_document in singular
has_one :personal_document, dependent: :destroy
accepts_nested_attributes_for :personal_document, allow_destroy: :true
# now you can do some like above for disponibility, personal_document_legal, bank_information
end
class PersonalDocument < ApplicationRecord
belongs_to :client
end
inside your controller you declare as follow
class ClientsController < ApplicationController
def barang_params
params.require(:client).permit(
:user_kind,
personal_document_attributes: [
:id,
:rg_front,
:rg_back,
:cpf,
:cnh_front,
:cnh_back,
:bank_card_front,
:address_proof,
:profile_picture
]
# this from your other question, and I think it's already correct
)
end
end
To access personal_documents of client
Client.find(1).personal_documents.cpf
To access client of personal_documents
PersonalDocument.find(id).client.name
both
document = PersonalDocument.find(id)
client = document.client
or
client = Client.find(1)
document = client.personal_documents
document.cpf
client.name
additionaly change :has_one to singular personal_document

How to build M to M relationships between two data model

Condition:
Tutor can follow or dis-follow student
Student can follow or dis-follow tutor
Tutor and Student are two different data models.
I build a middle data model calledapplication connecting both model
Application model:
class CreateApplications < ActiveRecord::Migration
def change
create_table :applications do |t|
t.belongs_to :tutor, index: true
t.belongs_to :student, index: true
t.timestamps null: false
end
end
end
application.rb
class Application < ActiveRecord::Base
belongs_to :tutor
belongs_to :student
end
student.rb
has_many :applications, :dependent => :destroy
has_many :tutors, through: :applications
tutor.rb
has_many :applications, :dependent => :destroy
has_many :students, through: :applications
schema.rb
ActiveRecord::Schema.define(version: 20170421093747) do
create_table "applications", force: :cascade do |t|
t.integer "tutor_id"
t.integer "student_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "applications", ["student_id"], name: "index_applications_on_student_id"
add_index "applications", ["tutor_id"], name: "index_applications_on_tutor_id".......
show.html.erb
# -->show who is the follower & person who followed & How many of them
# Error :undefined method `student' for nil:NilClass
<%= #application.student.count %> #undefined method `student' for nil:NilClass
<%= #application.tutor.count % >#undefined method `student' for nil:NilClass
<%= #application.student%> #undefined method `student' for nil:NilClass
In your controller, you need to set the #application variable.
def show
#application = Application.find(params[:id])
end
I would strongly advise against naming this class Application. The word "Application" has special meaning in Rails and you will almost certainly run into conflicts. Same goes for ApplicationController.
When model has many relationship with other model you want to use plural name, i.e. tutor.students or student.tutors
UPD: Also, you didn't pass variable into view properly, check your controller.
UPD1: Also, it's meaningless to use count on application record (btw you want to rename it to something like Courses, Classes or just StudentTutor because Application.rb is the model all your models inherit from by default, same goes to ApplicationController). It always has one student and one tutor (it belongs to both models). If you want to count related records you want to find student (if you want to count his tutors) or tutor (for vise-versa)

Rails rollback, change type and migrate again

I have a requests table;
class CreateRequests < ActiveRecord::Migration
def change
create_table :requests do |t|
t.string :from
t.string :to
t.timestamps null: false
end
end
end
I would like to rollback the database with rake db:rollback STEP = 5
destroy the Request model and create request table with;
t.datetime :from
t.datetime :to
However, I have a migration table to Request model on STEP = 2,
class AddStatusToRequest < ActiveRecord::Migration
def change
add_column :requests, :status, :string, :default => "Pending"
end
end
The problem is, if I destroy Request table and create new Request table with datetime types it creates after STEP = 2 and when I rake db:migrate rails does not add Status column to Request table. How can I overcome this?
You can create a new migration to change the column type using change_column:
command line: rails g migration change_request_to_from_column_types'
new migration:
class ChangeRequestToFromColumnTypes < ActiveRecord::Migration
def change
change_column :requests, :from, :datetime
change_column :requests, :to, :datetime
end
end
It's best not to retroactively change migrations after they've been applied to avoid state conflicts like the one you have. Perhaps the simplest solution is to create a new migration to add the types to the requests table using a guard to do nothing if the column already exists:
class AddStatusToRequestIfNotExists < ActiveRecord::Migration
def change
unless column_exists? :requests, :status
add_column :requests, :status, :string, :default => "Pending"
end
end
end
EDIT
You'll also want to make sure that you can run migrations from scratch, so you might want to update your existing migration to guard against a missing table as follows:
class AddStatusToRequest < ActiveRecord::Migration
def change
unless table_exists? :requests
add_column :requests, :status, :string, :default => "Pending"
end
end
end
Not ideal, but it's probably the safest approach.

Create a form for accessing relationships in RoR 3

This is kind of evolution of my previous question (although I changed a lot, including model names so figured I should better create another thread).. and the nature of question changed as well..
Currently I am struggling to create a form for the function
#dot2.link!(#dot)
The function works fine in console.
Here is the info from models / controllers (I tried to keep it minimal):
class User < ActiveRecord::Base
has_many :dots, :dependent => :destroy
....
end
Dot
class Dot < ActiveRecord::Base
belongs_to :user
has_many :linkages
...
def link!(new_dot)
linkages.create!(:end_id => new_dot.id)
end
...
end
Linkages:
class LinkagesController < ApplicationController
...
def create
#dot = current_user.dots.find(params[:linkages][:end_id])
#dot2 = Dot.find(params[:id])
#dot2.link!(#dot)
end
Linkages migration:
class CreateLinkages < ActiveRecord::Migration
def self.up
create_table :linkages do |t|
t.integer :start_id
t.integer :end_id
t.timestamps
end
add_index :linkages, :start_id
add_index :linkages, :end_id
add_index :linkages, [:start_id, :end_id], :unique => true
end
...
end
Now in console if I do
User.first.dots.first.link!(User.second.dots.second)
everything works fine.
How would I create a form for it (using just dot_id) as input??
Thanks!
Look up the collection_select form helper. It renders an association however you like, defaults to a Selectbox.