Custom non auto-incrementing ActiveRecord "id" column possible? - ruby-on-rails

I'd like to be able to use a custom id (instead of the auto-incrementing default ones) for a Rails model. Basically, all the ids will be iTunes store ids, which are just long integers. Is it possible to turn off the default auto-incrementing ids and require that one be set? These ids will also be used as foreign keys in other models.

Something like this:
create_table :blah, {:id => false} do |t|
t.int :my_custom_int_id
end
execute "ALTER TABLE blah ADD PRIMARY KEY (my_custom_int_id);"

You can manually set the id before you save the model.
a = Model.new
a.id = 8888 #from itunes
a.save
However, you should consider a separate field called itunes_id instead of this approach.

Related

How to enforce uniqueness of an entire row?

I've seen other SO questions like - How do you validate uniqueness of a pair of ids in Ruby on Rails? - which describes adding a scoped parameter to enforce uniqueness of a key pair, i.e. (from the answer)
validates_uniqueness_of :user_id, :scope => [:question_id]
My question is how do you do this kind of validation for an entire row of data?
In my case, I have five columns and the data should only be rejected if all five are the same. This data is not user entered and the table is essentially a join table (no id or timestamps).
My current thought is to search for a record with all of the column values and only create if the query returns nil but this seems like a bad work around. Is there an easier 'rails way' to do this?
You'll need to create a custom validator (http://guides.rubyonrails.org/active_record_validations.html#performing-custom-validations):
class TotallyUniqueValidator < ActiveModel::Validator
def validate(record)
if record.attributes_for_uniqueness.values.uniq.size == 1
record.errors[:base] << 'All fields must be unique!'
end
end
end
class User
validates_with TotallyUniqueValidator
def attributes_for_uniqueness
attributes.except :created_at, :updated_at, :id
end
end
The important line here is:
if record.attributes_for_uniqueness.values.uniq.size == 1
This will grab a hash of all the attributes you want to check for uniqueness (in this case everything except id and timestamps) and converts it to an array of just the values, then calls uniq on it which returns only uniq values and if the size is 1 then they were all the same value.
Update based on your comment that your table doesn't have an id or timestamps:
You can then simply do:
if record.attributes.except(:id).values.uniq.size == 1
...because I'm pretty sure it still has an id unless you're sure it doesn't then just remove the except part.
You can add a unique index to the table in a migration:
add_index :widgets, [:column1, :column2, :column3, :column4, :column5], unique: true
The resulting index will require that each combination of the 5 columns must be unique.

How to stop rails from calling next_seq for a join table without id?

I'm working with Rails 3.2.13 and fb-adapter (firebird, but I don't believe that to be relevant here).
I have a join model that other two model use with "has_many A, through: X".
I've created the table without an "id" (acording to rails guides, and common sense because the pk is a combination of the two) like this:
create table "X", id: false do |t|
t.integer "a_id", null: false
t.integer "b_id", null: false
end
Then when I've tried to create a model and I got "ActiveRecord::UnknownPrimaryKey"; ok I had googled and I've found Why do I get an Unknown Primary Key exception for a join table in Rails 4?
So in the join Model I've set:
class X < ActiveRecord::Base
self.primary_key = [:a_id, :b_id]
...
end
Now I can create a new model, but when I try to save it I run into "Fb::Error: Invalid token
generator X_SEQ is not defined", so basically rails is trying to get the next value for the id, but because the column wasn't created the generator or sequence for it neither.
Here is the relevant code in activerecord/relation.rb:
def insert(values)
primary_key_value = nil
if primary_key && Hash === values
primary_key_value = values[values.keys.find { |k|
k.name == primary_key
}]
if !primary_key_value && connection.prefetch_primary_key?(klass.table_name)
### exception get's raised here in next_sequence_value ###
primary_key_value = connection.next_sequence_value(klass.sequence_name)
values[klass.arel_table[klass.primary_key]] = primary_key_value
end
end
...
end
I don't know if this happens also with the sqlite3 / mysql / postegres adapters, but I believe that if the PK is compound rails should not try to get a "next value for it".. or there must be some way to prevent it from trying to get the next value.. Any ideas how to solve this (without patching activerecord)? Thanks
For pure many-to-many relationships, use has_and_belongs_to_many relationships in your models. No model is needed for your join table. Use has_many :through relationships where additional data (such as a sort order or timestamps) is needed in your join table. In such a case, a full-fledged model would be appropriate and models require a primary key.

Set Primary Key value 0 and auto increment on Migration PostgreSQL

