3

I am trying to translate a table from pure SQL to Flask-Sqlalchemy, but the documentation available is not clear about how to do this specific scenario - Primary keys that are also foreign keys.

The SQL to build the table is the following and it works fine:

CREATE TABLE IF NOT EXISTS `ws`.`Perfil_Plano_Transacao` (
    `pptr_perf_id` INT NOT NULL,
    `pptr_tran_id` INT NOT NULL,
    `pptr_plan_id` INT NOT NULL,
    `pptr_dt_incluscao` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `pptr_dt_atualizacao` TIMESTAMP NULL,
PRIMARY KEY (`pptr_perf_id`, `pptr_tran_id`, `pptr_plan_id`),
INDEX `fk_Perfil_Plano_Transacao_Transacao1_idx` (`pptr_tran_id` ASC),
INDEX `fk_Perfil_Plano_Transacao_Plano1_idx` (`pptr_plan_id` ASC),
CONSTRAINT `fk_Perfil_Plano_Transacao_Perfil1`
    FOREIGN KEY (`pptr_perf_id`)
    REFERENCES `ws`.`Perfil` (`perf_id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
CONSTRAINT `fk_Perfil_Plano_Transacao_Transacao1`
    FOREIGN KEY (`pptr_tran_id`)
    REFERENCES `ws`.`Transacao` (`tran_id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION,
CONSTRAINT `fk_Perfil_Plano_Transacao_Plano1`
    FOREIGN KEY (`pptr_plan_id`)
    REFERENCES `ws`.`Plano` (`plan_id`)
    ON DELETE NO ACTION
    ON UPDATE NO ACTION)
ENGINE = InnoDB

And the Python code I've come until here is:

class PerfilPlanoTransacaoModel(db.Model):

    __tablename__ = 'perfil_plano_transacao'

    pptr_perf_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    pptr_plan_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    pptr_tran_id = db.Column(db.Integer, primary_key=True, autoincrement=False)
    pptr_dt_inclusao = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    pptr_dt_atualizacao = db.Column(db.DateTime, nullable=True)

    __table_args__ = (
        db.ForeignKeyConstraint(
            ['pptr_perf_id', 'pptr_plan_id', 'pptr_tran_id'],
            ['perfil.perf_id', 'plano.plan_id', 'transacao.tran_id'],
            ['fk_Perfil_Plano_Transacao_Perfil1', 'fk_Perfil_Plano_Transacao_Plano1', 'fk_Perfil_Plano_Transacao_Transacao1']
        ),
    )

I would like to know if I am going toward the right way. I dind't find, for example, how to declare the name of the foreign key constraint and how to set the INDEX. Is there a more Flaks-Sqlalchemy way to do all this?

Carlos Ost
  • 492
  • 7
  • 22
  • why would you have more than a single primary_key? and why would it be possible? I think you want a foreingKey and a relationship field – NotSoShabby Aug 20 '19 at 15:44
  • @NotSoShabby Because in this case, the primary key is composed by the primary keys of other 3 tables (Role, Plan and Transaction). It is a very common and known use case. In this case, this table is used to give me the transactions a profile can make under a specific plan. Ex: If you pay for plan type A and you are the vip role, you can make transactions X and Y, but if you are the regular role, you can just use transaction Y. – Carlos Ost Aug 20 '19 at 16:15
  • isnt the other foreing keys belong to the other tables? im pretty sure you can only have one primary key per table. what error are you getting? – NotSoShabby Aug 20 '19 at 16:41
  • @NotSoShabby Yes, they belong to other tables as expected for a foreign key :-). On SQL it is very common to have a composed primary key and the SQL presented works fine. The point is I can't find examples of this specific presented scenario in Flask-Sqlalchemy. I didn't get any errors yet, because I din't tried to run the code. I am trying to find better examples to make sure it will work as expected and not just work. – Carlos Ost Aug 20 '19 at 17:18

2 Answers2

7

The answer by @Halvor is right, but I'll just add that you have a composite primary key but not a composite foreign key, you have three single column foreign keys pointing to different tables which means you can declare the foreign key in your column definition too:

from sqlalchemy import ForeignKey

