Rails Model - custom primary key with ID as a custom column - ruby-on-rails

I struggle with modeling my table to Rails model with custom primary key and id as a regular (not null) column.
The problem is my table has:
auto_id primary key column with AUTO_INCREMENT
id as custom column with NOT NULL constraint on it (it should be set by the application side before saving)
class InitialMigration < ActiveRecord::Migration[6.1]
def change
create_table :cars, id: false, force: true do |t|
t.primary_key :auto_id
t.string :id, null: false
end
end
end
I want to:
Make the primary_key being auto generated by database.
Set the id value directly in the application before saving the model.
But when I try to save a Car instance to database I have a problem with setting id from the app.
class Car < ActiveRecord::Base
self.primary_key = "auto_id"
before_create do
binding.pry
end
end
Even in the binding.pry line, when I call self.id = '1234' it's being reassigned to auto_id field, instead of id.
Thus id columns always remain NULL which leads to a DB error Field 'id' doesn't have a default value.
[2] pry(#<Car>)> self.id = '9'
=> "9"
[3] pry(#<Car>)> self
=> #<Car:0x00007fcdb34ebe60
auto_id: 9,
id: nil>
PS. It's Rails 6.

I'd avoid using id this way by renaming it to something else. In rails id is whatever the #primary_key is set to. There is a whole module dedicated to it, that defines id attribute methods:
https://api.rubyonrails.org/classes/ActiveRecord/AttributeMethods/PrimaryKey.html
These methods can be overridden, but shouldn't:
class Car < ApplicationRecord
self.primary_key = "auto_id"
def id= value
_write_attribute("id", value)
end
def id
_read_attribute("id")
end
end
"id" is hardcoded in write_attribute to use primary_key, so we can't use write_attribute to set id attribute itself.
Also, id methods are used in other places. Overriding them is a bad idea.
>> Car.create(id: "99")
=> #<Car:0x00007f723e801788 auto_id: nil, id: "99">
# ^
# NOTE: Even though car has been saved, `auto_id` is still `nil`
# but only in `car` object. Because we have overridden
# `id`, primary key is read from `id` column and then `id`
# attribute is set in `id=`. `auto_id` is bypassed.
>> Car.last
=> #<Car:0x00007f774d9e8dd0 auto_id: 1, id: "99">
# NOTE: Other methods are broken whenever `id` is used
>> Car.last.to_key
=> ["99"]
>> Car.last.to_gid
=> #<GlobalID:0x00007f774f4def68 #uri=#<URI::GID gid://stackoverflow/Car/99>>
A better way is to not touch id methods:
class Car < ApplicationRecord
self.primary_key = "auto_id"
def id_attribute= value
_write_attribute("id", value)
end
def id_attribute
_read_attribute("id")
end
end
>> car = Car.create(id_attribute: "99")
=> #<Car:0x00007fb1e44d9458 auto_id: 2, id: "99">
>> car.id_attribute
=> "99"
>> car.id
=> 2
>> car.auto_id
=> 2

Related

Setting up a polymorphic association where associated tables have different primary key types (UUID and Integer)

I have 5 tables which are all likeable by a user, and a likes table which records this.
The problem is one of the 5 tables uses uuid and not integer for their primary key. So I can't have a typical likeable_id & likeable_type
Is it possible to set up a polymorphic association that handles different primary key types?
Thanks in advance
I don't have your tables name, but here is an example for a join table
class CreateJoins < ActiveRecord::Migration
def change
create_table :joins do |t|
t.belongs_to :uuid_id_model, type: :uuid # you specify the type
t.belongs_to :integer_id_model # you don't need anything
t.index %i[uuid_id_model integer_id_model], unique: true
t.timestamps
end
end
end
I other words, specify on the columns that they are of type uuid
If you need to change a column type of your join tables, you can do like so :
class ChangeColumnType < ActiveRecord::Migration
def change
change_column :some_join_table, :some_join_column, :uuid
end
end
The solution I ended up with is adding a likeable_id_string which stores any uuid primary key. I then override the getters and setters to do the following:
# Overriding getters and setters to handle multiple primary key types on
# this polymorphic association ( String and Integer )
# Setters
def likeable=(value)
if !value.nil? && value.id.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value.id
self[:likeable_type] = value.class.name
else
self[:likeable_id_string] = nil
super(value)
end
end
# in case likeable_type and likeable_id are updated seperately. I believe
# this is done in FE (rather than passing an object to .likeable)
def likeable_id=(value)
if !value.nil? && value.is_a?(String)
self[:likeable_id] = nil
self[:likeable_id_string] = value
else
self[:likeable_id_string] = nil
super(value)
end
end
# Getters
def likeable
if self.likeable_id
return super
elsif !self.likeable_type.nil?
return self.likeable_type.constantize.find(likeable_id_string)
else
return nil
end
end
You would think that the likeable= setter isn't needed once I've overridden the likeable_id= .. but seems like it is. If anyone knows why I need both please let me know as it would seem logical that likeable delegates to likeable_id and likeable_type
I feel like there's a use case for this to be a Gem. Something I might do soon. Please get in touch if you would like to see this as a gem (or if you think that's a terrible idea)

How to store enum as string to database in rails

How do I create a migration in ruby where the default is a string rather than an Integer, I want to store enum into the database, but I do not want to store it as Integer, because then it does not make sense to another application that wants to use the same table. How do I do default: "female" instead of default:0
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, :integer, default: 0
end
end
class User < ActiveRecord::Base
enum sex: [:female, :male]
has_secure_password
end
I
Reading the enum documentation, you can see Rails use the value index of the Array explained as:
Note that when an Array is used, the implicit mapping from the values to database integers is derived from the order the values appear in the array.
But it is also stated that you can use a Hash:
it's also possible to explicitly map the relation between attribute and database integer with a Hash.
With the example:
class Conversation < ActiveRecord::Base
enum status: { active: 0, archived: 1 }
end
So I tested using Rails 4.2.4 and sqlite3 and created an User class with a string type for sex type and a Hash in the enum with string values(I am using fem and mal values to differ from female and male):
Migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :sex, default: 'fem'
end
end
end
Model:
class User < ActiveRecord::Base
enum sex: { female: 'fem', male: 'mal' }
end
And in console:
u = User.new
#=> #<User id: nil, sex: "fem">
u.male?
#=> false
u.female?
#=> true
u.sex
#=> "female"
u[:sex]
#=> "fem"
u.male!
# INSERT transaction...
u.sex
#=> "male"
u[:sex]
#=> "mal"
I normally do the following:
# in the migration in db/migrate/…
def self.up
add_column :works, :status, :string, null: false, default: 'offering'
end
# in app/models/work.rb
class Work < ApplicationRecord
ALL_STATES = %w[canceled offering running payment rating done].freeze
enum status: ALL_STATES.zip(ALL_STATES).to_h
end
By using a hash as argument for enum (see docs) this stores strings in the database. At the same time this still allows you to use all the cool Rails helper methods:
w = Work.new
#=> #<Work id: nil, status: "offering">
w.rating?
#=> false
w.offering?
#=> true
w.status
#=> "offering"
w[:status]
#=> "offering"
w.done!
# INSERT transaction...
w.status
#=> "done"
w[:status]
#=> "done"
Update for one-liner:
I overlooked completely we've got index_by since Rails 1.2.6. This makes the solution a one-liner even:
enum status: %w[canceled offering running payment rating done].index_by(&:to_sym)
Alternatively we've got index_with since Rails 6.0.0:
enum status: %i[canceled offering running payment rating done].index_with(&:to_s)
enum in Rails and ENUM type in MySQL are 2 different things.
enum in Rails is just a wrapper around your integer column so it's easier for you to use strings in queries, rather than integers. But on database level it's all converted to integers (automatically by Rails), since that's the type of the column.
ENUM type in MySQL is vendor-specific column type (for example, SQLite doesn't support it, but PostgreSQL does). In MySQL :
An ENUM is a string object with a value chosen from a list of permitted values that are enumerated explicitly in the column specification at table creation time.
CREATE TABLE shirts (
name VARCHAR(40),
size ENUM('x-small', 'small', 'medium', 'large', 'x-large')
);
INSERT INTO shirts (name, size) VALUES ('dress shirt','large'), ('t-shirt','medium'),
('polo shirt','small');
SELECT name, size FROM shirts WHERE size = 'medium';
+---------+--------+
| name | size |
+---------+--------+
| t-shirt | medium |
+---------+--------+
For the migration, you need to do this:
class AddSexToUsers < ActiveRecord::Migration
def change
add_column :users, :sex, "ENUM('female', 'male') DEFAULT 'female'"
end
end
Take a look at this Gist, Rails doesn't provide it out of the box so you have to use a concern:
https://gist.github.com/mani47/86096220ccd06fe46f0c09306e9d382d
There's steps to add enum as string to model Company
bin/rails g migration AddStatusToCompanies status
class AddStatusToCompanies < ActiveRecord::Migration[7.0]
def change
add_column :companies, :status, :string, null: false, default: 'claimed'
add_index :companies, :status
end
end
bin/rails db:migrate
Values are strings (symbols not working)
add Default
add Prefix
enum status: {
claimed: 'claimed',
unverified: 'unverified',
verified: 'verified',
}, default: 'claimed'
Add validation (or will raise sql exception)
validates :status, inclusion: { in: statuses.keys }, allow_nil: true
To my knowledge it is not possible with standard Rails enum. Look at https://github.com/lwe/simple_enum, it is more functionally rich, and also allows storing of enum values as strings to DB (column type string, i.e. varchar in terms of DB).

How to make active record initialize with params in Rails 4?

I am using Ruby on Rails 4 and I have this User model:
require 'uuid'
UUID.state_file = false
UUID.generator.next_sequence
class User < ActiveRecord::Base
attr_accessor :email, :password
has_many :entries
after_initialize do |user|
user.entry_hash = UUID.new.generate
end
end
Which is based on the following DB migration:
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string "email"
t.string "password"
t.string "entry_hash"
t.timestamps
end
end
end
I want to automatically generate a uuid as a hash associated with that User.
Using the rails console to create a new User, I do the following:
1) I use create a User with an email and password:
irb(main):001:0> u = User.create(:email=>'abc#gmail.com', :password=>'myPass')
(0.1ms) BEGIN
SQL (0.6ms) INSERT INTO `users` (`created_at`, `entry_hash`, `updated_at`) VALUES ('2013-12-07 22:32:28', '60744ec0-41bd-0131-fde8-3c07542e5dcb', '2013-12-07 22:32:28')
(3.7ms) COMMIT
=> #<User id: 2, email: nil, password: nil, entry_hash: "60744ec0-41bd-0131-fde8-3c07542e5dcb", created_at: "2013-12-07 22:32:28", updated_at: "2013-12-07 22:32:28">
2) But this is weird. If you look at the SQL, it doesn't insert anything but the entry_hash, and the output object shows email and password as nil. However, when I try to access those properties, I get the ones I put.
irb(main):002:0> u.email
=> "abc#gmail.com"
irb(main):003:0> u.id
=> 2
irb(main):004:0> u.password
=> "myPass"
irb(main):005:0> u.entry_hash
=> "60744ec0-41bd-0131-fde8-3c07542e5dcb"
I am very new to Ruby on Rails and I know some magical stuff goes on in the background, but can someone enlighten me as to whats going on here? I just want to create an object with parameters.
Cheers
UPDATE:
I fixed the problem I was having by removing the attr_accessor line. Anyone know why that made it work?
attr_accessor
Can be used for values you don't want to store in the database directly and that will only exist for the life of the object (e.g. passwords).
Is used when you do not have a column in your database, but still want to show a field in your forms. This field is a “virtual attribute” in a Rails model.
The method create creates the row in the database and also returns the ruby object. That is why accessing the fields through the variable u worked, it is alive while the console is open. However, nothing made it to the database.

