Add Single Table Inheritance (STI) to existing models - ruby

I currently have multiple tables that are very similar. I should probably have created them with STI.
TypeOne < ActiveRecord::Base
TypeTwo < ActiveRecord::Base
TypeThree < ActiveRecord::Base
TypeOne(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
TypeTwo(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
TypeThree(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime)
I am now trying to add STI to these. I created a BaseModel and added a type to that model.
BaseModel(id: integer, parent_id: integer, created_at: datetime, updated_at: datetime, type: string)
I also ran a migration and added to all of the types, a type column.
class AddTypeToTables < ActiveRecord::Migration
def change
add_column :type_ones, :type, :string
add_column :type_twos, :type, :string
add_column :type_threes, :type, :string
end
end
I want to combine all of the type tables into a STI. There is existing data in the models. If I were to combine them into a single table, I imagine the id's for the respective tables would conflict. For example:
#<TypeOne id: 4, parent_id: 1, created_at: "2015-05-08 18:39:09", updated_at: "2015-09-07 19:42:03">
#<TypeTwo id: 4, parent_id: 1, created_at: "2015-04-08 17:48:59", updated_at: "2015-09-07 14:17:48">
If I try to use becomes, it appears to change the class, but I cannot find the record in the BaseModel
TypeOne.last.becomes!(BaseModel)
#<BaseModel id: 4, parent_id: 1, created_at: "2015-05-08 18:39:09", updated_at: "2015-09-07 19:42:03">
BaseModel.all
=> []
I have also tried to change the type column of the inherited table to the basemodel
to = TypeOne.first
to.type = "BaseModel"
to.save
BaseModel.all
=> []
I have tried to change the classes for each to be a child of the BaseModel
TypeOne < BaseModel
TypeTwo < BaseModel
TypeThree < BaseModel
When I do this, I lose the connection to the existing data and each of the models appear empty.
How can I combine the existing tables?

As you've tagged it with PostgreSQL I'll include how to do what I suggested in my comment as an answer:
INSERT INTO base_model SELECT * FROM type_one ORDER BY id ASC;
INSERT INTO base_model SELECT * FROM type_two ORDER BY id ASC;
INSERT INTO base_model SELECT * FROM type_three ORDER BY id ASC;
To safely do this on a production dataset, put the SQL in a migration in db/migrate (i.e. in a file like db/migrate/20150907185938_integrate_tables.rb) and test it on your local database first. This should get you pretty close:
class IntegrateTables < ActiveRecord::Migration
def up
execute "INSERT INTO base_model SELECT * FROM type_one ORDER BY id ASC;"
execute "INSERT INTO base_model SELECT * FROM type_two ORDER BY id ASC;"
execute "INSERT INTO base_model SELECT * FROM type_three ORDER BY id ASC;"
end
def down
raise ActiveRecord::IrreversibleMigration, "It is unclear where original data stops and inserted data begins, can't migrate down"
end
end
Please mark this answer as accepted if it worked for you :)

Related

Active Record, Polymorphic Has Many Through with STI

I'm having some trouble with a Polymorphic Has Many Through association with STI. Let me explain what I'm trying to do:
Let's say I have a Contract. A Contract can have many Companies as parties to the agreement, namely, a Contract can have more than one Licensor (the party granting rights in the Contract) and more than one Licensee (the party receiving rights under the Contract). Both Licensors and Licensees are Companies that can be parties of more than one Contract.
So far I have the following code:
#contract.rb
class Contract < ApplicationRecord
has_many :relationships, dependent: :destroy
has_many :companies, through: :relationships
has_many :licensors, through: :relationships, source: :party, source_type: "Licensor"
has_many :licensees, through: :relationships, source: :party, source_type: "Licensee"
end
#relationship.rb
class Relationship < ApplicationRecord
belongs_to :contract
belongs_to :party, polymorphic: true
end
#company.rb
class Company < ApplicationRecord
has_many :relationships, as: :party, dependent: :destroy
has_many :contracts, through: :relationships
end
#licensor.rb
class Licensor < Company
end
#licensee.rb
class Licensee < Company
end
I think I'm very close on getting this to work. So far, the above code allows me to create a new contract and add licensors and licensees, as follows:
c = Contract.new(nickname:"Test Contract")
lor = c.licensors.new(name:"The Licensor Company")
lee = c.licensees.new(name:"Some Licensee Company")
c.save
Then the following will work:
c.licensors # results in...
Licensor Load (1.1ms) SELECT "companies".* FROM "companies" INNER JOIN "relationships" ON "companies"."id" = "relationships"."party_id" WHERE "relationships"."contract_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["contract_id", 1], ["party_type", "Licensor"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Licensor id: 1, name: "The Licensor Company", created_at: "2018-02-14 19:46:19", updated_at: "2018-02-14 19:46:19">]>
c.licensees # results in...
Licensee Load (1.3ms) SELECT "companies".* FROM "companies" INNER JOIN "relationships" ON "companies"."id" = "relationships"."party_id" WHERE "relationships"."contract_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["contract_id", 1], ["party_type", "Licensee"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Licensee id: 2, name: "Some Licensee Company", created_at: "2018-02-14 19:46:19", updated_at: "2018-02-14 19:46:19">]>
So the Licensor and Licensee are being correctly created and their party_type is being correctly set.
Unfortunately, what doesn't work is the following:
lor = Licensor.first
lor.contracts # which results in...
Contract Load (0.9ms) SELECT "contracts".* FROM "contracts" INNER JOIN "relationships" ON "contracts"."id" = "relationships"."contract_id" WHERE "relationships"."party_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["party_id", 1], ["party_type", "Company"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy []>
As you can see, I'm unable to query a list of Contracts for a specific Licensor or Licensee. It appears this is because my current setup results in query with a party_type of "Company". I assume this is because both Licensor and Licensee inherit from Company.
Is there a way to set the party_type in the Has Many Through association in the Licensor or Licensee models?
Any help would be greatly appreciated.
You could use rewhere. In Company define relationships with an association scope like this:
has_many :relationships, ->(x) { rewhere(party_type: x.class.name) }, as: :party, dependent: :destroy
You can see the party_type of Licensor being queried in the join:
>> l = Licensor.first
Licensor Load (1.4ms) SELECT "companies".* FROM "companies" WHERE "companies"."type" IN ('Licensor') ORDER BY "companies"."id" ASC LIMIT $1 [["LIMIT", 1]]
=> #<Licensor id: 1, name: "The Licensor Company", type: "Licensor", created_at: "2018-02-23 00:38:13", updated_at: "2018-02-23 00:38:13">
>> l.contracts
Contract Load (1.9ms) SELECT "contracts".* FROM "contracts" INNER JOIN "relationships" ON "contracts"."id" = "relationships"."contract_id" WHERE "relationships"."party_id" = $1 AND "relationships"."party_type" = $2 LIMIT $3 [["party_id", 1], ["party_type", "Licensor"], ["LIMIT", 11]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Contract id: 1, name: "Test Contract", created_at: "2018-02-23 00:38:13", updated_at: "2018-02-23 00:38:13">]>

Phoenix/Ecto - association not working

Working through a sample guide. What's detailed in the chapter doesn't work in my app. Pretty simple stuff, it would seem. I've got a Video model:
defmodule Rumbl.Video do
use Rumbl.Web, :model
schema "videos" do
field :url, :string
field :title, :string
field :description, :string
belongs_to :user, Rumbl.User
belongs_to :category, Rumbl.Category
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:url, :title, :description])
|> validate_required([:url, :title])
|> assoc_constraint(:category)
end
end
I've also got a Category model:
defmodule Rumbl.Category do
use Rumbl.Web, :model
schema "categories" do
field :name, :string
timestamps()
end
#doc """
Builds a changeset based on the `struct` and `params`.
"""
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:name])
|> validate_required([:name])
end
def alphabetical(query) do
from c in query, order_by: c.name
end
def names_and_ids(query) do
from c in query, select: {c.name, c.id}
end
end
In an IEX session, I load a Video record as so:
iex(21)> video = Repo.one(from v in Video, limit: 1)
[debug] QUERY OK source="videos" db=16.0ms
SELECT v0."id", v0."url", v0."title", v0."description", v0."user_id", v0."category_id", v0."inserted_at", v0."updated_at" FROM "videos" AS v0 LIMIT 1 []
%Rumbl.Video{__meta__: #Ecto.Schema.Metadata<:loaded, "videos">,
category: #Ecto.Association.NotLoaded<association :category is not loaded>,
category_id: nil, description: "test1", id: 2,
inserted_at: #Ecto.DateTime<2017-01-02 06:50:26>, title: "test1",
updated_at: #Ecto.DateTime<2017-01-02 06:50:26>, url: "test1 video.com",
user: #Ecto.Association.NotLoaded<association :user is not loaded>,
user_id: 10}
I get why the category and user associations are not loaded. I didn't preload the user and there isn't a category association yet to load.
Either way, I've got my video in memory:
iex(22)> v.id
2
Now I load my category:
iex(23)> category = Repo.get_by Category, name: "Comedy"
[debug] QUERY OK source="categories" db=0.0ms
SELECT c0."id", c0."name", c0."inserted_at", c0."updated_at" FROM "categories" AS c0 WHERE (c0."name" = $1) ["Comedy"]
%Rumbl.Category{__meta__: #Ecto.Schema.Metadata<:loaded, "categories">, id: 4,
inserted_at: #Ecto.DateTime<2017-01-07 07:03:00>, name: "Comedy",
updated_at: #Ecto.DateTime<2017-01-07 07:03:00>}
Just to prove that I have it:
iex(24)> category.id
4
Now I try to associate the video with the category:
iex(25)> changeset = Video.changeset(video, %{category_id: category.id})
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Rumbl.Video<>,
valid?: true>
iex(26)> Repo.update(changeset)
{:ok,
%Rumbl.Video{__meta__: #Ecto.Schema.Metadata<:loaded, "videos">,
category: #Ecto.Association.NotLoaded<association :category is not loaded>,
category_id: nil, description: "test1", id: 2,
inserted_at: #Ecto.DateTime<2017-01-02 06:50:26>, title: "test1",
updated_at: #Ecto.DateTime<2017-01-02 06:50:26>, url: "test1 video.com",
user: #Ecto.Association.NotLoaded<association :user is not loaded>,
user_id: 10}}
I don't understand why there aren't any changes in the changeset. This is how the guide instructs to do an association. Am I missing something?
Thanks,
John
I figured it out. I needed to add the category_id to the list of params in the Video model:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:url, :title, :description, :category_id])
|> validate_required([:url, :title])
|> assoc_constraint(:category)
end

Why doesn't passing attributes to Model.new work?

I have a model WordpressBlogger that has these attributes: (note the specific order of these columns: screen_name was added AFTER the table was generated)
id, rss_feed_id, blog_url, year_founded, created_at, updated_at, screen_name, latest_entry
When I do blogger = WordpressBlogger.new(:screen_name => "test") in my console, it doesn't seem to return a model with the screen_name set.
Instead I get this:
=> #<WordpressBlogger id: nil, rss_feed_id: nil, blog_url: nil, year_founded: nil, created_at: nil, updated_at: nil, screen_name: nil, latest_entry: nil>
Yet, when I do blogger = WordpressBlogger.new(:year_founded => 2011) in my console, it returns a model with this attribute set.
=> #<WordpressBlogger id: nil, rss_feed_id: nil, blog_url: nil, year_founded: 2011, created_at: nil, updated_at: nil, screen_name: nil, latest_entry: nil>
Is this a weird Rails bug where the attribute doesn't get set if the column in the database is AFTER Rails-created attributes such as created_at and updated_at?
UPDATE: I forgot to add it to attr_accessible.
Most likely, there is a reference to attr_accessible in the WordpressBlogger class. You probably need to add year_founded to the list of attr_accessible elements that are allowed bulk assignment.
It's actually not creating the Model. Notice there's no id set. To create it you must either
blogger = WordpressBlogger.new(:screen_name => "test")
blogger.save
or
blogger = WordpressBlogger.create(:screen_name => "test")
Is this in your model?
attr_accessor :screen_name, :year_founded
Or you may have to exit and run rails c again
You most probably have the console running when you ran your migrations. A quick reload! or restart of the console should make all columns visible.

How to I insert an array into a Postgresql table using ActiveRecord?

I have active_record setup to use a Postgresql database. One of the columns is a character varying[] (basically a varchar array).
Anyway, my import routine then reads a tab-delimited text file and inserts records. All is fine until I get to an array. The column that should be converted to an array is comma separated. But the line itself is tab separated.
A sample of the data I'm importing looks like (tab delimited):
Col1 Col2 Col3 Col4
----------------------------------------------
Apple Pear Sweet,Round,Green Fruit
Col3 is imported like (ruby): col3.split(/,/) which gives me an array in Ruby. But active_record bombs out with:
PG::Error: ERROR: array value must start with "{" or dimension information (ActiveRecord::StatementInvalid)
How can I insert that column correctly?
Also, sometimes, col3 will be NULL.
I was able to insert using the following Ruby code:
alternatenames = '{' + s[3].split(/,/).map {|n| '"' + n + '"'}.join(",") + '}'
Check Postgres' docs on this: http://www.postgresql.org/docs/9.2/static/arrays.html
You can instantiate a model using an array like [:My, :symbols] or ["My", "Strings"], however it (in my experience and form what it seems in the docs) will save the elements as strings.
Search.create(tokens: [{hash: 'value'}, {test: "fails"}])
=> TypeError: can't cast Hash to string
Where as:
[15] pry(main)> Search.create(tokens: [:G, :F])
=> #<Search id: 78, tokens: [:G, :F], created_at: "2013-12-18 06:29:36", updated_at: "2013-12-18 06:29:36">
[16] pry(main)> Search.last
=> #<Search id: 78, tokens: ["G", "F"], created_at: "2013-12-18 06:29:36", updated_at: "2013-12-18 06:29:36">
In my tests, I have a SearchEngine, Search, and Term.
class SearchEngine < ActiveRecord::Base
has_and_belongs_to_many :terms
has_many :searches, through: :terms
end
class Term < ActiveRecord::Base
has_and_belongs_to_many :searches
has_and_belongs_to_many :searche_engines
end
class Search < ActiveRecord::Base
has_many :rankings
has_many :results, through: :rankings
has_and_belongs_to_many :terms
has_many :search_engines, through :terms
end
# These work:
# these next two are the way postgrespl says to query against the array. You get the
Search.where(tokens: '{A,B}')
Search.where(tokens: '{C,D}').first_or_create
[3] pry(main)> Search.where(tokens: ['C','D']).first
ActiveRecord::StatementInvalid: PG::InvalidTextRepresentation: ERROR: array value must start with "{" or dimension information
[4] pry(main)> Search.where(tokens: '{C,D}').first
=> #<Search id: 77, tokens: ["C", "D"], created_at: "2013-12-18 06:27:24", updated_at: "2013-12-18 06:27:24">
term = "accident"
Search.where("? = ANY (tokens)", term).first
=> #<Search id: 8, tokens: ["accident", "prevention", "safety"], created_at: "2013-12-18 07:48:13", updated_at: "2013-12-18 07:48:13">
Search.create(tokens: [:Aortic, :Any, :Other, :Elements])
Search.where("'Aortic' = ANY (tokens)").first
Parent.first.first_relationships.first.second_.where("'smelly' = ANY (tokens)").first
# The next one will create one with an empty array for tokens and push it into Term.searches anyway. Same thing with 'smelly'
Term.first.searches.where("':smelly' = ANY (tokens)").first_or_create do |s| Term.first.searches << s
end
# These error
Search.where(tokens: "Aortic").first
Search.where(tokens: [:Aortic, :Any, :Other, :Elements]).first
Also, if you have nested arrays, you could do a where search with this: '{{1,2,3},{4,5,6},{7,8,9}}' to find an row with column value [[1,2,3],[4,5,6],[7,8,9]]

Nested Attributes not updating

With the following models:
class Location < ActiveRecord::Base
has_many :group_locations
has_many :groups, :through => :group_locations
accepts_nested_attributes_for :group_locations
end
class GroupLocation < ActiveRecord::Base
belongs_to :group
belongs_to :location
end
class Group < ActiveRecord::Base
has_many :group_locations
has_many :locations, :through => :group_locations
end
the following commands in rails console does not update the associated records:
>> l = Location.find(1)
=> #<Location id: 1, phone: "(949) 788-9999", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-07 23:57:32">
\>\> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
>> l.update_attributes(:phone => "(949) 788-9998", :group_locations_attributes =>
[{:group_id => 4, :location_id => 1}, {:group_id => **38**, :location_id => 1}])
=> true
>> l
=> #<Location id: 1, phone: "(949) 788-9998", ... created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-08 02:05:00">
>> l.group_locations
=> [#<GroupLocation group_id: 4, location_id: 1, created_at: "2011-06-02 00:58:07",
updated_at: "2011-06-02 00:58:07">, #<GroupLocation group_id: **37**, location_id: 1,
created_at: "2011-06-02 00:58:07", updated_at: "2011-06-02 00:58:07">]
Note that the update_attributes call attempts to change the second GroupLocation to have group_id = 38, but the change is not made (even though the phone number did change). After looking at the code generated when this was implemented in the controller and view, changing the array to a hash (which is what is created in that case) has no different results (and the form/controller) have the same effect of not updating the associated records even though the main record is updated.
Any idea what I need to do to get the nested attributes to update?
From the logs you've displayed, it doesn't appear that your GroupLocation model has an :id primary key on it. While the join table for a HABTM has just the foreign keys (group_id, location_id) on it, the model used for a has_many :through association does need a primary key as well, :id by default. Otherwise, there is no way to determine which of the child objects to update in the case of an update.
Think of it this way - you are creating your association through another discrete model that should be able to stand entirely on its own.
The convention for nested attributes is if the hash passed to the nested_attributes includes an :id, then it is considered an update, if it doesn't then it's considered a create. In your case, you're not passing in an :id, so you get new GroupLocation records where you just wanted to update existing.
I believe, also, that once you have this in place correctly, you will be able to get rid of the attr_accessible, I don't think that should be necessary.
For good info on the nested attributes functionality that covers most of this, check out this page.
The actual answer is that the nested attributes must be accessible via attr_accessible. "accepts_nested_attributes" will only do what I want if it is accompanied by "attr_accessible :group_locations".

Resources