Migrations in Rails, difference between null and optional options? - ruby-on-rails

Im working on learning Rails, and have found that the details are where you can really end up sinking yourself in the future. As I'm creating a very simple store, doing migrations I have a few belongs_to's that are optional object relationships. As I was reading trying to better understand null here:
Understanding rails migration statement (:null => false)
It occurred to me that using null: true would allow that column to be null, and I was couldn't find any information / questions addressing the difference between:
create_table :items do |t|
...
t.belongs_to :shopping_cart, null: true
...
end
and
create_table :items do |t|
...
t.belongs_to :shopping_cart, optional: true
...
end
What should I be doing for this kind of optional relationship, I'm new to Ruby/Rails but "convention over configuration" has me wanting to understand the right way to do this. Thanks so much!

I think you may be confused about how optional works in a belongs_to relationship.
In a migration file, you're telling the database how a table is going to be structured. What columns are called, what kind of data they hold, whether or not NULL is an acceptable value, etc. This relates directly to your model code through the ActiveRecord ORM that Rails uses.
So in a migration, null: true means at the database level that NULL is an acceptable value for the given column. null: false would mean that NULL is not acceptable and that you always expect something to be stored there. Generally, null: false is used in conjunction with a default so that there's always a value in the column even if it's not specified when the record is created.
optional is used in the model code itself to tell ActiveRecord that a belongs_to relationship is expected, but it may not exist and that's ok. In your case, that would look something like this:
class Item < ApplicationRecord
belongs_to :shopping_cart, optional: true
end
That means that an item may have a shopping_cart_id present in the database, but it doesn't have to. Items can exist with a shopping_cart_id column that contains NULL.
Really null: true and optional aren't directly related. Nullability at the database level is determined by what's in a migration file. The presence of a foreign_key in a belongs_to relationship like items belonging to shopping carts is more about the model code than it is about the migration code.

Related

Polymorphic Association Rails : Different primary_keys types (id & uuid)