I have a model with 2 fields => :name and :age
I need to do a Migration that add a column :position which needs auto increment and start with 0 (zero).
I tried these way:
class AddPosition < ActiveRecord::Migration
def up
add_column :clientes, :position, :integer, :default => 0, :null => false
execute "ALTER TABLE clientes ADD PRIMARY KEY (position);"
end
But it doesn't work because it not auto increment. If I try to use primary key as type:
class AddPosition < ActiveRecord::Migration
def up
add_column :clientes, :position, :primary_key, :default => 0, :null => false
end
end
rake db:migrate don't run because multiple values.
Anyone could explain a way to have zeros and autoincrement on Primary Key w/ Rails 3.2?
Here's how you can set up auto increment column in PostgreSQL:
# in migration:
def up
execute <<-SQL
CREATE SEQUENCE clients_position_seq START WITH 0 MINVALUE 0;
ALTER TABLE clients ADD COLUMN position INTEGER NOT NULL DEFAULT nextval('clients_position_seq');
SQL
end
But unfortunately it may not be what you need. The above would work if you'd insert values into clients table with SQL like this: INSERT INTO clients(name, age) VALUES('Joe', 21), and rails doesn't work that way.
The first problem is that rails expects primary key to be called id. And while you can override this convention, it would cause more problems than it would solve. If you want to bind position to primary key value, better option is to add virtual attribute to your model, a method like this:
def position
id.nil? ? nil : id - 1
end
But let's suppose you already have conventional primary key id and want to add position field so that you can reorder clients after they have been created, without touching their ids (which is always a good idea). Here comes the second problem: rails won't recognize and respect DEFAULT nextval('clients_position_seq'), i.e. it won't pull values from PG backed sequence and would try to put NULL in position by default.
I'd like to suggest looking at acts_as_list gem as better option. This would make DB sequence manipulations unnecessary. Unfortunately it uses 1 as initial value for position but that can be cured by setting custom name for list position field and defining method as I showed above.

How to add sequences to a migration and use them in a model?