Rails 3.0.5 belongs_to association not updating primary key in declaring class

I am trying to do a basic belongs_to/has_many association but running into a problem. It seems that the declaring class's foreign key column is not being updated. Here are my models:
#
# Table name: clients
#
# id :integer not null, primary key
# name :string(255)
# created_at :datetime
# updated_at :datetime
#
class Client < ActiveRecord::Base
has_many :units
...
attr_accessible :name
end
#
# Table name: units
#
# id :integer not null, primary key
# client_id :integer
# name :string(255)
# created_at :datetime
# updated_at :datetime
#
class Unit < ActiveRecord::Base
belongs_to :client
...
attr_accessible :name
end
When I open rails console I do the following:
#This works as it should
c1 = Client.create(:name => 'Urban Coding')
u1 = c1.units.create(:name => 'Birmingham Branch')
The above gives me the correct results. I have a client and a unit. The unit has the client_id foreign key field populated correctly.
#This does not work.
c1 = Client.create(:name => 'Urban Coding')
u1 = Unit.create(:name => 'Birmingham Branch')
u1.client = c1
I feel the above should have the same effect. However this is not the case. I have a unit and a client but the units client_id column is not populated. Not sure exactly what I am doing wrong here. Help is appreciated. Let me know if you need more information.
You're simply not saving u1, hence no change to the database.
If you want it assigned and saved in a single operation, use update_attribute
u1.update_attribute(:client, c1)
Yep, I think if you save it the ID will get set.
The first syntax is much better. If you don't want to do the save action right away, which create does for you, then use build:
c1 = Client.create(:name => 'Urban Coding')
u1 = c1.units.build(:name => 'Birmingham Branch')
# do stuff with u1
u1.save
This works:
c1 = Client.create(:name => 'Urban Coding')
u1 = Unit.create(:name => 'Birmingham Branch')
u1.client_id = c1.id
u1.save
c1.save
but the other way is the better way to create it.

