Insert Ecto model with already existing model as an association - phoenix-framework

I have 2 models, entries:
schema "entries" do
belongs_to :exception, Proj.Exception
field :application, :string
end
And exceptions:
schema "exceptions" do
field :name, :string
end
The migration script:
def change do
create table(:exceptions) do
add :name, :string, null: false
end
create table(:entries) do
add :exception_id, references(:exceptions), null: false
add :application, :string, null: false
end
end
My goal is to store exceptions that happen in another system. I want the project to be able to store each exception in the second table exception if they are not already there and then store the application name and the exception id in the first table entries. There will be 1000s of records in entries and a handful in exceptions.
Assuming entry_params uses this JSON format:
{
exception: "NPE",
application: "SomeApp"
}
the method that should create the entries:
def create(conn, %{"entry" => entry_params}) do
exception = Repo.get_by(Exception, name: entry_params["exception"]) ||
Repo.insert!(%Exception{name: entry_params["exception"]})
changeset =
Entry.changeset(%Entry{}, entry_params)
|> Ecto.Changeset.put_assoc(:exception, exception)
Repo.insert!(changeset)
end
This will print out:
** (ArgumentError) unknown assoc `exception` in `put_assoc`
If I change the entries model to use has_one instead of belongs_to (and I think belongs_to "feels" bad here. An entry does not belong to an exception, it just has an exception) it throws the following:
** (Postgrex.Error) ERROR (not_null_violation): null value in column "exception_id" violates not-null constraint
table: entries
column: exception_id
What I want basically to first create an Exception (if it does not exist) and than create a new Entry of a system error and put the previously begotten Exception in the entry as an association.
What is wrong here?

Typo. belongs_to :exception, Proj.Exception should be belongs_to :exceptions, Proj.Exception
Association. Based on the data model in the question, I think the put_assoc is the wrong way around because in the data schema in the question, an exception has_many entries and an entry belongs_to exceptions. Ecto.Changeset.put_assoc(entries_changeset, :exception, exception) should be Ecto.Changeset.put_assoc(exception_changeset, :entries, entries)
Attempted solution:
entries schema:
schema "entries" do
field :application, :string
belongs_to :exceptions, Proj.Exception, on_replace: :nilify
end
exceptions schema:
schema "exceptions" do
field :name, :string
has_many :entry, Proj.Entry, on_delete: :delete_all, on_replace: :delete
end
migration script:
def change do
create table(:exceptions) do
add :name, :string, null: false
end
create table(:entries) do
add :application, :string, null: false
add :exception_id, references(:exceptions)
end
end
Assuming entry_params uses this JSON format:
{
exception: "NPE",
application: "SomeApp"
}
create or update the exceptions and the associated entries:
def create(conn, %{"entry" => entry_params}) do
new_entry = Entry.changeset(%Entry{}, entry_params)
changeset =
case Repo.get_by(Exception, name: entry_params["exception"]) do
:nil ->
exception = %Exception{name: entry_params["exception"]} |> Repo.insert!
Ecto.Changeset.build_assoc(exception, :entries, [new_entry])
struct ->
changeset = Ecto.Changeset.change(struct)
data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:model) # Ecto 1.x
# data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:data) # Ecto 2.0.x
Ecto.Changeset.put_assoc(changeset, :entries, [new_entry | data.entries])
end
Repo.insert!(changeset)
end

Related

Association Type mismatch

I am trying to add User details into 2 tables The table Associations are
Distributor Table:
class Distributor < ApplicationRecord
has_one :authentication, dependent: :destroy
end
Authentication Table
class Authentication < ApplicationRecord
belongs_to :distributor
belongs_to :user
validates :email,presence: true
before_save :create_remember_token
private
def create_remember_token
self.remember_token = SecureRandom.urlsafe_base64
end
end
Authentication table details
Authentication(id: integer, email: string, password: string, created_at: datetime, updated_at: datetime, admin: boolean, remember_token: string, user_id: integer, mail_confirmation: boolean, distributor: boolean, distributor_id: integer)
Controller for create
def create
#distributor = Distributor.new(distributor_params)
if #distributor.save
params[:distributor][:distributor_id] = #distributor.id
params[:distributor][:password] = "Default"
params[:distributor][:distributor] = "true"
#authe_distributor = Authentication.new(authen_distributor_params)
if #authe_distributor.save
redirect_to #distributor
else
render 'new'
end
else
render 'new'
end
end
whenever i am trying to add the details i getting this error.. anyone tell me what i am doing wrong..
Your Authentication model has both a column and an association with the same name: distributor. This is problematic because all though you are trying to set the column named distributor to the "true" rails ends up trying to set the association to that value which is what causes the exception.
Rename either the column or the association so that you no longer have this clash and the problem should go away.