I want to have a "Customer" Model with a normal primary key and another column to store a custom "Customer Number". In addition, I want the db to handle default Customer Numbers. I think, defining a sequence is the best way to do that. I use PostgreSQL. Have a look at my migration:
class CreateAccountsCustomers < ActiveRecord::Migration
def up
say "Creating sequenze for customer number starting at 1002"
execute 'CREATE SEQUENCE customer_no_seq START 1002;'
create_table :accounts_customers do |t|
t.string :type
t.integer :customer_no, :unique => true
t.integer :salutation, :limit => 1
t.string :cp_name_1
t.string :cp_name_2
t.string :cp_name_3
t.string :cp_name_4
t.string :name_first, :limit => 55
t.string :name_last, :limit => 55
t.timestamps
end
say "Adding NEXTVAL('customer_no_seq') to column cust_id"
execute "ALTER TABLE accounts_customers ALTER COLUMN customer_no SET DEFAULT NEXTVAL('customer_no_seq');"
end
def down
drop_table :accounts_customers
execute 'DROP SEQUENCE IF EXISTS customer_no_seq;'
end
end
If you know a better "rails-like" approach to add sequences, would be awesome to let me know.
Now, if I do something like
cust = Accounts::Customer.new
cust.save
the field customer_no is not pre filled with the next value of the sequence (should be 1002).
Do you know a good way to integrate sequences? Or is there a good plugin?
Cheers to all answers!
I have no suggestions for a more 'rails way' of handling custom sequences, but I can tell you why the customer_no field appears not to be being populated after a save.
When ActiveRecord saves a new record, the SQL statement will only return the ID of the new record, not all of its fields, you can see where this happens in the current rails source here https://github.com/rails/rails/blob/cf013a62686b5156336d57d57cb12e9e17b5d462/activerecord/lib/active_record/persistence.rb#L313
In order to see the value you will need to reload the object...
cust = Accounts::Customer.new
cust.save
cust.reload
If you always want to do this, consider adding an after_create hook in to your model class...
class Accounts::Customer < ActiveRecord::Base
after_create :reload
end
I believe that roboles answer is not correct.
I tried to implement this on my application (exactly the same env: RoR+PostgreSQL), and I found out that when save is issued on RoR with the object having empty attributes, it tries to perform an INSERT on the database mentioning that all VALUES shall be set to NULL. The problem is the way PostgreSQL handles NULLs: in this case, the new row will be created but with all values empty, i.e. the DEFAULT will be ignored. If save only wrote on the INSERT statement attributes filled on RoR, this would work fine.
In other words, and focusing only on the type and customer_no attribute mentioned above, this is the way PostgreSQL behaves:
SITUATION 1:
INSERT INTO accounts_customers (type, customer_no) VALUES (NULL, NULL);
(this is how Rails' save works)
Result: a new row with empty type and empty customer_no
SITUATION 2:
INSERT INTO accounts_customers (type) VALUES (NULL);
Result: a new row with empty type and customer_no filled with the sequence's NEXTVAL
I have a thread going on about this, check it out at:
Ruby on Rails+PostgreSQL: usage of custom sequences
I faced a similar problem, but I also put :null => false on the field hopping that it will be auto-populated with nextval.
Well, in my case AR was still trying to insert NULL if no attribute was supplied in the request, and this resulted in an exception for not-null constraint violation.
Here's my workaround. I just deleted this attribute key from #attributes and #changed_attributes and in this case postgres correctly put the expected sequence nextval.
I've put this in the model:
before_save do
if (#attributes["customer_no"].nil? || #attributes["customer_no"].to_i == 0)
#attributes.delete("customer_no")
#changed_attributes.delete("customer_no")
end
end
Rails 3.2 / Postgres 9.1
If you're using PostgreSQL, check out the gem I wrote, pg_sequencer:
https://github.com/code42/pg_sequencer
It provides a DSL for creating, dropping and altering sequences in ActiveRecord migrations.

Getting types of the attributes in an ActiveRecord object

I would like to know if it is possible to get the types (as known by AR - eg in the migration script and database) programmatically (I know the data exists in there somewhere).
For example, I can deal with all the attribute names:
ar.attribute_names.each { |name| puts name }
.attributes just returns a mapping of the names to their current values (eg no type info if the field isn't set).
Some places I have seen it with the type information:
in script/console, type the name of an AR entity:
>> Driver
=> Driver(id: integer, name: string, created_at: datetime, updated_at: datetime)
So clearly it knows the types. Also, there is .column_for_attribute, which takes an attr name and returns a column object - which has the type buried in the underlying database column object, but it doesn't appear to be a clean way to get it.
I would also be interested in if there is a way that is friendly for the new "ActiveModel" that is coming (rails3) and is decoupled from database specifics (but perhaps type info will not be part of it, I can't seem to find out if it is).
Thanks.
In Rails 3, for your model "Driver", you want Driver.columns_hash.
Driver.columns_hash["name"].type #returns :string
If you want to iterate through them, you'd do something like this:
Driver.columns_hash.each {|k,v| puts "#{k} => #{v.type}"}
which will output the following:
id => integer
name => string
created_at => datetime
updated_at => datetime
In Rails 5, you can do this independently of the Database. That's important if you use the new Attributes API to define (additional) attributes.
Getting all attributes from a model class:
pry> User.attribute_names
=> ["id",
"firstname",
"lastname",
"created_at",
"updated_at",
"email",...
Getting the type:
pry> User.type_for_attribute('email')
=> #<ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MysqlString:0x007ffbab107698
#limit=255,
#precision=nil,
#scale=nil>
That's sometimes more information than needed. There's a convenience function that maps all these types down to a core set (:integer, :string etc.)
> User.type_for_attribute('email').type
=> :string
You can also get all that data in one call with attribute_types which returns a 'name': type hash.
You can access the types of the columns by doing this:
#script/console
Driver.columns.each {|c| puts c.type}
If you want to get a list of all column types in a particular Model, you could do:
Driver.columns.map(&:type) #gets them all
Driver.columns.map(&:type).uniq #gets the unique ones
In rails 5 this will give you a list of all field names along with their data type:
Model_Name.attribute_names.each do |k| puts "#{k} = #{Model_Name.type_for_attribute(k).type}" end
Rails 5+ (works with virtual attributes as well):
Model.attribute_types['some_attribute'].type
This snippet will give you all the attributes of a model with the associated database data types in a hash. Just replace Post with your Active Record Model.
Post.attribute_names.map {|n| [n.to_sym,Post.type_for_attribute(n).type]}.to_h
Will return a hash like this.
=> {:id=>:integer, :title=>:string, :body=>:text, :created_at=>:datetime, :updated_at=>:datetime, :topic_id=>:integer, :user_id=>:integer}
Assuming Foobar is your Active Record model. You can also do:
attributes = Foobar.attribute_names.each_with_object({}) do |attribute_name, hash|
hash[attribute_name.to_sym] = Foobar.type_for_attribute(attribute_name).type
end
Works on Rails 4 too
In Rails 4 You would use Model.column_types.

Resources