261

Let's say that I have a class that represents locations. Locations "belong" to customers. Locations are identified by a unicode 10 character code. The "location code" should be unique among the locations for a specific customer.

The two below fields in combination should be unique
customer_id = Column(Integer,ForeignKey('customers.customer_id')
location_code = Column(Unicode(10))

So if i have two customers, customer "123" and customer "456". They both can have a location called "main" but neither could have two locations called main.

I can handle this in the business logic but I want to make sure there is no way to easily add the requirement in sqlalchemy. The unique=True option seems to only work when applied to a specific field and it would cause the entire table to only have a unique code for all locations.

Jonathon Reinhart
  • 132,704
  • 33
  • 254
  • 328
Ominus
  • 5,501
  • 8
  • 40
  • 44

4 Answers4

446

Extract from the documentation of the Column:

unique – When True, indicates that this column contains a unique constraint, or if index is True as well, indicates that the Index should be created with the unique flag. To specify multiple columns in the constraint/index or to specify an explicit name, use the UniqueConstraint or Index constructs explicitly.

As these belong to a Table and not to a mapped Class, one declares those in the table definition, or if using declarative as in the __table_args__:

# version1: table definition
mytable = Table('mytable', meta,
    # ...
    Column('customer_id', Integer, ForeignKey('customers.customer_id')),
    Column('location_code', Unicode(10)),

    UniqueConstraint('customer_id', 'location_code', name='uix_1')
    )
# or the index, which will ensure uniqueness as well
Index('myindex', mytable.c.customer_id, mytable.c.location_code, unique=True)


# version2: declarative
class Location(Base):
    __tablename__ = 'locations'
    id = Column(Integer, primary_key = True)
    customer_id = Column(Integer, ForeignKey('customers.customer_id'), nullable=False)
    location_code = Column(Unicode(10), nullable=False)
    __table_args__ = (UniqueConstraint('customer_id', 'location_code', name='_customer_location_uc'),
                     )
Wang Dingwei
  • 4,661
  • 6
  • 32
  • 43
van
  • 74,297
  • 13
  • 168
  • 171
  • 1
    I face the same problem also, but using UniqueConstraint didn't help me. After I try with Index('...') then I get a unique constraint. Is there any explanation with this behaviour? – swdev Oct 10 '13 at 06:51
  • 1
    @swdev: which RDBMS do you use? – van Oct 11 '13 at 09:30
  • I am using PostgreSQL. Is there an issue with this? – swdev Oct 11 '13 at 15:50
  • No, forget `psql` should work fine. Now: do you create a table using `Location.create(engine)` or `meta.create_all()`? Because declaring the `UniqueConstraint` like in the answer will only issue this part of the `DDL`. It will not on its own enforce the constraint. – van Oct 12 '13 at 11:51
  • I forgot to tell you that I am using Flask framework. So, it is part of model definition defined as a usual python class : `class Branch(db.Models): ...' and `UniqueConstraint` is located after and outsite the class indented block. – swdev Oct 13 '13 at 03:39
  • 3
    Thanks, but my question was: did you use SA (and Flask) to create a DB schema, or did create it separately? – van Oct 14 '13 at 17:36
  • 1
    Why is the .c. used? – Smiley Mar 19 '20 at 12:36
  • 2
    @Smiley `.c.` is a shortcut to `.columns.` – van Mar 21 '20 at 06:21
  • I'm trying this solution, but it doesn't seem to work for me. I'm not sure of the correct protocol in Stackoverflow to show you my code. If I open a new problem it will be probably be rejected as a duplicate. If I post it here, it appears like an answer. Any suggestions? – jersey bean May 19 '20 at 22:55
  • 1
    ...continuing from my previous comment. The answer doesn't seem to work for me. The only main difference between your solution and my code is that the foreign key for my table isn't in the UniqueConstraint. Does it have to be? The sqlalchemy docs seem to show UniqueConstraint as a positional parm, with ForeignKeyConstraint as the first parm. But your solution only shows UniqueConstraint in the tuple. Does I need to move the ForeignKeyConstrain from the normal definition into __table_args__? Maybe you someone could answer or revise to clarify these points? – jersey bean May 19 '20 at 22:59
  • `from sqlalchemy.schema import UniqueConstraint` – Matt Kleinsmith Jan 24 '23 at 18:47
45
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()

class Location(Base):
      __table_args__ = (
        # this can be db.PrimaryKeyConstraint if you want it to be a primary key
        db.UniqueConstraint('customer_id', 'location_code'),
      )
      customer_id = Column(Integer,ForeignKey('customers.customer_id')
      location_code = Column(Unicode(10))
Sicco
  • 6,167
  • 5
  • 45
  • 61
joash
  • 2,205
  • 2
  • 26
  • 31
  • 24
    Must be `__table_args__ = (db.UniqueConstraint('customer_id', 'location_code'),)`, don't forget the comma at the end. – bertdida Jul 05 '20 at 03:14
  • 14
    For anyone wondering why there’s that comma, `(42)` is the same as `42`: parentheses have no effect around a single value. However, `(42,)` is shorthand for a tuple of a single element: `(42,) == tuple([42])`. – bfontaine Jul 19 '21 at 14:13
  • This doesn't answer the question; the question doesn't mention Flask. – Gloweye Mar 29 '23 at 11:40
  • To adapt, I'd need intimate knowledge of Flask. And if I knew how Flask did all that, I'd have no reason to visit this question in the first place. Therefore, this answer is bad. Mostly because it completely bypasses the parts of SQLAlchemy that the actual question is about. – Gloweye Mar 31 '23 at 08:31
  • ...what exactly is your problem? There's no need to be a condescending dick. It's absolutely stupid to take a Flask course to learn to use SQL Alchemy, and even stupider to presume that anybody reading the answer on a QA site would need an unrelated course to understand it. – Gloweye Apr 06 '23 at 21:58
1

This Python3 answer is completely derivative, it just puts everything from above into a small self-contained working example for MySQL. I needed a uniqueness constraint on the table that implements a many-to-many relationship. Maybe you can run this to debug local environment problems, in my case they were purely between the keyboard and the chair :)

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy import Column, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy.orm import relationship
import logging

logging.basicConfig()
logging.getLogger('sqlalchemy.engine').setLevel(logging.INFO)
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+mysqlconnector://user:pass@localhost/mydb'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)

user_role = db.Table(
    'user_role',
    Column('uid', String(6), ForeignKey('user.uid')),
    Column('role', String(32), ForeignKey('role.name')),
    UniqueConstraint('uid', 'role', name='idx_uid_role'))

class UserModel(db.Model):
    __tablename__ = 'user'
    uid = Column(String(6), primary_key=True)
    create_time = Column(Integer, nullable=False)
    login_time = Column(Integer, nullable=True)
    roles = relationship('RoleModel', secondary='user_role',
                         backref='user', lazy='joined')

class RoleModel(db.Model):
    __tablename__ = 'role'
    name = Column(String(32), primary_key=True)
    description = Column(String(256), nullable=False)

db.create_all()

After you run this, check the indexes defined for the table like this:

mysql> show index from user_role;

And you should see:

+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table     | Non_unique | Key_name     | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| user_role |          0 | idx_uid_role |            1 | uid         | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| user_role |          0 | idx_uid_role |            2 | role        | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
| user_role |          1 | role         |            1 | role        | A         |           0 |     NULL |   NULL | YES  | BTREE      |         |               | YES     | NULL       |
+-----------+------------+--------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
3 rows in set (0.00 sec)

Create test data:

mysql> insert into user values ('abc123', 1, 2);
mysql> insert into role values ('role1', 'Description');
mysql> insert into user_role (uid, role) values ('abc123', 'role1');

Finally, test the uniqueness constraint by running that last insert a second time, you should see:

mysql> insert into user_role (uid, role) values ('abc123', 'role1');
ERROR 1062 (23000): Duplicate entry 'abc123-role1' for key 'user_role.idx_uid_role'
chrisinmtown
  • 3,571
  • 3
  • 34
  • 43
1
__table_args__ = (
    Index("idx_room_user", room_id, uid, unique=True),
    UniqueConstraint(room_id, uid, name='u_room_user'),
)

It worked under sqlalchemy 2.0

杨晓亮
  • 11
  • 2
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – xlmaster Feb 21 '23 at 07:34