'column "id" does not exist' error while trying to associate a Role with a User using rails-authorization

I'm exploring Rails for the first time and trying to add some fairly straightforward role-based security to my test app. Some Googling seemed to indicate rails-authorization is the way to go for this. I followed the README and everything seemed to be going well, but now I'm trying to associate a User with a Role and it fails. Here's the snippet from my script/console session:
>> u = User.find(:first)
=> #<User id: 1, login: "cwhit", name: "", email: "cwhitfield#unica.com", crypted_password: "7ac064547fb8992e8e53e936df31657a40f9c5af", salt: "56671492059f8e40eb3d509940944aaba31ebc72", created_at: "2009-03-26 18:06:04", updated_at: "2009-03-26 18:06:04", remember_token: nil, remember_token_expires_at: nil>
>> r = Role.find(:first)
=> #<Role id: 1, name: "ProjectManager", authorizable_type: nil, authorizable_id: nil, created_at: "2009-03-27 11:02:35", updated_at: "2009-03-27 11:02:35">
>> u.has_role r
ActiveRecord::StatementInvalid: PGError: ERROR: column "id" does not exist
LINE 1: ....546623', '2009-03-27 11:42:16.546623', 5, 1) RETURNING "id"
^
: INSERT INTO "roles_users" ("created_at", "updated_at", "role_id", "user_id") VALUES('2009-03-27 11:42:16.546623', '2009-03-27 11:42:16.546623', 5, 1) RETURNING "id"
Am I just doing something silly, or is this a known issue? I found essentially the same error in a question in the Google Group for the rails-authorization plugin, but there was no solution provided.
Here's my basic config:
OS X
Rails 2.3.2
PostgresQL
Plugins:
restful-authentication
rails-authorization
Ran into the same problem with models where I was using a primary key other than 'id'.
Turns out I had forgotten to declare it in the model class definition using "set_primary_key". Perhaps you need to do something like this:
class CulpritClass < ActiveRecord::Base
set_primary_key :key_field_you_want
end
Try installing the composite_primary_keys gem (link) and add the following to the RolesUser controller:
set_primary_keys :user_id, :role_id
Since there is no id field for that table (and there doesn't need to be), the primary key becomes (user_id, role_id) which you can add to the database with
ALTER TABLE roles_users ADD PRIMARY KEY (user_id, role_id);
This solved the problem in my case, which seems to be Postgres-specific.
I simply used two primary key declarations
set_primary_key :user_id
set_primary_key :role_id
This worked for my purposes but keep in mind that it returns (in the sql at least) the second primary key that you define. In this case it would be the role_id of the object.
I had the same problem but on destroy (of roles). I tried the composite_primary_keys gem but found that it broke other things in Rails 2.3.4. Specifically, it caused Rails :belongs_to association to generate the wrong attributed id in this situation:
belongs_to :inviter, :class_name => 'User'
It was generating user_id as the attribute name instead of inviter_id.
The ultimate solution was to add an id column to roles_users. Since I already had a roles_users table with no surrogate key I had to do this lil migration:
class AddIdToRolesUsers < ActiveRecord::Migration
def self.up
# create new table the way it should be (_with_ a traditional Rails id column)
create_table :x_roles_users do |t|
t.belongs_to :role
t.belongs_to :user
t.timestamps
end
execute 'insert into x_roles_users (user_id, role_id, created_at, updated_at) select * from roles_users'
drop_table :roles_users
rename_table :x_roles_users, :roles_users
end
def self.down
remove_column :roles_user, :id
end
end

Resources