I would like to define a polymorphic table. My problem being that one table's primary_key is of type uuid(string) and the other id(integer).
I thought maybe having a model_able_id and a model_able_uuid varying depending on the model_type but i cannot figure that out and it would probably break tons of activerecord features for polymorphic.
Some other things i have thought of would be to use STI, but i'm still confused, and, of course, i could migrate ids to uuids and that'd sort me out (but i'd rather not).
Specify the type for the (polymorphic) foreign key in the options. Change :string to :uuid if required.
create_table :table_name do |t|
t.references :target, type: :string, polymorphic: true, null: false
end
Then both string and integer target_ids are acceptable.
Check the API docs.
Was facing same issue, there is no official solution yet, these two issues are open in rails repo about this:
https://github.com/rails/rails/issues/33407
https://github.com/rails/rails/issues/33525
There is a suggestion in one of these issues, it didn't work for me(it was generating both joins, the default one generated by rails which causes failure and one which i defined).
I ended up not using association at all, i defined a method to get the actual records properly with a custom query. I didn't need dependent: destroy otherwise i could have defined a before_destroy method too.
Hope it helps someone.
I had exactly the same problem. My solution modifies the type of the _id field to string.
def change
add_reference :ratings, :rater, polymorphic: true, index: true
change_column :ratings, :rater_id, :string
end
I hope it helps.

rails_admin: changed relationship, form not correct

I have a relationship between compositions and languages. One composition should be written in one and only one language.
My schema.rb contains the following lines:
...
create_table "compositions", force: :cascade do |t|
...
t.integer "product_language_id", null: false
end
...
add_foreign_key "compositions", "languages", column: "product_language_id"
...
I realized that the relationship was written wrong, so I changed the models to be like this:
previously there was a belongs_to :language line in composition.rb which I changed to has_one :language
previously there was a has_many :compositions line in language.rb which I changed to belongs_to :composition
Edit: FIRST QUESTION: is the procedure I made correct? I'm still a beginner at Ruby on Rails.
Now, in rails_admin, there's no possibility to select the language under the new form for composition, there is the line but no list, box nor anything, just the label name 'language', although I have an entry for it in its table.
Edit 2: Once reverting the relationship back to its initial status which I supposed it were incorrect, in rails_admin there is the possibility to add compositions from the language form, but I'd like to have also a drop down menu in the composition form to select the language, which is not appearing. Any suggestion?
Can you tell me where I'm failing? Thanks in advance
Thanks to this upwork freelancer I corrected it leaving the relationship in its original belongs_to / has_many status with the foreign key addition in composition.rb:
belongs_to :language, :foreign_key => 'product_language_id'
Rails assumes everything will be done according to convention. So foreign key of a table is expected to be tablename_id. When we break from the convention, we have to add additional options in our model to tell Rails that the foreign key is not what it expects, its something different.
We could write it this way as well:
belongs_to :product_language, class_name: 'Language'

Add custom fields to object in ROR application

I'm working on CRM platform.
I would like my users to add, edit and delete custom fields in Client, Contact and Lead objects. Those fields may be plain textfield, list, checkbox, tag etc. Those fields may be required or not. Those fields may have custom validation (that user will define).
Say one company from financials would like to add income to Client object, another would add order configuration to Lead object.
Is there any "enterprise-level" solution (ROR gem) for my problem.
Of cause I know about Custom configuration and config gem, but it doesn't look extensible enough.
Hard question, but this is how I would try to deal with it: I would make all the objects to be derived from a CustomField object, then I would create a one to many relationship between it and a Field model. Something like this:
create_table :field_types do |t|
t.string :name # This would identify the fields: checkbox, plain text, etc
end
create_table :fields do |t|
t.belongs_to :custom_field, null: false, index: true
t.belongs_to :field_type, null: false, index: true
t.string :name
end
class Field < ApplicationRecord
belongs_to :custom_field
belongs_to :field_type
end
class CustomField < ApplicationRecord
has_many :fields
end
This way you could just look into the specified fields on the database and mount it at the view.
Then I would create a table for each type of field that could be used by the users to save the data from the CustomField objects. For instance, I would check the Client field specifier, mount a view with checkboxes A and B. Then, I would get the data from the checkboxes and save each of them at the table Checkboxes with an identifier, so that I could tell that it came from clients.
Depending on what you need to do, another idea that pops to my head is to save the data as a JSON string into the database. This way you could have different fields with different values, all you would need to do is serialize and deserialize to save and load it from the database, respectively.
Sorry if it was a little confusing. Hope it helps.
Assuming your database is relational:
I would suggest to use Entity-Attribute-Value pattern:
https://en.wikipedia.org/wiki/Entity%E2%80%93attribute%E2%80%93value_model.
Here is a gem for it:
https://github.com/iostat/eav_hashes
Also document-oriented database like MongoDB would be an option, if you ever consider changing database. It is schemaless, so you can have different attributes for different instance.
I'm not aware of any out of the box options available, but you might be better off rolling your own on something like this anyway. It will allow you more flexibility, and shouldn't be terrible to implement. In terms of models, I'd probably go with a single-table inheritance table for the fields, probably using a jsonb column for customization options (assuming postgres):
create_table :fields do |t|
t.string :type, null: false # TextField, ListField, etc.
t.jsonb :config, default: {}, null: false
t.belongs_to :contact
end
You can then subclass as necessary for different use-cases:
class Field < ApplicationRecord
belongs_to :contact
end
class TextField < Field
def required=(required)
config[:required] = required
end
end
class CheckboxField < Field
def default_checked=(default_checked)
config[:default_checked] = default_checked
end
end
You can look into something like jsonb_accessor to make for a cleaner interface to the jsonb column.
Likewise, single-table inheritance looks like it may also make sense for the contacts as well, not sure what the base table should be, but maybe something like:
create_table :contacts do |t|
t.string :type, null: false # Contact, Lead, Client
end
class Contact < ApplicationRecord
end
class Lead < Contact
end
Here are some examples I found helpful for custom fields:
http://railscasts.com/episodes/403-dynamic-forms?view=asciicast
And:
https://github.com/lab2023/postgresql_jsonb_ransack_rails_5
https://gist.github.com/ismailakbudak/2ca1feac945999ec3e7d9cf0a373497a

index: true vs foreign_key: true (Rails)

Following a guide, I ran the following command:
rails g migration CreateSnippetsUsers snippet:belongs_to user:belongs_to
This created the following migration:
class CreateSnippetsUsers < ActiveRecord::Migration[5.0]
def change
create_table :snippets_users do |t|
t.belongs_to :snippet, foreign_key: true
t.belongs_to :user, foreign_key: true
end
end
end
In the past I've seen the same thing, but with index: true instead of foreign_key: true. What's the difference between the two?
Indexes, foreign keys and foreign keys constraints are strictly related concepts in databases that are often confused or misunderstood.
REFERENCES
When you declare a reference, you're simply saying to include a column whose values should match those of another table (and in Rails you also get some useful methods to navigate through the associated models). In the example:
create_table :appointments do |t|
t.references :student
end
the appointments table will have a column named student_id whose values should be in the pool of students' id values.
INDEXES
Since when you add a reference you will probably use that column often, you may (and probably should!) also tell you database to boost the look up speed using the reference column. You can do this with the option index: true (which by the way is a default option in the reference method since Rails 5). Indexes have few drawbacks, the main being a larger memory consumption.
FOREIGN KEY CONSTRAINTS
From what said so far, reference column and foreign column are synonyms. But do you remember when I said that a reference column's values should match those of another table? If you simply declare a reference, it's your responsibility to ensure that a matching row on the referenced table exists, or someone will end up doing nonsense actions like creating appointments for non-existing students. This is an example of database integrity, and fortunately there are some mechanisms that will grant a stronger level of integrity. These mechanisms are called ' database constraints'. What the option foreign_key: true does is exactly to add this kind of constraint on the reference column, to reject any entry whose foreign key values are not in the referenced table.
Database integrity is a complex task, growing in difficulty with the database's complexity. You probably should add also other kind of constraints, like using they keywords dependent: :destroy in your class to ensure that when you delete a student, all of its existing appointments are also destroyed.
As usual, here's a RTFM link: https://guides.rubyonrails.org/association_basics.html
Index improve speed of data retrieval operations on database tables. When we write index: true to any column, it adds a database index to this column. For example I was creating a table:
create_table :appointments do |t|
t.references :student, index: true
end
It will create student_id column in appointments table.
A foreign key have different use case, it is a relationship between tables. It allow us to declare an index in one table that is related to an index in another table and also some constraints are placed.The database enforces the rules of this relationship to maintain referential integrity. For example we have two table profiles and educations, and a profile may have many educations.
create_table :educations do |t|
t.belongs_to :profile, index: true, foreign_key: true
end
Now we have profile_id column in educations table which is foreign key of profiles table. It prevents a record from being entered into the educations table unless it contains a profile_id value that exists in the profiles table. So referential integrity will be maintained.

Why is my foreign key association not yielding results?

I have two models in my rails app, Appointment and Policy. My Appointment model has_many :policies, class_name: "Policy", foreign_key: 'writing_code' and my Policy model belongs_to :appointment. The writing_code column in each table is a String.
While it would appear that the association has been made (my app runs), #appointment.policies yields no results. Can someone shed some light on where I've gone wrong here?
Also, to preemptively answer the obvious question, I cannot simply use appointment_id because I will be uploading policy data that has an appointment's (or user's) "writing code" associated to each record. The data will not include the appointment id since it comes from a separate, third party system.
Thanks in advance for all your help!
EDIT:
schema:
create_table "policies", :force => true do |t|
t.integer "product_id"
t.decimal "premium"
t.string "writing_code"
t.datetime "created_at", :null => false t.datetime "updated_at", :null => false end`
I think here is your problem:
You have:
class Appointment
has_many :policies, class_name: "Policy", foreign_key: 'writing_code'
end
Here is what the guides says
"By convention, Rails assumes that the column used to hold the foreign key on this model is the name of the association with the suffix _id added."
Here is an example from the guides:
class Order < ActiveRecord::Base
belongs_to :customer, class_name: "Patron",
foreign_key: "patron_id"
end
In your case, your class name is Policy but your foreign_key name is writing_code. which is not conventional.
Rails is very opinionated. Stay within the way rails likes to work and life gets much easier.
I Agree completely with #WaliAli said also.
"#appointment.policies yields no results"
For appointment to have many policies, each policy needs to be linked with an appointment, in the Model AND in the the table schema.
What this means is that policies should have a field 'appointment_id' which is an integer.
Add an appointment_id field to the policies table.
$ rails generate migration AddAppointmentIdToPolicies appointment_id:integer
$ rake db:migrate
Then you do the following:
class Policy
has_many :appointments # this lets you do #policy.appointments
end
class Appointment
belongs_to :policy # this lets you do #appointment.policy
end
For 99% of rails apps doing it another way, whereby has_many & belongs_to includes more optional paramaters is a code smell.
[Update 1:]
"I have policy data that will be uploaded from a legacy system via .csv
file. This data will have no knowledge of the appointment_id, which is
why I need to use the writing_code as the foreign key value, as it's
the only common data that can associate the two models."
I would suggest resolving this as part of your CSV importer.
As you import each record, do a find for an appointment record that has a matching 'writing code' and then save the record to include the appointment id.
Something like this:
# Inside your CSV importer script
csv_rows.each do |row|
policy = Policy.new
policy.appointment_id = Appointment.find(writing_code: row.writing_code).id
# more code here..
policy.save
end

Resources