class PerfilPlanoTransacaoModel(db.Model):

    __tablename__ = 'perfil_plano_transacao'

    pptr_perf_id = db.Column(
        db.Integer, 
        ForeignKey('perfil.perf_id'),
        primary_key=True, 
        autoincrement=False,
    )
    pptr_plan_id = db.Column(
        db.Integer, 
        ForeignKey('plano.plan_id'),
        primary_key=True, 
        autoincrement=False,
    )
    pptr_tran_id = db.Column(
        db.Integer, 
        ForeignKey('transacao.tran_id'),
        primary_key=True, 
        autoincrement=False,
    )
    pptr_dt_inclusao = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
    pptr_dt_atualizacao = db.Column(db.DateTime, nullable=True)

Using ForeignKeyConstraint is a little bit more verbose than using ForeignKey and the ForeignKey objects we create in your column definitions in this example are eventually converted to ForeignKeyConstraint objects anyway, its just easier to use the ForeignKey object when you are dealing with single column keys. The ForeignKeyConstraint object defined in __table_args__ is usually only used directly when you need to create a composite foreign key, for example if you had another table that wanted to reference perfil_plano_transacao, it would need to be a composite foreign key and you'd have to define it as you have done above.

I'll defer to @Halvor's answer for the rest of your question.

SuperShoot
  • 9,880
  • 2
  • 38
  • 55
  • Thank you so much for your answer. I will definitely use a mix of both (yours and @Halvor), because your contribution shows a high level usage of the library, but the accepted answer gave me exactly the answers I was looking for. – Carlos Ost Aug 21 '19 at 12:52
2

How to declare the name of the foreign key constraint

Adding multiple foreign key constraints can be done by having multiple ForeignKeyConstraints in your __table_args__. For example:

__table_args__ = (
    ForeignKeyConstraint(['pptr_perf_id'], ['perfil.perf_id'], name='fk_Perfil_Plano_Transacao_Perfil1'),
    ForeignKeyConstraint(['pptr_plan_id'], ['plano.plan_id'], name='fk_Perfil_Plano_Transacao_Plano1'),
    ForeignKeyConstraint(['pptr_tran_id'], ['transacao.tran_id'], name='fk_Perfil_Plano_Transacao_Transacao1'),
)

Here you see that your define your local column, then the column in the original table and give it a name. The reason the first two parameters are arrays are to allow for composite foreign keys.

Making this change to your code should evaluate to the following query:

CREATE TABLE perfil_plano_transacao (
        pptr_perf_id INTEGER NOT NULL,
        pptr_plan_id INTEGER NOT NULL,
        pptr_tran_id INTEGER NOT NULL,
        pptr_dt_inclusao DATETIME NOT NULL,
        pptr_dt_atualizacao DATETIME,
        PRIMARY KEY (pptr_perf_id, pptr_plan_id, pptr_tran_id),
        CONSTRAINT "fk_Perfil_Plano_Transacao_Perfil1" FOREIGN KEY(pptr_perf_id) REFERENCES perfil (perf_id),
        CONSTRAINT "fk_Perfil_Plano_Transacao_Plano1" FOREIGN KEY(pptr_plan_id) REFERENCES plano (plan_id),
        CONSTRAINT "fk_Perfil_Plano_Transacao_Transacao1" FOREIGN KEY(pptr_tran_id) REFERENCES transacao (tran_id)
)

How to set the INDEX

The simple way of adding an index is setting it on the column declaration:

pptr_perf_id = Column(Integer, primary_key=True, autoincrement=False)
pptr_plan_id = Column(Integer, primary_key=True, autoincrement=False, index=True)
pptr_tran_id = Column(Integer, primary_key=True, autoincrement=False, index=True)

Which would lead to the following two queries:

CREATE INDEX ix_perfil_plano_transacao_pptr_plan_id ON perfil_plano_transacao (pptr_plan_id)
CREATE INDEX ix_perfil_plano_transacao_pptr_tran_id ON perfil_plano_transacao (pptr_tran_id)

Or you can add it separately after the table declaration:

from sqlalchemy import Index
Index('fk_Perfil_Plano_Transacao_Transacao1_idx', PerfilPlanoTransacaoModel.pptr_tran_id.asc())
Index('fk_Perfil_Plano_Transacao_Plano1_idx', PerfilPlanoTransacaoModel.pptr_plan_id.asc())

Which would lead to the following two queries:

CREATE INDEX "fk_Perfil_Plano_Transacao_Transacao1_idx" ON perfil_plano_transacao (pptr_tran_id ASC)
CREATE INDEX "fk_Perfil_Plano_Transacao_Plano1_idx" ON perfil_plano_transacao (pptr_plan_id ASC)
Halvor Holsten Strand
  • 19,829
  • 17
  • 83
  • 99