RoR Achievement System - Polymorphic Association & Design Issues - ruby-on-rails

I'm attempting to design an achievement system in Ruby on Rails and have run into a snag with my design/code.
Attempting to use polymorphic associations:
class Achievement < ActiveRecord::Base
belongs_to :achievable, :polymorphic => true
end
class WeightAchievement < ActiveRecord::Base
has_one :achievement, :as => :achievable
end
Migrations:
class CreateAchievements < ActiveRecord::Migration
... #code
create_table :achievements do |t|
t.string :name
t.text :description
t.references :achievable, :polymorphic => true
t.timestamps
end
create_table :weight_achievements do |t|
t.integer :weight_required
t.references :exercises, :null => false
t.timestamps
end
... #code
end
Then, when I try this following throw-away unit test, it fails because it says that the achievement is null.
test "parent achievement exists" do
weightAchievement = WeightAchievement.find(1)
achievement = weightAchievement.achievement
assert_not_nil achievement
assert_equal 500, weightAchievement.weight_required
assert_equal achievement.name, "Brick House Baby!"
assert_equal achievement.description, "Squat 500 lbs"
end
And my fixtures:
achievements.yml...
BrickHouse:
id: 1
name: Brick House
description: Squat 500 lbs
achievable: BrickHouseCriteria (WeightAchievement)
weight_achievements.ym...
BrickHouseCriteria:
id: 1
weight_required: 500
exercises_id: 1
Even though, I can't get this to run, maybe in the grand scheme of things, it's a bad design issue. What I'm attempting to do is have a single table with all the achievements and their base information (name and description). Using that table and polymorphic associations, I want to link to other tables that will contain the criteria for completing that achievement, e.g. the WeightAchievement table will have the weight required and exercise id. Then, a user's progress will be stored in a UserProgress model, where it links to the actual Achievement (as opposed to WeightAchievement).
The reason I need the criteria in separate tables is because the criteria will vary wildly between different types of achievements and will be added dynamically afterwards, which is why I'm not creating a separate model for each achievement.
Does this even make sense? Should I just merge the Achievement table with the specific type of achievement like WeightAchievement (so the table is name, description, weight_required, exercise_id), then when a user queries the achievements, in my code I simply search all the achievements? (e.g. WeightAchievement, EnduranceAchievement, RepAchievement, etc)

