I am having problems with the uniqueness validator in Rails 6 when I use the scope attribute.
I have a many-to-many relationship between Product and Machine. My join table is called ProductMachine.
In my join table I have the following validation:
validates :product_id, uniqueness: { scope: :machine }
I am building an import tool that allows me to create the associations for products and machines via a csv import. In this import, I have code that creates new records like this:
machine = Machine.first
machine.product_machines.new(product_id: 1234)
machine.product_machines.new(product_id: 1234)
machine.product_machines.new(product_id: 5678)
machine.save!
When I call machine.save! the new product_machines are inserted into the database and the validation does not fail. If I run this same exact code a second time, it fails as expected. I assume this is because the scope is making a where clause in the validation which causes the new records to be missed since the are not persisted. How can this be fixed? Here is the documentation for the scoped validation.
https://guides.rubyonrails.org/active_record_validations.html#uniqueness
You're validating that for any given machine it can only link to the same product once. You're not validating that you can only link one machine to one product.
Normally these sorts of join tables have a UNIQUE constraint as well to be sure you don't have duplicate links.
If you're trying to enforce one link and one link only, making that table effectively a one-to-one, you need to constrain differently.
Related
How do I associate two entries in a database that are connected through a many-to-many relationship in Rails?
I'm trying to associate Users and Issues for an issue tracker. I'm using has_and_belongs_to_many, not :through. I have a :user_id and :issue_id available to me, but there doesn't seem to be User.issues.find(id) or Issue.users.find(id) available to me. I have a route post "/", to: "home#create". I'm trying to make a create method in home_controller.rb.
From the look of it you're calling the method on the User class and not an instance.
If you want to get the issues connected to a user you need to fetch the user first:
User.find(id).issues
If you want to add a record to the association you can use the shovel method or any of the methods generated by the association macro:
User.find(id).issues << Issue.find(3)
User.find(id).issues.push(Issue.find(3))
User.find(id).issue_ids = [1, 2, 3]
Besides that you have a smattering of naming issues in your schema. Use snake_case everywhere in your database schema unless you have a good reason why you want to break the conventions and feel like explicitly configuring table and foreign key names.
I would also really question if you really want to use has_and_belongs_to_many. It should only really be used if you can't foresee that you ever will need to add additional attributes to the join table or never need to query the table directly - it seems pretty unrealistic that that would be true in an issue tracker. You want has_many through: - pretty much always.
I have a route post "/", to: "home#create". I'm trying to make a
create method in home_controller.rb.
Don't throw everything into a junk drawer controller. Think about your app in terms of resources that can be CRUD:ed and create controllers that handle just that resource. You should think about what the relation between a user and an issue is in your domain and how you can model it as an actual entity in the domain logic instead of just plumbing.
Maybe all I need to do is direct you to Rails Guides: Active Record Associations.
There is neither of these
User.issues.find(id)
Issue.users.find(id)
because when you are finding an issue or user by id, you don't use the association. Instead use these:
Issues.find(id)
Users.find(id)
Since the :id is unique this will work and should be what you want.
The only time you want to query issues or users using the association will be when you have the data for the other end of the relationship.
user = User.find(user_id)
issue = user.issues.where(id: issue_id)
Since the :id field is unique, this is the same as Issues.find(id). However if you want to get a collection of a user's issues with some other data, you can put the condition for that data in the where.
You can create an issue for a user this way:
user = User.find(user_id)
issue = User.issues.create( ... )
i have Account Model,Asset, Capital and Revenue this table are all inherited in my Account model. i have 3 kind of attributes in my Account model. name, code and type. when i create an account where will be to insert will happen one in my account and the other one is in my type for example
Account.create(name: "test123", code:"test123", type:"Asset")
sql will run Two Insert one for Account model and one for Asset Table
and my sunspot work well it will reindex my database and i can search my params
but when i update my model Account my sql run one insert and one update
my question is how can i reindex my model when i update. with a particular data. i can do Sunspot.reindex but this is will load all data in my sql. that will cause me to slow
sql will run Two Insert one for Account model and one for Asset Table
FYI you use STI when you want to share same database table between multiple models because they are similar in attributes and behavior. Like AdminUser model is likely to have almost same attributes/columns as PublisherUser or ReaderUser. Therefore you might wish to have a common table called users or model User and share this table among the above mentioned models.
Point is: ActiveRecord will run a single SQL query not two, like:
INSERT INTO "accounts" ("name", "code", "type") VALUES ('test123', 'test123', 'Asset')
my question is how can i reindex my model when i update. with a particular data. i can do Sunspot.reindex but this is will load all data in my sql. that will cause me to slow
Actually sunspot_rails is designed to auto-reindex whenever you make changes to your model/record. It listens to the save callbacks.
But you need to make sure that you are not using methods like update_column(s). See the list of silent create/update methods which do not trigger callbacks and validations at all.
In addition, you need to understand the concept of batch size in terms of Solr. For performance reasons, all of your new indexes are not immediately committed. Committed means, writing indexes to database like in RDBMS commits.
By default the batch_size for commits is 50. Meaning after 50 index method executions only the indexes will be committed and you will be able to search the records. To change it, use following
# in config/initializers/sunspot_config.rb
Sunspot.config.indexing.default_batch_size = 1 # or any number
or
# in models; its not considered good though
after_commit do
Sunspot.commit
end
For manual re-indexing, you can use like #Kathryn suggested.
But, I don't think you need to intervene in the auto-operation. I think you were not seeing immediate results so you were worrying.
According to the documentation, objects will be indexed automatically if you are on Rails. But it also mentions you can reindex a class manually:
Account.reindex
Sunspot.commit
It also suggests using Sunspot.index on individual objects.
i put this to my model
after_update do
Sunspot.index Account.where(id: self.id)
end
I'm looking documentation/tips on importing data from an external API.
I'm building a website where users can add a company to their profile. However, I want people to be unable to create duplicate companies.
Therefore I want to call upon data from the official government API to verify the existence of the dossier integer by matching it to the integer entered on the form.
Any suggestions/explanations/tips are welcomed. If I find the solution I will obviously share this here.
Thanks in advance,
Julian
You can add a unique constrain to the name of the company, so you avoid duplicates
In Company.rb you can add the following validation:
validates :name, uniqueness: true
You also can use find_or_create_by like this to avoid duplicates
Company.find_or_create_by(first_name: 'company_name') do |company|
company.location = 'Wherever'
end
Here you are saying: Find the first company named 'company_name' or create a new one with a different location.
References:
reference for model validations
reference for find_or_create_by
Using uniqueness validations in Rails is not safe when there are multiple processes unless the constraint is also enforced on the database (in my case a PostgreSQL database, so see this blog post).
In my case, the uniqueness validation is conditional: it should only be enforced if another attribute in the model becomes true. So I have
class Model < ActiveRecord::Base
validates_uniqueness_of :text, if: :is_published?
def is_published?
self.is_published
end
end
So the model has two attributes: is_published (a boolean) and text (a text attribute). text should be unique across all models of type Model if is_published is true.
Using a unique index (as suggested in the linked blog post) is too constraining because it would enforce the constraint regardless of the value of is_published.
Is anyone aware of a "conditional" index on a PostgreSQL database? Or another way to fix this?
Yes, use a partial UNIQUE index.
CREATE UNIQUE INDEX tbl_txt_is_published_idx ON tbl (text) WHERE is_published;
Example:
How to add a conditional unique index on PostgreSQL
I think that - given speed is not your main concern - you can achieve proper uniqueness validation without creating additional db indexes. Goal can be achieved at the application level. This is especially valuable if you want conditional uniqueness, as some dbs (e.g. versions of MySQL < 8) does not support partial indexes (or so called filtered indexes).
My solution is based on following assumption:
uniqueness check (validator) is run by Rails in the same transaction as save/destroy action that relies on it.
This assumption seems to be true: https://api.rubyonrails.org/classes/ActiveRecord/Transactions/ClassMethods.html
Both #save and #destroy come wrapped in a transaction that ensures that whatever you do in validations or callbacks will happen under its protected cover.
transaction calls can be nested. By default, this makes all database statements in the nested transaction block become part of the parent transaction.
Having that you can use pessimistic locking (https://api.rubyonrails.org/classes/ActiveRecord/Locking/Pessimistic.html) to exclusively lock records that you want to evaluate for uniqueness in validator. That will prevent another, simultaneously running validator - and actually anything that happens after it - from executing until lock is released at the end of transaction. That ensures atomicity of validate-save pair and proper uniqueness enforcement.
In your code it would look like that:
class Model < ActiveRecord::Base
validates :text, uniqueness: {
conditions: ->{ lock.where(is_published: true) }
}
end
The only downside I can see is having db records locked for the whole validate-save process. That won't work well under heavy load, but then many applications don't work under such conditions anyway.
Here is my situation. I have model called Account. An account can have one or more contracts. The problem is that i'm dealing with a legacy application and each account's contracts are stored in a different database.
Example:
Account 1's contract are in account1_db.contracts.
Account 2's contract are in account2_db.contracts.
The database name is a field stored in accounts table.
How can i make rails association work with this?
This is a legacy PHP application and i simply can't change it to store everything in one table. I need to make it work somehow.
I tried this, but it didn't worked:
has_many :contracts, :conditions => [lambda{ Contract.set_table_name(self.database + '.contracts'); return '1' }]
Any ideas?
Why isn't database migration an option?
You're approaching this the wrong way. You want the two systems in your integration to be loosely coupled. By trying to get the two associated, you're creating an array of interdependencies that will later come around to backstab you. The approach you are trying creates tight coupling and reduces cohesion.
But, to directly answer your question, see below. Once again, I don't recommend implementing what I say below, but it would technically be a solution.
The first thing is that rails associations work only with foreign key. In fact, all database associations work this way. There isn't a ActiveRecord method of association without foreign keys as it defies what it means to associate two objects.
So you're not going to get it done with a has_many association. Instead, I would just manually create a function on your Contract model that simulates a has_many association.
class Account
memoize :contracts
def contracts
# Load from other database in here
end
def contracts=
# Push to other database in here
end
end