Rails relationship confusion - ruby-on-rails

I am trying to get some relationships in Rails set up and am having some confusion with how to use the ones I have configured.
My scenario is this:
I have a model called Coaster. I wish each Coaster to be able to have 0 or more versions. I wish to be able to find all versions of a Coaster from it's instance and also in reverse.
My models and relationships as they stand:
coaster.rb:
has_many :incarnations
has_many :coaster_versions,
through: :incarnations
incarnation.rb:
belongs_to :coaster
belongs_to :coaster_version,
class_name: "Coaster",
foreign_key: "coaster_id"
Database schema for Incarnations:
create_table "incarnations", force: :cascade do |t|
t.integer "coaster_id"
t.integer "is_version_of_id"
t.boolean "is_latest"
t.integer "version_order"
end
and my code that happens when importing Coasters from my CSV data file:
# Versions
# Now determine if this is a new version of existing coaster or not
if Coaster.where(order_ridden: row[:order_ridden]).count == 1
# Create Coaster Version that equals itself.
coaster.incarnations.create!({is_version_of_id: coaster.id, is_latest: true})
else
# Set original and/or previous incarnations of this coaster to not be latest
Coaster.where(order_ridden: row[:order_ridden]).each do |c|
c.incarnations.each do |i|
i.update({is_latest: false})
end
end
# Add new incarnation by finding original version
original_coaster = Coaster.unscoped.where(order_ridden: row[:order_ridden]).order(version_number: :asc).first
coaster.incarnations.create!({is_version_of_id: original_coaster.id, is_latest: true})
Now all my DB tables get filled in but I am unsure how to ensure everything is working how I want it to.
For example I have two coasters (A and B), B is a version of A. When I get A and ask for a count of it's coaster_versions, I only get 1 returned as a result? Surely I should get 2 or is that correct?
In the same line, if I get B and call coaster_versions I get 1 returned as well.
I just need to ensure I am getting back the correct results really.
Any comments would be highly appreciated as I have been working on this for ages now and not getting very far.
Just incase anyone is going to reply telling me to look at versioning gems. I went this route initially and it was great but the problem there is that in MY case a Coaster and a VERSION of a coaster are both as important as each other and I can't do Coaster.all to get ALL coasters whether they were versions or not. Other issues along the same line also cropped up.
Thanks

First of all, welcome to the wonderful world of history tracking! As you've found, it's not actually that easy to keep track of how your data changes in a relational database. And while there are definitely gems out there that can just track history for audit purposes (e.g. auditable), sounds like you want your history records to still be first-class citizens. So, let me first analyze the problems with your current approach, and then I'll propose a simpler solution that might make your life easier.
In no particular order, here are some pain points with your current system:
The is_latest column has to be maintained, and is at risk for going out of sync. You likely wouldn't see this in testing, but in production, at scale, it's a very valid risk.
Your incarnations table creates a one-master-version-with-many-child-versions structure, which is fine except that (similar to is_latest) the ordering of the versions is controlled by the version_order column which again needs to be maintained and is at risk of being incorrect. Your import script doesn't seem to set it, at the moment.
The incarnations relationship makes it difficult to tell that B is a version of A; you could fix this with some more relations, but that will also make your code more complex.
Complexity. It's hard to follow how history is tracked, and as you've found, it's hard to manage the details of inserting a new version (A and B should both agree that they have 2 versions, right? Since they're the same coaster?)
Now, I think your data model is still technically valid -- the issues you're seeing are, I think, problems with your script. However, with a simpler data model, your script could become much simpler and thus less prone to error. Here's how I'd do it, using just one table:
create_table "coasters", force: :cascade do |t|
t.string "name"
t.integer "original_version_id"
t.datetime "superseded_at"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
The original_version_id serves the same purpose as your incarnations table does, to link a version back to the original record. The superseded_at column is both usable as an is_latest check and a way to order the versions (though below, I just order by id for simplicity). With that structure, this is my Coaster class:
class Coaster < ActiveRecord::Base
belongs_to :original_version, class_name: "Coaster"
scope :latest, -> { where(superseded_at: nil) }
scope :original, -> { where('original_version_id = id') }
# Create a new record, linked properly in history.
def self.insert(attrs)
# Find the current latest version.
if previous_coaster = Coaster.latest.find_by(name: attrs[:name])
# At the same time, create the new version (linked back to the original version)
# and deprecate the current latest version. A transaction ensures either both
# happen, or neither do.
transaction do
create!(attrs.merge(original_version_id: previous_coaster.original_version_id))
previous_coaster.update_column(:superseded_at, Time.now)
end
else
# Create the first version. Set its original version id to itself, to simplify
# our logic.
transaction do
new_record = create!(attrs)
new_record.update_column(:original_version_id, new_record.id)
end
end
end
# Retrieve all records linked to the same original version. This will return the
# same result for any of the versions.
def versions
self.class.where(original_version_id: original_version_id)
end
# Return our version as an ordinal, e.g. 1 for the very first version.
def version
versions.where(['id <= ?', id]).count
end
end
This makes adding new records simple:
irb> 5.times { Coaster.insert(name: "Coaster A") }
irb> 4.times { Coaster.insert(name: "Coaster B") }
irb> Coaster.latest.find_by(name: "Coaster A").version
(2.2ms) SELECT COUNT(*) FROM "coasters" WHERE "coasters"."original_version_id" = $1 AND (id <= 11) [["original_version_id", 7]]
=> 5
irb> Coaster.original.find_by(name: "Coaster A").version
(2.3ms) SELECT COUNT(*) FROM "coasters" WHERE "coasters"."original_version_id" = $1 AND (id <= 7) [["original_version_id", 7]]
=> 1
Granted, it's still complex code that would be nice to have made simpler. My approach is certainly not the only one, nor necessarily the best. Hopefully you learned something, though!

