I have three rails objects: User
, DemoUser
and Stats
. Both the User
and the DemoUser
have many stats associated with them. The User
and Stats
tables are stored on Postgresql (using ActiveRecord). The DemoUser
is stored in redis. The id for the DemoUser
is a (random) string. The id for the User
is a (standard-rails) incrementing integer.
The stats
table has a user_id
column that can contain either the User
id or the DemoUser
id. For that reason, the user_id
column is a string, rather than an integer.
There isn't an easy way to translate from the random string to an integer, but there's a very easy way to translate the integer id to a string ( 42 -> "42"
). The ids are guaranteed not to overlap (there won't be a User
instance with the same id as a DemoUser
, ever).
I have some code that manages those stats. I'd like to be able to pass over a some_user
instance (which can either be a DemoUser
or a User
) and then be able to use the id
to fetch Stats
, update them etc. Also would be nice to be able to define a has_many
for the User
model, so I can do things like user.stats
However, operations like user.stats
would create a query like
SELECT "stats".* FROM "stats" WHERE "stats"."user_id" = 42
which then breaks with PG::UndefinedFunction: ERROR: operator does not exist: character varying = integer
Is there a way to either let the database (Postgresql), or Rails do auto-translation of the ids on JOIN? (the translation from integer to string should be simple, eg 42 -> "42"
)
EDIT : updated the question to try to make things as clear as possible. Happy to accept edits or answer questions to clarify anything.
You can't define a foreign key between two types that don't have built-in equality operators.
The correct solution is to change the string column to be an integer.
In your case you could create a user-defined =
operator for varchar = string
, but that would have messy side effects elsewhere in the database; for example, it would allow bogus code like:
SELECT 2014-01-02 = '2014-01-02'
to run without an error. So I'm not going to give you the code to do that. If you truly feel it's the only solution (which I don't think is likely to be correct) then see CREATE OPERATOR
and CREATE FUNCTION
.
One option would be to have separate user_id
and demo_user_id
columns in your stats
table. The user_id
would be an integer that you could use as a foreign key to the users
table in PostgreSQL and the demo_user_id
would be a string that would link to your Redis database. If you wanted to treat the database properly, you'd use a real FK to link stats.user_id
to users.id
to ensure referential integrity and you'd include a CHECK constraint to ensure that exactly one of stats.user_id
and stats.demo_user_id
was NULL:
check (user_id is null <> demo_user_id is null)
You'll have to fight ActiveRecord a bit to properly constrain your database of course, AR doesn't believe in fancy things like FKs and CHECKs even though they are necessary for data integrity. You'd have to keep demo_user_id
under control by hand though, some sort of periodic scan to make sure they link up with values in Redis would be a good idea.
Now your User
can look up stats using a standard association to the stats.user_id
column and your DemoUser
can use stats.demo_user_id
.
For the time being, my 'solution' is not to use a has_many
in Rails, but I can define some helper functions in the models if necessary. eg
class User < ActiveRecord::Base
# ...
def stats
Stats.where(user_id: self.id.to_s)
end
# ...
end
also, I would define some helper scopes to help enforce the to_s
translation
class Stats < ActiveRecord::Base
scope :for_user_id, -> (id) { where(user_id: id.to_s) }
# ...
end
This should allow calls like
user.stats
and Stats.for_user_id(user.id)
I think I misunderstood a detail of your issue before because it was buried in the comments.
(I strongly suggest editing your question to clarify points when comments show that there's something confusing/incomplete in the question).
You seem to want a foreign key from an integer column to a string column because the string column might be an integer, or might be some unrelated string. That's why you can't make it an integer column - it's not necessarily a valid number value, it might be a textual key from a different system.
The typical solution in this case would be to have a synthetic primary key and two UNIQUE
constraints instead, one for keys from each system, plus a CHECK
constraint preventing both from being set. Eg
CREATE TABLE my_referenced_table (
id serial,
system1_key integer,
system2_key varchar,
CONSTRAINT exactly_one_key_must_be_set
CHECK (system1_key IS NULL != system2_key IS NULL),
UNIQUE(system1_key),
UNIQUE(system2_key),
PRIMARY KEY (id),
... other values ...
);
You can then have a foreign key referencing system1_key
from your integer-keyed table.
It's not perfect, as it doesn't prevent the same value appearing in two different rows, one for system1_key
and one for system2_key
.
So an alternative might be:
CREATE TABLE my_referenced_table (
the_key varchar primary key,
the_key_ifinteger integer,
CONSTRAINT integerkey_must_equal_key_if_set
CHECK (the_key_ifinteger IS NULL OR (the_key_ifinteger::varchar = the_key)),
UNIQUE(the_key_ifinteger),
... other values ...
);
CREATE OR REPLACE FUNCTION my_referenced_table_copy_int_key()
RETURNS trigger LANGUAGE plpgsql STRICT
AS $$
BEGIN
IF NEW.the_key ~ '^[\d]+$' THEN
NEW.the_key_ifinteger := CAST(NEW.the_key AS integer);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER copy_int_key
BEFORE INSERT OR UPDATE ON my_referenced_table
FOR EACH ROW EXECUTE PROCEDURE my_referenced_table_copy_int_key();
which copies the integer value if it's an integer, so you can reference it.
All in all though I think the whole idea is a bit iffy.
I think I may have a solution for your problem, but maybe not a massively better one:
class User < ActiveRecord::Base
has_many :stats, primary_key: "id_s"
def id_s
read_attribute(:id).to_s
end
end
Still uses a second virtual column, but maybe more handy to use with Rails associations and is database agnostic.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.