Rspec integral factories in test

I apologize if my English is not perfect but it is not my native language.
I have to test a MySql database with Rails 4.2, Rspec 3.3 and FactoryGirl 4.5.
The tests of base models are green. The problems comes when I have to test a model that contains foreign keys that can't be duplicated.
At first I have two models (dimension.rb through feature.rb and technical.rb) each having a foreign key that comes from the same model (current.rb):
#models/dimension.rb
class Dimension < ActiveRecord::Base
belongs_to :current
has_many :features
...
end
#models/feature.rb
class Feature < ActiveRecord::Base
belongs_to :dimension
has_many :bxes
...
end
#models/technical.rb
class Technical < ActiveRecord::Base
belongs_to :current
has_many :bxes
...
end
These two models are placed in the final model (bxe.rb)
#models/bxe.rb
class Bxe < ActiveRecord::Base
belongs_to :feature
belongs_to :technical
...
validates :technical_id, presence: true
validates :feature_id, presence: true
end
The Current model is:
#models/current.rb
class Current < ActiveRecord::Base
has_many :technicals
has_many :dimensions
validates :current, presence: true, uniqueness: true
validates :value, presence: true, uniqueness: true
end
The factories are the following:
#spec/factories/current.rb
FactoryGirl.define do
factory :current do
trait :lower do
current '800A'
value 800
end
trait :higher do
current '2000A'
value 2000
end
end
end
#spec/factories/dimension.rb
FactoryGirl.define do
factory :dimension do
...
trait :one do
current {create(:current, :lower)}
end
trait :two do
current {create(:current, :higher)}
end
end
end
#spec/factories/feature.rb
FactoryGirl.define do
factory :feature do
descr 'MyString'
dimension { create(:dimension, :one) }
...
end
end
#spec/factories/technical.rb
FactoryGirl.define do
factory :technical do
...
trait :A do
current { create(:current, :lower) }
end
trait :L do
current { create(:current, :higher) }
end
end
end
#spec/factories/bxes.rb
FactoryGirl.define do
factory :bxe do
...
technical {create(:technical, :A) }
feature
end
end
When I run the test on the models the first command (technical) runs and the factory creates a Current record with id = 1 but the second (features) fails, since the factory try again to create the record of Current with the same data, action prohibited from the model current.rb
#rspec spec/models
2.1.2 :001 > FactoryGirl.create(:bxebusbar, :one)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... INSERT INTO `currents` (`current`, `value`, `created_at`, `updated_at`) VALUES ('800A', 800, ..., ...)
... INSERT INTO `technicals` (..., `current_id`, ..., `created_at`, `updated_at`) VALUES (..., 1, ..., ...)
... Current Exists ... SELECT 1 AS one FROM `currents` WHERE `currents`.`current` = BINARY '800A' LIMIT 1
... ActiveRecord::RecordInvalid: Validation failed: Current has already been taken, Value has already been taken
I think that the problem can be solved by creating once only Current record and then using it in the technical and features factories, what would happen in reality, but I do not know how to do that.
Any suggestion? Thanks
You can use sequences to generate values that will not be duplicated. Another option is to use DatabaseCleaner and setup it to clean database after each test.
With first option:
FactoryGirl.define do
factory :current do
trait :lower do
sequence :current do { |n| "#{n}A" }
sequence :value do {|n| "#{n}" }
end
end
end
Or to setup DataBase Cleaner: Database cleaner

Sequel : DRY between schema migration and model validate method