Related

How to query on a method value?

Consider this table:
create_table "liquor_lots", force: :cascade do |t|
t.integer "recipe_id"
t.datetime "created_at", precision: 6, null: false
t.integer "counter"
end
And the resulting model
class LiquorLot < ApplicationRecord
def lotcode
"#{recipe_id}#{created_at.strftime("%y")}#{created_at.strftime("%W")}#{created_at.strftime("%u")}"
end
def pallet_lotcode
"#{lotcode}-#{counter}"
end
end
I'd like to do the equivalent of this in SQL:
Select distinct(lotcode) from liquor_lots
I've tried this and it understandably fails because lotcode is not a column on the liquor_lots table. But I've always been advised against adding columns to store data that is derived from data in other columns.
So how do I search for those values?
For context, my lotcode actually consists of many more values concatenated together, I just limited to three in the example for readability.
As far as I know, with basic ActiveRecord you cannot do that.
ActiveRecord would have to know too much about your ruby code.
You could implement a SQL query that concatenates the relevant values by hand (see comment to your question).
Or you can query all objects (or just the relevant values using pluck()) and then work on that with standard Ruby Array/Enumerable methods (in memory). If the application is not performance-critical, happens rarely, and you do not have thousands of the liquor_lots, that would be an okay productivity-tradeoff in my eyes.
Besides storing it in an own column, you could also extract the codes in separate table and make PalletLotcode an entity of its own. LiquorLots would than belong_to a single PalletLotcode which would have_many LiquorLots. But compared to the separate column this is a rather complex operation, but makes sense if other information is to be stored on the Lotcodes.
You can try something like:
LiquorLot.where("recipe_id = :rcp_id AND created_at >= :begin_of_day AND created_at <= :end_of_day", {begin_of_day: calculate_begin_of_day, end_of_day: calculate_end_of_date, rcp_id: id})
calculate_begin_of_day and calculate_end_of_date can be implemented using Date.comercial method and Date.beginning_of_day and Date.end_of_day

ThinkingSphinx: OR-condition on the SQL-backed indices?