The way achievement systems generally work is that there are a large number of various achievements that can be triggered, and there's a set of triggers that can be used to test wether or not an achievement should be triggered.
Using a polymorphic association is probably a bad idea because loading in all the achievements to run through and test them all could end up being a complicated exercise. There's also the fact that you'll have to figure out how to express the success or failure conditions in some kind of table, but in a lot of cases you might end up with a definition that does not map so neatly. You might end up having sixty different tables to represent all the different kinds of triggers and that sounds like a nightmare to maintain.
An alternative approach would be to define your achievements in terms of name, value and so on, and have a constant table which acts as a key/value store.
Here's a sample migration:
create_table :achievements do |t|
t.string :name
t.integer :points
t.text :proc
end
create_table :trigger_constants do |t|
t.string :key
t.integer :val
end
create_table :user_achievements do |t|
t.integer :user_id
t.integer :achievement_id
end
The achievements.proc column contains the Ruby code you evaluate to determine if the achievement should be triggered or not. Typically this gets loaded in, wrapped, and ends up as a utility method you can call:
class Achievement < ActiveRecord::Base
def proc
#proc ||= eval("Proc.new { |user| #{read_attribute(:proc)} }")
rescue
nil # You might want to raise here, rescue in ApplicationController
end
def triggered_for_user?(user)
# Double-negation returns true/false only, not nil
proc and !!proc.call(user)
rescue
nil # You might want to raise here, rescue in ApplicationController
end
end
The TriggerConstant class defines various parameters you can tweak:
class TriggerConstant < ActiveRecord::Base
def self.[](key)
# Make a direct SQL call here to avoid the overhead of a model
# that will be immediately discarded anyway. You can use
# ActiveSupport::Memoizable.memoize to cache this if desired.
connection.select_value(sanitize_sql(["SELECT val FROM `#{table_name}` WHERE key=?", key.to_s ]))
end
end
Having the raw Ruby code in your DB means that it is easier to adjust the rules on the fly without having to redeploy the application, but this might make testing more difficult.
A sample proc might look like:
user.max_weight_lifted > TriggerConstant[:brickhouse_weight_required]
If you want to simplify your rules, you might create something that expands $brickhouse_weight_required into TriggerConstant[:brickhouse_weight_required] automatically. That would make it more readable by non-technical people.
To avoid putting the code in your DB, which some people may find to be in bad taste, you will have to define these procedures independently in some bulk procedure file, and pass in the various tuning parameters by some kind of definition. This approach would look like:
module TriggerConditions
def max_weight_lifted(user, options)
user.max_weight_lifted > options[:weight_required]
end
end
Adjust the Achievement table so that it stores information on what options to pass in:
create_table :achievements do |t|
t.string :name
t.integer :points
t.string :trigger_type
t.text :trigger_options
end
In this case trigger_options is a mapping table that is stored serialized. An example might be:
{ :weight_required => :brickhouse_weight_required }
Combining this you get a somewhat simplified, less eval happy outcome:
class Achievement < ActiveRecord::Base
serialize :trigger_options
# Import the conditions which are defined in a separate module
# to avoid cluttering up this file.
include TriggerConditions
def triggered_for_user?(user)
# Convert the options into actual values by converting
# the values into the equivalent values from `TriggerConstant`
options = trigger_options.inject({ }) do |h, (k, v)|
h[k] = TriggerConstant[v]
h
end
# Return the result of the evaluation with these options
!!send(trigger_type, user, options)
rescue
nil # You might want to raise here, rescue in ApplicationController
end
end
You'll often have to strobe through a whole pile of Achievement records to see if they've been achieved unless you have a mapping table that can define, in loose terms, what kind of records the triggers test. A more robust implementation of this system would allow you to define specific classes to observe for each Achievement, but this basic approach should at least serve as a foundation.

Related

In Rails where do I put the error detection logic to validate user input from a form?