I'm wondering if I miss a way to avoid repeat validation code in my Sequel::Model#validate subclass method since I've already put all constraints into my migration file.
Here's a simple example of what I'm talking about :
Sequel.migration do
change do
create_table :users do
primary_key :id
String :name, :null => false, :unique => true
end
end
end
class User < Sequel::Model
def validate
super
validates_presence :name
validates_unique :name
validates_type String :name
end
end
It seems very painful and errors prone to have to repeat all the constraints in the validate method. Did I miss something or there's no other way to do that ?
Any advice will be appreciated, thanks
Sequel has some nice plugins and extensions.
Sequel::Model.plugin(:auto_validations)
Sequel::Model.plugin(:constraint_validations)
and
DB.extension(:constraint_validations)
auto_validations
The auto_validations plugin automatically sets up three types of
validations for your model columns:
type validations for all columns
not_null validations on NOT NULL columns (optionally, presence
validations)
unique validations on columns or sets of columns with unique indexes
See http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/AutoValidations.html
constraint_validations
The constraint_validations extension is designed to easily create
database constraints inside create_table and alter_table blocks. It
also adds relevant metadata about the constraints to a separate table,
which the constraint_validations model plugin uses to setup automatic
validations.
See http://sequel.jeremyevans.net/rdoc-plugins/files/lib/sequel/extensions/constraint_validations_rb.html
and
http://sequel.jeremyevans.net/rdoc-plugins/classes/Sequel/Plugins/ConstraintValidations.html
Your example would look like this
Sequel::Model.plugin(:auto_validations)
Sequel::Model.plugin(:constraint_validations)
Sequel.migration do
up do
extension(:constraint_validations)
create_table :users do
primary_key :id
String :name, :null => false, :unique => true
validate do
presence :name,
name: :presence_name
end
end
end
down do
extension(:constraint_validations)
drop_table(:users)
end
end
class User < Sequel::Model
end
I think, it's normal. Don't worry.

Wrong STI column is used during migration

In a new migration I tried to add a column and populate its values:
def up
add_column :topics, :status, :string
Topic.reset_column_information
Topic.find_each do |t|
if t.ended?
t.status = 'ended'
end
end
Topic.reset_column_information
end
I am not sure why, but after the column is added, during the Topic.find_each the following exception were raised:
The single-table inheritance mechanism failed to locate the subclass: 'other'.
My Topic model did have the following declared:
self.inheritance_column = 'class_name'
So I am not sure why it still try to look up the STI subclass using the type column.
I found that once I removed reset_column_information, the problem is gone.
add_column :topics, :status, :string
# Topic.reset_column_information
Topic.find_each do |t| ...
reset_column_information probably has a bug at removing the STI column setting.

Many-to-Many Uniqueness Constraint Test Not Working

I have a many-to-many relationship with a join table in my Rails application. I'm using the has_many :through idiom in my models. To keep things simple, lets call my first class Student, my second class Course, and the join table class Enrollment (which contains fields student_id and course_id). I want to make sure that a given Student is associated with a given Course at most once (i.e. the {student_id, course_id} tuple should be unique in the enrollment table).
So I have a migration a that enforces this uniqueness.
def change
add_index :enrollments, [:student_id, :course_id], :unique => true
end
In addition my model classes are defined as such:
class Student < ActiveRecord::Base
has_many :enrollments
has_many :courses, :through => :enrollment
end
class Course < ActiveRecord::Base
has_many :enrollments
has_many :students, :through => :enrollment
end
class Enrollment < ActiveRecord::Base
belongs_to :student
belongs_to :course
validates :student, :presence => true
validates :course, :presence => true
validates :student_id, :uniqueness => {:scope => :course_id}
end
In a rails console, I can do the following:
student = Student.first
course = Course.first
student.courses << course
#... succeeds
student.courses << course
#... appropriately fails and raises an ActiveRecord::RecordInvalid exception
In my RSpec test, I do the exact same thing and I get no exception with the following code:
#student.courses << #course
expect { #student.courses << #course }.to raise_error(ActiveRecord::RecordInvalid)
And so my test fails and reports:
expected ActiveRecord::RecordInvalid but nothing was raised
What's going on here? What could I be doing wrong? How do I fix it?
Rails uses model level validation, if you want strict checking for uniquiness you need to use database level - foreign keys for example. But in this case you need to catch exceptions from database connector.
This is strange because in my code (very similar to your) validation for unique raises exception.
There's a couple of things here that could be happening:
#courses has changed between uses.
#student has changed between uses.
By using let you'll protect these values from changing between expectations.
let(:course) { Course.first }
let(:student) { Student.first }
subject{ student.courses << course << course }
it { should raise_error(ActiveRecord::RecordInvalid) }
Or, there could just be something wrong with your code :)

Resources