I am trying to use ThinkingSphinx in my Rails 5 project. I read an instruction at http://freelancing-gods.com/thinking-sphinx/
I need to implement the OR logic on SQL-backed indices.
Here is my class:
class Message < ApplicationRecord
belongs_to :sender, class_name: 'User', :inverse_of => :messages
belongs_to :recipient, class_name: 'User', :inverse_of => :messages
end
and its indexer:
ThinkingSphinx::Index.define :message, :with => :active_record, :delta => true do
indexes text
indexes sender.email, :as => :sender_email, :sortable => true
indexes recipient.email, :as => :recipient_email, :sortable => true
has sender_id, created_at, updated_at
has recipient_id
end
schema.rb:
create_table "messages", force: :cascade do |t|
t.integer "sender_id"
t.integer "recipient_id"
t.text "text"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "read", default: false
t.boolean "spam", default: false
t.boolean "delta", default: true, null: false
t.index ["recipient_id"], name: "index_messages_on_recipient_id", using: :btree
t.index ["sender_id"], name: "index_messages_on_sender_id", using: :btree
end
So I need to search only within 2 indices at once - :sender_email and :recipient_email - but ignoring indexes text.
In pseudocode I need something like this:
Message.search 'manager1#example.com' :conditions => {:sender_email => 'client1#example.com' OR :receiver_email => 'client1#example.com'}
Which means: find all the messages between 'manager1#example.com' and 'client1#example.com' (each of them could be either a sender or a receiver) - ignoring the messages containing the text with words 'manager1#example.com' or 'client1#example.com'.
Unfortunately, the docs say:
The :conditions option must be a hash, with each key a field and each value a string.
In other words, I need a conditional index set (at run-time) - but simultaneously over 2 indices (not 1 as documented).
I mean that it is a bad idea to allow only hashes as a condition - and no strings (like ActiveRecord queries do allow http://guides.rubyonrails.org/active_record_querying.html#pure-string-conditions ).
PS I would say that the ThinkingSphinx documentation http://freelancing-gods.com/thinking-sphinx/ is pretty bad and needs to be totally rewritten from scratch. I read it all and did not understand anything. It has no examples (complete examples - only partial - thus totally unclear). I even don't understand what are fields and attributes and how do they differ. Associations, conditions, etc - all is unclear. Very bad. The gem itself looks pretty good - but its documentation is awful.
I'm sorry to hear you've not been able to find a solution that works for you in the documentation. The challenging thing with Sphinx is that it uses the SphinxQL syntax, which is very similar to SQL, but also quite different at times - and so people often expect SQL-like behaviour.
It's also part of the challenge of maintaining this gem - I'm not sure it's wise to mimic the ActiveRecord syntax too closely, otherwise that could make things more confusing.
The key thing to note here is that you can make use of Sphinx's extended query syntax for matches to get the behaviour you're after:
Message.search :conditions => {
:sender_email => "(client1#example.com | manager1#example.com)",
:receiver_email => "(client1#example.com | manager1#example.com)"
}
This will return anything where the sender is either of the two values, and the receiver is either of the two values. Of course, this will include any messages sent from client1 to client1, or manager1 to manager1, but I'd expect that's rare and maybe not that big a problem.
One caveat to note is that # and . aren't usually treated as searchable word characters, so you may need to add them to your charset_table.
Also, given you're actually performing exact matches on the entire values of database columns, this does feel like a query that's actually better served by some database indices on the columns and using SQL instead. Sphinx (and I'd say most/all other full-text search libraries) are best suited to matching words and phrases within larger text fields.
As for the documentation… I've put a lot of effort into trying to make them useful, though I realise there's still a lot of improvement that could take place. I do have a page that outlines how fields and attributes differ - if that's not clear, feedback is definitely welcome.
Keeping documentation up-to-date requires a lot of effort in small and new projects - and Thinking Sphinx is neither of these, being 10 years old in a few months. I'm proud at how it still works well, it still supports the latest versions of Rails, and it's still actively maintained and supported. But it's open source - it's done in my (and others') spare time. It's not perfect. If you find ways to improve things, then please do contribute! The code and docs are on GitHub, and pull requests are very much welcome.

ActiveRecord::Base.connection.execute(..) looking for column with name of a value I am trying to insert into a table

Newbie question.
I am trying to use ActiveRecord::Base.connection.execute(..) in a ruby 3.1 app and docs I have read seem to be straight forward but for the life in me I can seem to understand why I cant get the code below to work. The error message I am getting suggests that the execute function is looking for a column with the name of one of the values I am trying to save but I dont understand why.
Firstly, my db table structure is as follows:
create_table "countries", :force => true do |t|
t.string "iso3"
t.string "iso2"
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
And the code Im playing with is as follows:
code = 'ZA'
name = 'South Africa'
ActiveRecord::Base.connection.execute("INSERT INTO countries ('iso3', 'iso2', 'name')
VALUES ('Null', #{code}, #{name})")
The error message I am getting is as follows:
SQLite3::SQLException: no such column: ZA: INSERT INTO countries ('iso3', 'iso2', 'name')
VALUES ('Null', ZA, SouthAfrica)
Where did you get the basis for this? Code of this variety is a sterling example of what not to do.
If you have ActiveRecord, then you have ActiveRecord::Model, and with that you're on the right track and pretty much done. You don't need to write raw SQL for routine things of this variety. It's not necessary, and more, it's extremely dangerous for the reasons you've just discovered. You can't just shove random things in to your query or you will end up with nothing but trouble.
What you should be doing is declaring a model and then using it:
# Typically app/models/country.rb
class Country < ActiveRecord::Base
end
To insert once you have a model is made seriously easy:
Country.create(
:code => 'ZA',
:name => 'South Africa'
)
A good ActiveRecord reference is invaluable as this facility will make your life significantly easier if you make use of it.
Within Rails you usually go about generating these automatically so that you have something rough to start with:
rails generate model country
This will take care of creating the migration file, the model file, and some unit test stubs you can fill in later.
The error is just because if the missing quotes. it should be like:
INSERT INTO countries ('iso3', 'iso2', 'name') VALUES ('Null', 'ZA', 'SouthAfrica')

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.

Change starting id number

I have an 'Account' model in Rails with its corresponding 'accounts' table in the database. If I wipe the database and start over, the 'account_id' field will always start at 1 and count up from there. I would like to change the starting number, so that, when the very first account is created in a fresh database, the 'account_id' is, say, 1000. Is there a way to do that in Rails, or do I need specialized database-dependent SQL code?
For the sake of illustration, here is a simplified version of my 'accounts' table:
create_table "accounts", :force => true do |t|
t.string "email", :null => false
t.string "crypted_password", :null => false
t.string "name", :null => false
t.boolean "email_verified", :default => false
end
for PostgreSQL:
execute("ALTER SEQUENCE accounts_id_seq START with 1000 RESTART;")
see https://www.postgresql.org/docs/current/static/sql-altersequence.html
You'll need to do some specialized database-dependent SQL to get this functionality.
If you're using MySQL, you can add the following code to your migration after the create_table code:
execute("ALTER TABLE tbl AUTO_INCREMENT = 1000")
For sqlite
sequences are stored in the table sqlite_sequence (name,seq)
Check first if the sequence already exists?
select name,seq from sqlite_sequence where name = 'accounts'
if sequence.empty?
insert into sqlite_sequence(name,seq) values('accounts', 1000);
else
update sqlite_sequence set seq = 1000 where name = 'accounts';
A pure Ruby, database-independent approach could be:
class MyModel
before_create do
self.id = [1000, (self.class.maximum(:id) || 0) + 1].max if self.id.nil?
end
end
When you're creating lots of records at once, this may not perform so well though.
Another possible concept might be to simply use a start_at variable in your model file?
Such as define a base number such as start_at = 53131 and then...
Make an accessor method (could call it "key") which adds your start_at number to your database's real ID before returning it.
And you could make a attr writer method that subtracts the start_at before saving the key, that may not even be necessary depending on your implementation.
Example in pseudo-code so bear with me.
class FakeModel
attr_accessible :name
start_at = 53121
def self.find_by_key(key)
find_by_id(key-start_at))
end
def key
(self.id+start_at)
end
end
Not sure how practical this is or if it would even work 100% but at least you wouldn't have to modify the database to handle it.
in SQL Server:
execute('DBCC CHECKIDENT (accounts, reseed, 1000)')
In my case, the development environment and the production environment are using different type of database.
This code block will run the relevant execution accordin to DB type - just put it in the relevant migration:
puts 'Migration trys to set initial account ID to adapter:' + ActiveRecord::Base.connection.adapter_name
case ActiveRecord::Base.connection.adapter_name
when 'MySQL'
execute('ALTER TABLE accounts AUTO_INCREMENT = 1000')
when 'SQLServer'
execute('DBCC CHECKIDENT (accounts, reseed, 1000)')
when 'SQLite'
begin
execute('insert into sqlite_sequence(name,seq) values(\'accounts\', 1000);')
rescue
puts 'insert error... updating'
end
execute('update sqlite_sequence set seq = 1000 where name = \'accounts\';')
else
puts "cant recognize the database"
end

Resources