I'm writing a minimal rails app as a way to learn a bit more about rails.
The app is going to track stuff (books to start with). So I need a "Location" to identify where a given item is.
create_table "locations", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t|
t.bigint "located_at"
t.integer "sort"
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["name"], name: "index_locations_on_name", unique: true
end
"sort" is irrelevant for this question.
In the form to create a new location the user gets to enter a name (say "X") and optionally say where "X" itself is located (say "Y").
So to create the location in the controller I will do something like
#location = Location.new(location_params)
But I need to turn "Y" into a location.id for "Y". In addition, if "Y" doesn't exist, I need to raise some kind of error.
What I have now is a virtual variable located_at_text and in the controller I do this:
modified_location_params = location_params
located_at_text = modified_location_params["located_at_text"]
located = nil
located = Location.find_by_name(located_at_text) unless located_at_text.nil? or located_at_text.strip.empty?
modified_location_params["located_at"] = located.nil? ? 0 : located.id
#location = Location.new(modified_location_params)
(I don't know why I can't fiddle with location_params ... but that would be a different question ... I'll worry about that once I know where I'm best off putting my code. Also my app does't mind a 0 for location.id).
Various tutorials suggest that some of this logic should be in the model, but various examples also do similar work in the controller.
Which is the "rails way"?
I made some assumptions because not all of the example code made sense to me, but the basic outline would be:
Use ActiveRecord callbacks instead of checking input in the controller
Make sure relationships are correctly set so you can build objects from them
Use first_or_create to clean up the parent lookup/creation
In my experience, manipulating input params is a code smell. Sometimes you have to do it, but usually it's telling you something is incorrectly designed.
Some of the param manipulation you can accomplish with ActiveRecord callbacks. One note, it looks like located_at_text is not part of a model, you will need to check this in the controller or add an attr_reader to the model (but I don't think you should, this is another code smell).
class Location < ApplicationRecord
validates :name, presence: true
end
Set up the relationships in the model. This will let you build out the related records.
class Location < ApplicationRecord
belongs_to :located_at
validates :name, presence: true
end
class LocatedAt < ApplicationRecord
has_many :locations
end
Normally, this setup would carry the ID for LocatedAt in the url (e.g. .../#{located_at_id}/location/new), but this isn't required if you are going to explicitly pass a located_at id which it looks like you are.
This is outside the scope of your question, but if you are breaking from the url pattern where parent ids are passed in the url, it would be very useful for your users to give them some kind of auto-complete search or a UI element that lets them know explicitly that they are creating a new LocatedAt record. It will also make your life easier because you can pass in the id resulting from the search or a trigger param to create the new record first.
Finally, use first_or_create to check that located_at exists (and if not create it). This assumes that there is only one field on LocatedAt and it is called name.
Note: I changed the model from Location since it didn't make sense to me that Location was going to look for itself in the example.
#located_at = LocatedAt
.where(name: params[:form_name][:located_at_text])
.first_or_create
#location = #located_at.locations.new(location_params)

How should I use Rails to index and query a join table?

I have a ruby on Rails 4 app, using devise and with a User model and a Deal model.
I am creating a user_deals table for has_many/has_many relationship between User and Deal.
Here is the migration
class CreateUserDeals < ActiveRecord::Migration
def change
create_table :user_deals do |t|
t.belongs_to :user
t.belongs_to :deal
t.integer :nb_views
t.timestamps
end
end
end
When a user load a Deal (for example Deal id= 4), I use a method called show
controllers/deal.rb
#for the view of the Deal page
def show
end
In the view of this Deal id=4 page, I need to display the nb of views of the Devise's current_user inside the Deal page the user is currently on.
deal/show.html
here is the nb of views of user: <% current_user.#{deal_id}.nb_views%>
Lets' say I have 10M+ user_deals lines, I wanted to know if I should use an index
add_index :user_deals, :user_id
add_index :user_deals, :deal_id
or maybe
add_index(:deals, [:user_id, deal_id])
Indeed in other situations I would have said Yes, but here I don't know how Rails works behind the scenes. It feels as if Rails is aware of what to do without me needing to speed up the process,...as if when Rails loads this view that there is no SQL query (such as 'find the nb of views WHERe user_id= x and deal_id= Y')....because I'm using just for the current_user who is logged-in (via devise's current_user) and for deal_id Rails knows it as we are on the very page of this deal (show page) so I just pass it as a parameter.
So do I need an index to speed it up or not?
Your question on indexes is a good one. Rails does generate SQL* to do its magic so the normal rules for optimising databases apply.
The magic of devise only extends to the current_user. It fetches their details with a SQL query which is efficient because the user table created by devise has helpful indexes on it by default. But these aren't the indexes you'll need.
Firstly, there's a neater more idiomatic way to do what you're after
class CreateUserDeals < ActiveRecord::Migration
def change
create_join_table :users, :deals do |t|
t.integer :nb_views
t.index [:user_id, :deal_id]
t.index [:deal_id, :user_id]
t.timestamps
end
end
end
You'll notice that migration included two indexes. If you never expect to create a view of all users for a given deal then you won't need the second of those indexes. However, as #chiptuned says indexing each foreign key is nearly always the right call. An index on an integer costs few write resources but pays out big savings on read. It's a very low cost default defensive position to take.
You'll have a better time and things will feel clearer if you put your data fetching logic in the controller. Also, you're showing a deal so it will feel right to make that rather than current_user the centre of your data fetch.
You can actually do this query without using the through association because you can do it without touching the users table. (You'll likely want that through association for other circumstances though.)
Just has_many :user_deals will do the job for this.
To best take advantage of the database engine and do this in one query your controller can look like this:
def show
#deal = Deal.includes(:user_deals)
.joins(:user_deals)
.where("user_deals.user_id = ?", current_user.id)
.find(params["deal_id"])
end
Then in your view...
I can get info about the deal: <%= #deal.description %>
And thanks to the includes I can get user nb_views without a separate SQL query:
<%= #deal.user_deals.nb_views %>
* If you want to see what SQL rails is magically generating just put .to_sql on the end. e.g. sql_string = current_user.deals.to_sql or #deal.to_sql
Yes, you should use an index to speed up the querying of the user_deals records. Definitely at least on user_id, but probably both [:user_id, :deal_id] as you stated.
As for why you don't see a SQL query...
First off, your code in the view appears to be incorrect. Assuming you have set up a has_many :deals, through: :user_deals association on your User class, it should be something like:
here is the nb of views of user: <%= current_user.deals.find(deal_id).nb_views %>
If you see the right number showing up for nb_views, then a query should be made when the view is rendered unless current_user.deals is already being loaded earlier in the processing or you've got some kind of caching going on.
If Rails is "aware", there is some kind of reason behind it which you should figure out. Expected base Rails behavior is to have a SQL query issued there.
Is a cleaner way of indexing other tables not:
class CreateUserDeals < ActiveRecord::Migration
def change
create_table :user_deals do |t|
t.references :user
t.references :deal
t.integer :nb_views
t.timestamps
end
end
end

I want to put string and array data in sqlite3 in ruby on rails

Hi I'm new at programming but I try to make my own service
I'm using cloud9 Ide, ruby on rails and sqlite3
Anyway I'll parse data of a dictionary and I'm about to design database like this(just for example)
[col-1] [col-2] [col-3]
[row-1] fruit apple [a,p,p,l,e]
[row-2] fruit kiwi [k,i,w,i]
...
[row-50] flower lily [l,i,l,y]
[row-51] flower rose [r,o,s,e]
...
3 column and thousands of rows
To give you more details, when an user types "fruit" in text area I want to show word list from "apple" to "kiwi"!!
I learned about storing 'string' that only users submit like this
class CreateFans < ActiveRecord::Migration
def change
create_table :fans do |t|
t.string :username
t.timestamps null: false
end
end
end
But I really don't know how to store my own data
I want to know how to add column and rows and storing local data, not user input!
Actually, I studied yesterday reading .xlsx file and show in ruby on rails through gem 'roo' but I don't know how to use this properly in database. and I want to know there is alternatives...
Thanks for reading my question and I appreciate if you give me advices :)
You can add columns to databases with migrations.
The columns don't have to be only from user input.
for example you could have a migration...
class CreateMyWord < ActiveRecord::Migration
def change
create_table :my_words do |t|
t.string :genre
t.string :word
t.string :letters
t.timestamps null: false
end
end
end
When you define your model you specify that the attribute letters is actually an array...
class MyWord < ActiveRecord::Base
serialize :letters
end
serialize will automatically convert an array to a string representation when storing the record, and will automatically convert it back when retrieving the record.
You can then populate the table yourself in your seeds.db file which you can execute with the command rake db:seed
the seeds db would possibly look like...
my_initial_words = [
['fruit', 'apple', ['a','p','p','l','e'],
['fruit', 'kiwi', ['k','i', 'w', 'i'],
...
]
my_iniital_words.each do |word_data|
MyWord.create(genre: word_data[0], word: word_data[1], letters: word_data[2])
end
Note that if the letters of the word always match the word, you don't really need a column letters in the database, just have a method in the model that creates the letters array for you when you need it.
class MyWord < ActiveRecord::Base
def letters
word.split('')
end
end

FactoryGirl blowing up spec due to foreign key in model

I've got a model Foo which has state_code as a foreign key. The States table is a (more or less) static table created to hold the codes and names for the 50 states, as well as other US postal codes (e.g. "PR" for Puerto Rico). I opted to use state_code as the primary key on States and as the foreign key on Foo, rather than something like state_id. It reads better to humans, and simplifies view logic where I want to call the state code. (EDIT - just to clarify: I don't mean calling code to access the model from the view; I mean that displaying the state as #foo.state_code seems simpler than #foo.state.state_code.)
Foo also has a has_many relationship with model Bar. Both model specs pass a spec for valid factories but for some reason when running a feature spec that builds an instance of Bar, the test blows up due to a foreign key issue related to state_code
I get passing model specs for all of my models, including the test for a valid factory. However, I'm running into trouble whenever I try to create a test object for 'Bar'. Using build blows up on a foreign key error for state_code in Foo (despite fact that the Foo factory explicitly specifies a value that is confirmed to exist as a state_code in States). Using build_stubbed for the Bar object doesn't seem to persist the object.
The models:
# models/foo.rb
class Foo < ActiveRecord
belongs_to :state, foreign_key: 'state_code', primary_key: 'state_code'
has_many :bars
validates :state_code, presence: true, length: { is: 2 }
# other code omitted...
end
# models/state.rb
class State < ActiveRecord
self.primary_key = 'state_code'
has_many :foos, foreign_key: 'state_code'
validates :state_code, presence: true, uniqueness: true, length: { is: 2 }
# other code omitted...
end
# models/bar.rb
class Bar < ActiveRecord
belongs_to :foo
# other code omitted
end
The factory below passes green for my Foo and Bar models, so from the model point of view the factories seem fine:
# spec/factores/foo_bar_factory.rb
require 'faker'
require 'date'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state_code { 'AZ' } # Set code for Arizona b/c doesn't matter which state
end
factory :bar do
name { Faker::Name.name }
website_url { Faker::Internet.url }
# other columns omitted
association :foo
end
end
...where the basic specs are:
# spec/models/foo_spec.rb
require 'rails_helper'
describe Foo, type: :model do
let(:foo) { build(:foo) }
it "has a valid factory" do
expect(foo).to be_valid
end
# code omitted...
end
# spec/models/bar_spec.rb
require 'rails_helper'
describe Bar, type: :model do
let(:bar) { build_stubbed(:bar) } # have to build_stubbed - build causes error
it "has a valid factory" do
expect(bar).to be_valid
end
end
This spec passes, with no issues. But if I use build(:bar) for Bar instead of build_stubbed, I get an error on foreign key:
1) Bar has a valid factory
Failure/Error: let(:bar) { build(:bar) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "bars" violates foreign key constraint "fk_rails_3dd3a7c4c3"
DETAIL: Key (state_code)=(AZ) is not present in table "states".
The code 'AZ' is definitely in the states table, so I'm unclear why it fails.
In a feature spec I'm attempting to create instances of bar that persist in the database, so I can test they are appearing correctly in #index, #show, and #edit actions. However I can't seem to get it working correctly. The feature spec fails:
# spec/features/bar_pages_spec.rb
require 'rails_helper'
feature "Bar pages" do
context "when signed in as admin" do
let!(:bar_1) { build_stubbed(:bar) }
let!(:bar_2) { build_stubbed(:bar) }
let!(:bar_3) { build_stubbed(:bar) }
# code omitted...
scenario "clicking manage bar link shows all bars" do
visit root_path
click_link "Manage bars"
save_and_open_page
expect(page).to have_css("tr td a", text: bar_1.name)
expect(page).to have_css("tr td a", text: bar_2.name)
expect(page).to have_css("tr td a", text: bar_3.name)
end
end
This spec fails with a message indicating no matches. Using save_and_open_page doesn't show the expected items in the view. (I have a working page with development data though, so I know that the logic actually works as expected). The thoughtbot post on build_stubbed indicates that it should persist objects:
It makes objects look look like they’ve been persisted, creates
associations with the build_stubbed strategy (whereas build still uses
create), and stubs out a handful of methods that interact with the
database and raises if you call them.
...but it doesn't appear to be doing so in my spec. Attempting to use build in lieu of build_stubbed in this spec generates the same foreign key error noted above.
I'm really stuck here. The models appear to have valid factories and pass all specs. But feature specs either blow up the foreign key relationship or don't seem to persist the build_stubbed object between views. It feels like a mess but I can't figure out the right approach to fix it. I have actual, working views in practice, that do what I expect - but I'd like to have test coverage that works.
UPDATE
I went back and updated all of the model code to remove the natural key for state_code. I followed all of #Max's recommendations. The Foo table now uses state_id as the foreign key for states; I copied in the code for app/models/concerns/belongs_to_state.rb as recommended, etc.
Updated schema.rb:
create_table "foos", force: :cascade do |t|
# columns omitted
t.integer "state_id"
end
create_table "states", force: :cascade do |t|
t.string "code", null: false
t.string "name"
end
add_foreign_key "foos", "states"
The model specs passed, and some of my simpler feature specs passed. I now realize that the problem is only when more than one Foo object gets created. When this happens, the second object fails due to the uniqueness constraint on the column :code
Failure/Error: let!(:foo_2) { create(:foo) }
ActiveRecord::RecordInvalid:
Validation failed: Code has already been taken
I've tried to set the :state_id column directly in the factory for :foo to avoid calling the :state factory. E.g.
# in factory for foo:
state_id { 1 }
# generates following error on run:
Failure/Error: let!(:foo_1) { create(:foo) }
ActiveRecord::InvalidForeignKey:
PG::ForeignKeyViolation: ERROR: insert or update on table "foos" violates foreign key constraint "fk_rails_5f3d3f12c3"
DETAIL: Key (state_id)=(1) is not present in table "states".
Obviously state_id isn't in states, since it's id on states, and state_id in foos. Another approach:
# in factory for foo:
state { 1 } # alternately w/ same error -> state 1
ActiveRecord::AssociationTypeMismatch:
State(#70175500844280) expected, got Fixnum(#70175483679340)
Or:
# in factory for foo:
state { State.first }
ActiveRecord::RecordInvalid:
Validation failed: State can't be blank
All I really want to do is create an instance of the Foo object and have it include the relationship to one of the states from the states table. I don't anticipate doing a lot of changes to the states table - it's really just a reference.
I DON'T need to create a new state. I just need to populate the foreign key state_id on the Foo object with one of the 66 values in the :id column on the states table. Conceptually, the factory for :foo would ideally just pick an integer value between 1 and 66 for the :state_id. It works in console:
irb(main):001:0> s = Foo.new(name: "Test", state_id: 1)
=> #<Foo id: nil, name: "Test", city: nil, created_at: nil, updated_at: nil, zip_code: nil, state_id: 1>
irb(main):002:0> s.valid?
State Load (0.6ms) SELECT "states".* FROM "states" WHERE "states"."id" = $1 LIMIT 1 [["id", 1]]
State Exists (0.8ms) SELECT 1 AS one FROM "states" WHERE ("states"."code" = 'AL' AND "states"."id" != 1) LIMIT 1
=> true
Only way forward I can see right now is to get rid of the uniqueness constraint on :code column in states. Or - remove the foreign key constraint between foos and states, and let Rails enforce the relationship.
Sorry for the massive post...
I'm going to be a pain in the *rse and argue that conventions might be more important than developer convenience and perceived readability.
One of the great things with Rails is that the strong conventions allow us to open up any project and figure out what is going on pretty fast (provided the original author is not a total hack). Try that with a PHP project.
One of these conventions is that foreign keys are postfixed with _id. Many other components such as FactoryGirl rely on these conventions.
I would also argue that using the state code as a primary ID will cause issues if your app ever finds use beyond the US. What happens when you need to keep track of Canadian provinces or Indian states and territories? How are you going to deal with the unavoidable conflicts? Even if you think that this might not be the deal today remember that requirements change with time.
I would model it as:
create_table "countries", force: :cascade do |t|
t.string "code", null: false # ISO 3166-1 alpha-2 or alpha-3
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "countries", ["code"], name: "index_countries_on_code"
create_table "states", force: :cascade do |t|
t.integer "country_id"
t.string "code", null: false
t.string "name", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "states", ["code"], name: "index_states_on_code"
add_index "states", ["country_id", "code"], name: "index_states_on_country_id_and_code"
add_index "states", ["country_id"], name: "index_states_on_country_id"
"and simplifies view logic where I want to call the state code"
I would argue that you should not be doing database calls at all from your views if it is avoidable. Query upfront from your controller and pass data to your views. It makes it much simpler to optimise queries and avoid N+1 issues.
Use presenters or helper methods to help manage complexity. The slight inconvenience of having to do State.find_by(code: 'AZ') instead of State.find('AZ') is most likely not as important as you think.
added:
This is how you would use associations properly in FactoryGirl. Consider the simplicity in this solution a final argument why your custom foreign key arrangement may be causing more grief than convenience.
models:
class State < ActiveRecord::Base
# Only the State model should be validating its attributes.
# You have a major violation of concerns.
validates_uniqueness_of :state_code
validates_length_of :state_code, is: 2
end
# app/models/concerns/belongs_to_state.rb
module BelongsToState
extend ActiveSupport::Concern
included do
belongs_to :state
validates :state, presence: true
validates_associated :state # will not let you save a Foo or Bar if the state is invalid.
end
def state_code
state.state_code
end
def state_code= code
self.assign_attributes(state: State.find_by!(state_code: code))
end
end
class Foo < ActiveRecord::Base
include BelongsToState
end
class Bar < ActiveRecord::Base
include BelongsToState
end
Factories:
# spec/factories/foos.rb
require 'faker'
FactoryGirl.define do
factory :foo do
name { Faker::Company.name }
city { Faker::Address.city }
website { Faker::Internet.url }
state
end
end
# spec/factories/states.rb
FactoryGirl.define do
factory :state do
state_code "AZ"
name "Arizona"
end
end
These specs use shoulda-matchers for the extremely succint validation examples:
require 'rails_helper'
RSpec.describe Foo, type: :model do
let(:foo) { build(:foo) }
it { should validate_presence_of :state }
it 'validates the associated state' do
foo.state.state_code = 'XYZ'
foo.valid?
expect(foo.errors).to have_key :state
end
describe '#state_code' do
it 'returns the state code' do
expect(foo.state_code).to eq 'AZ'
end
end
describe '#state_code=' do
let!(:vt) { State.create(state_code: 'VT') }
it 'allows you to set the state with a string' do
foo.state_code = 'VT'
expect(foo.state).to eq vt
end
end
end
# spec/models/state_spec.rb
require 'rails_helper'
RSpec.describe State, type: :model do
it { should validate_length_of(:state_code).is_equal_to(2) }
it { should validate_uniqueness_of(:state_code) }
end
https://github.com/maxcal/sandbox/tree/31773581
Also, in your feature, controller or integration specs you need to use FactoryGirl.create not build_stubbed. build_stubbed does not persist models to the database and in these cases you need your controllers to be able to load the records from the database.
Also you should avoid using CSS selectors in your feature specs if possible. Feature specs should describe your application from a user's POV.
feature "Bar management" do
context "as an Admin" do
let!(:bars){ 3.times.map { create(:bar) } }
background do
visit root_path
click_link "Manage bars"
end
scenario "I should see all the bars on the management page" do
# just testing a sampling is usually good enough
expect(page).to have_link bars.first.name
expect(page).to have_link bars.last.name
end
scenario "I should be able to edit a Bar" do
click_link bars.first.name
fill_in('Name', with: 'Moe´s tavern')
# ...
end
end
end
There was a lot going on here, but with respect to the FactoryGirl issue blowing up on the foreign key relationship between Foo and State, I've figured it out.
#Max was spot on about the problem with using a natural key for the primary key on the states table. It doesn't follow Rails convention, and led to some mixing of concerns, such as potentially having to validate the foreign key (e.g. length 2) on the Foo table.
But even after fixing that to link the tables on a Rails-friendly key (:state_id as foreign key on foos, and :id as primary key on states) -- I still could not find any way to create more than a single instance of a Foo object using the :foo factory. It either failed when I tried to "plug" an integer value into state_id, or the :state factory would fail on the second instance, stating that the code already existed. (See my update in the question for details on the attempts and related fails).
The only way around seemed to be removing the uniqueness validation on State, or eliminating the foreign key relationship at the database layer (Postgres 9.4). I decided I didn't want to do the former. And in thinking about the latter, I realized that I really don't need the foreign key constraint in the database. The states table was intended just to provide a consistent list of state codes as a reference point. If I were to delete this table for some reason, it's not true that I'd want to destroy all of the Foo records. They essentially stand alone, with the state just an attribute of Foo. I briefly considered putting the state info into a constant, but meh.
Deleting the database-level foreign key constraint fixed things for me.
bin/rails g migration RemoveForeignKeyStatesFromFoos
class RemoveForeignKeyStatesFromFoos < ActiveRecord::Migration
def change
remove_foreign_key :foos, :states
end
end
This left the :state_id column intact on my foos table, but removed the line add_foreign_key "foos", "states" from my schema.rb
bin/rails g migration AddIndexToStateIdInFoos
class AddIndexToStateIdInFoos < ActiveRecord::Migration
def change
add_index :foos, :state_id
end
end
...added the line add_index "foos", ["state_id"], name: "index_foos_on_state_id", using: :btree to my schema.
After migrating both, I initially made the mistake of deleting the :state factory, thinking that I didn't need to create new states. After some headaches in test I realized that the test database isn't normally seeded with rake db:seed - so my tests were failing due to strange errors for Module::DelegationError. Rather than build a script to seed the test dB with states, I just modified the factory, and kept the association on the :foo factory.
# spec/factories/foo_factory.rb
FactoryGirl.define do
factory :foo do
# columns omitted
state
end
factory :state do
code { Faker::Address.state_abbr }
code { Faker::Address.state }
end
end
At this point, Rails still successfully validates the has_many and belongs_to relationships in the model (which were unchanged).
I understand the add_foreign_key method is relatively new to Rails, as of 4.2. I bit off on it by conflating the fact of the relationship with the need to establish an actual foreign key constraint at the database layer.
From the Rails Guide for ActiveRecord Associations:
You are responsible for maintaining your database schema to match your
associations. In practice, this means two things, depending on what
sort of associations you are creating. For belongs_to associations you
need to create foreign keys, and for has_and_belongs_to_many
associations you need to create the appropriate join table.
The use of the term "foreign keys" in this case appears to mean a different thing for Rails, versus Postgres. Rails seems perfectly happy so long as there is a column in the belongs_to table that matches the convention [parent_table_name]_id. This can be achieved by explicitly adding the column or by using references in a migration:
Using t.integer :supplier_id makes the foreign key naming obvious and
explicit. In current versions of Rails, you can abstract away this
implementation detail by using t.references :supplier instead
In my case, this was plenty sufficient -- an actual foreign key was not necessary.

Ruby on Rails : how to generate an application-wide sequence?

I am developing on RoR 4, with Oracle, PostGreSQL and MSSQL as target databases.
I am building a hierarchy of 4 objects, for which I need to display parent-child relationships through the same query whatever level I start from. Not easy to figure out, but the hint is that none of the object should have identical IDs.
The issue here is that rails maintains a dedicated sequence for each object, so duplicated IDs will appear for sure.
How can I create a sequence to fill a unique_id field which remains unique for all my data ?
Thanks for your help,
Best regards,
Fred
I finally found this solution:
1 - create a sequence to be used by each of concerned objects
class CreateGlobalSequence < ActiveRecord::Migration
def change
execute "CREATE SEQUENCE global_seq INCREMENT BY 1 START WITH 1000"
end
end
2 - Declare this sequence to be used for identity columns in each of concerned models
class BusinessProcess < ActiveRecord::Base
self.sequence_name = "global_seq"
...
end
class BusinessRule < ActiveRecord::Base
self.sequence_name = "global_seq"
...
end
and so on. It works fine.
Rails is great !
Thanks for your help, and best regards,
Fred
Id column for each table is unique identifier for each table record. It will not make any impact on other table Id column.
Don't know why you need this. But you can achieve it by some extent. Like below :
class CreateSimpleModels < ActiveRecord::Migration
def self.up
create_table :simple_models do |t|
t.string :xyz
t.integer :unique_id
t.timestamps
end
execute "CREATE SEQUENCE simple_models_unique_id_seq OWNED BY
simple_models.unique_id INCREMENT BY 1 START WITH 100000"
end
def self.down
drop_table :simple_models
execute "DELETE SEQUENCE simple_models_unique_id_seq"
end
end
But after 100000 record in db it will again going to similar for other model.
The default id column has the identity attribute, which is stored per-table. If your models fit the bill for Single Table Inheritance you'd be able to define a custom id attribute on the base class. In your case since you said it's a hierarchy that might be the way to go.
The harder? (STI is a bit to digest but very powerful) way of doing this involves what I'm working on this similar issue with a shared PAN (Private Account Number in this system) in a shared namespace.
class CreatePans < ActiveRecord::Migration
def change
create_table :pans do |t|
t.string :PAN
t.timestamps
end
end
end
class AddPanIdToCustomers < ActiveRecord::Migration
def change
add_column :customers, :pan_id, :integer
end
end
The first migration will add the ID table, the second adds the foreign key to the customers table. You'll also need to add the relationships to the models has_many :pans and belongs_to :customers. You can then refer to their identity by the :pan_id attribute (however you name it). It's a roundabout way of doing things, but in my case business requirements force it - hacky as it is.

Resources