Check if a primary key exists in an SqlAlchemy Session - sqlalchemy

I'd like to be able to check if an object with a given primary key exists in a Session object.
This is similar to the get function, which operates like:
> session.get({primary key})
... if {primary key} is in the session, return that object
... otherwise issue the SQL to check if the primary key is in the database
... if the primary key is in the database return that object otherwise None
I simply want to get the object if it's in the session without emitting any SQL.
(The reason for this is for my application I know that if the object is not already in the session it won't be in the database)

You can check if an object with a given primary key is in a session by looking it up in the session's identity_map.
The keys of the identity map are "identity key" tuples, which you can generate with the sqlalchemy.orm.util.identity_key function.
For example, given a mapped class MyClass with a primary key id, to check a session for an object with the id of 99:
from sqlalchemy.orm.util import identity_key
my_key = identity_key(MyClass, 99)
# will return the instance of MyClass in the session, or None
my_class_instance = my_session.identity_map.get(my_key)
Source: https://docs.sqlalchemy.org/en/latest/orm/session_api.html#sqlalchemy.orm.session.Session.identity_map

Related

Foreign keys and related data with gorm

I'm using golang and gorm to talk to a MySQL database.
I have a table with release metadata:
type OSType string
const (
Windows OSType = "windows"
Mac OSType = "mac"
)
type AgentMetadata struct {
Version string `gorm:"primary_key"`
OS OSType `gorm:"primary_key" sql:"type:ENUM('windows','mac')"`
Name string `sql:"not null"`
Description string `sql:"not null"`
ReleaseNotesUrl string `sql:"not null"`
UpdateTime time.Time `sql:"DEFAULT:current_timestamp"`
}
The releases are identified by a composite key - the OS and Version (number).
I have another table which defines the default version that clients should download (by OS):
type GlobalDefault struct {
OS OSType `gorm:"primary_key" sql:"type:ENUM('windows','mac')"`
Version string
AgentMetadata AgentMetadata
UpdateTime time.Time `sql:"DEFAULT:current_timestamp"`
}
What I want is to define two foreign keys from GlobalDefault to AgentMetadata (the pair OS and Version) and I want to be able to query the GlobalDefault table by its key OS and to get back a data structure which already contains the full AgentMetadata.
After a very long time and reading lots of documentatin, SO questions and code samples I tried to do the following:
func (repository *AgentRepository)GetGlobalDefault(os OSType) (error, AgentMetadata) {
gd := GlobalDefault{ OS:os }
result := AgentMetadata{}
return repository.connection.Find(&gd).Related(&result, "OS", "Version").Error, result
}
This "worked" in the sense that it really got the result filled up with AgentMetadata. However, it was not the correct metadata.
In my test I added two metadata records and one default:
And when I called err, queryResult := ar.GetGlobalDefault(defaultAgent.OS) instead of getting the 1.2.3 version metadata, I got the 1.2.3.1 metadata.
Indeed, when I turned on the gorm logs I saw that it ran the query:
[2017-07-15 17:51:50] [276.74ms] SELECT * FROM global_defaults WHERE global_defaults.os = 'windows'
[2017-07-15 17:51:50] [276.55ms] SELECT * FROM agent_metadata WHERE (os = 'windows')
First, it ignored the fact that I have a composite key in the agent_metadata table, and second, instead of doing a single query with a join, it made two queries (which is really a waste of time).
Another thing that bothers me is that I had to explicitly specify the foreign key names, whereas according to the documentation it seems that specifiying them is not needed or at least can be achieved by adding a tag:
type GlobalDefault struct {
OS OSType `gorm:"primary_key" sql:"type:ENUM('windows','mac')"`
Version string
AgentMetadata AgentMetadata `gorm:"ForeignKey:OS;AssociationForeignKey:OS"`
UpdateTime time.Time `sql:"DEFAULT:current_timestamp"`
}
Here I only added a tag for the OS column, however, I tried concatenating the foreign key names and neither option seemed to have an effect on the result. Without explicitly specifying the foreign key names in the API, the related data would just not be read. Having the pass the names to the query means that my DB mapping is not consolidated in a single place and I don't like that.
Can my scenario be solved? Can I have a composite foreign key? Can I specify all ORM properties in a single place? How can I make gorm create foreign keys in the DB (I noticed that the schema is created without foreign keys from GlobalDefault to AgentMetadata)?
How can I make gorm create foreign keys in the DB?
here is a sample:
db.AutoMigrate(&model.User{}).
AddForeignKey("account_id", "accounts(id)", "CASCADE", "CASCADE").
AddForeignKey("role_id", "roles(id)", "RESTRICT", "RESTRICT")

Updating object fields from separate processes? (kind of upsert)

I have Task objects with several attributes. These tasks are bounced between several processes (using Celery) and I'd like to update the task status in a database.
Every update should update only non-NULL attributes of the object. So far I have something like:
def del_empty_attrs(task):
for name in (key for key, val in vars(task).iteritems() if val is None):
delattr(task, name)
def update_task(session, id, **kw):
task = session.query(Task).get(id)
if task is None:
task = Task(id=id)
for key, value in kw.iteritems():
if not hasattr(task, key):
raise AttributeError('Task does not have {} attribute'.format(key))
setattr(task, key, value)
del_empty_attrs(task) # Don't update empty fields
session.merge(task)
However, get either IntegrityError or StaleDataError. What the right way to do this?
I think the problem is that every process has its own session, but I'm not sure.
a lot more detail would be needed to say for sure, but there is a race condition in this code:
def update_task(session, id, **kw):
# 1.
task = session.query(Task).get(id)
if task is None:
# 2.
task = Task(id=id)
for key, value in kw.iteritems():
if not hasattr(task, key):
raise AttributeError('Task does not have {} attribute'.format(key))
setattr(task, key, value)
del_empty_attrs(task) # Don't update empty fields
# 3.
session.merge(task)
If two processes both encounter #1, and find the object for the given id to be None, they both proceed to create a new Task() object with the given primary key (assuming id here is the primary key attribute). Both processes then race down to the Session.merge() which will attempt to emit an INSERT for the row. One process gets the INSERT, the other one gets an IntegrityError as it did not INSERT the row before the other one did.
There's no simple answer for how to "fix" this, it depends on what you're trying to do. One approach might be to ensure that no two processes work on the same pool of primary key identifiers. Another would be to ensure that all INSERTs of non-existent rows are handled by a single process.
Edit: other approaches might involve going with an "optimistic" approach, where SAVEPOINT (e.g. Session.begin_nested()) is used to intercept an IntegrityError on an INSERT, then continue on after it occurs.

Update empty string to NULL in a html form

I'm building a site in Laravel.
I have foreign key constraints set up among InnoDB tables.
My problem is that if i don't select a value in a, say, select box, the framework tries to insert or update a record in a table with '' (empty string). Which causes a MySQL error as it cannot find the equivalent foreign key value in the subtables.
Is there some elegant way to force the insertion of NULL in the foreign key fields other than checking out every single field? Or to force MySQL to accept '' as a "null" foreign key reference?
In other words: I have a, say, SELECT field with first OPTION blank. I leave the blank OPTION chosen. When I submit, an empty string '' is passed. In MySQL apparently I can do UPDATE table SET foreignKey=NULL but not UPDATE table SET foreignKey=''. It does not "convert" to NULL. I could check the fields one by one but and convert '' to NULL for every foreign key, maybe specifying all of them in an array, I was wondering if there's a more streamlined way to do this.
Maybe have to change my ON UPDATE action (which is not set) in my DB schema?
Edit: the columns DO accept the NULL value, the problem is in how the framework or MySQL handle the "empty value" coming from the HTML. I'm not suggesting MySQL "does it wrong", it is also logical, the problem is that you can't set a "NULL" value in HTML, and I would like to know if there's an elegant way to manage this problem in MySQL or Laravel.
In other words, do I have to specify manually the foreign keys and construct my query accordingly or is there another robust and elegant way?
My code so far for the model MyModel:
$obj = new MyModel;
$obj->fill(Input::all())); // can be all() or a subset of the request fields
$obj->save();
At least since v4 of Laravel (and Eloquent models), you can use mutators (aka setters) to check if a value is empty and transform it to null, and that logic is nicely put in the model :
class Anything extends \Eloquent {
// ...
public function setFooBarAttribute($value) {
$this->attributes['foo_bar'] = empty($value)?null:$value;
}
}
You can check out the doc on mutators.
I've been oriented by this github issue (not exactly related but still).
Instead of using
$obj = new MyModel;
$obj->fill(Input::all())); // can be all() or a subset of the request fields
$obj->save();
Use
$obj = new MyModel;
$obj->fieldName1 = Input::get('formField1');
$obj->fieldName2 = Input::has('formField2') && Input::get('formField2') == 'someValue' ? Input::get('formField2') : null;
// ...
$obj->save();
And make sure your database field accepts null values. Also, you can set a default value as null from the database/phpmyadmin.
You must remove the "not null" attribute from the field that maps your foreign key.
In the model add below function.
public function setFooBarAttribute($value)
{
$this->attributes['foo_bar'] = $value?:null;
}

Taking a datetime field into primary key throws fatal error

I would like to use the combination of two foreign keys plus the datetime field as my combined primary key.
But I get a
Catchable Fatal Error: Object of class DateTime could not be converted
to string in
C:\development\xampp\htdocs\happyfaces\vendor\doctrine\orm\lib\Doctrine\ORM\UnitOfWork.php
line 1337
when I do so. As soon as I remove the id: true from my YML entity declaration everything works fine again.
What is the problem that occurs here? It seems to be rather a Symfony2 or a Doctrine2 bug to me, because the datetime is set fine in the database if I don't declare the datetime column to be part of the primary key.
Can anyone help or advise?
Its not possible and not recommended. For primary key focus on primitive data types such as Integer or String. The most RDMS System prefer Integer as primary key for maximum performance.
Take look: http://doctrine-orm.readthedocs.org/en/2.1/tutorials/composite-primary-keys.html
Maybe a workaround could work by adding a new Doctrine data type. With a __toString() function, but I think Doctrine will force you to use primitive data types only.
class Foo
{
private $bar = 'test';
public function __toString()
{
return $this->bar;
}
}
echo new Foo();
Your error means in general DateTime has no __toString() function or is not string compatible. I never tested it to use a custom data type as primary key. So you've to try it yourself.
Take a look: http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/types.html
Another try is use String as Primary key and set your id with
$entity->setId(new \DateTime()->format('yyyy/mm/dd'));
Here is a similar question: Symfony/Doctrine: DateTime as primary key

Naming a multi-column constraint using JPA

The name attribute of #UniqueConstraint seems to have no effect.
#Entity
#Table(name = "TAG", uniqueConstraints = #UniqueConstraint(columnNames = {
"TAG_NAME", "USERS_ID" }, name="UQ_TAG_USER"))
public class Tag extends BaseEntity {
}
I'm usning SQL Server 2008, JPA 2.0 with Hibernate 3.6.
On the DB side an index, UQ__TAG__6EF57B66 is created instead of UQ_TAG_USER.
What am I missing? is there no way to enforce a given name from java side? and one must resort to editing schema files? we are a small shop without a DBA and I try to make do as much as I can by the help of hibernate schema facilities.
I assume you are using hibernate because you have it in the tags for this question. It's a bug/missing feature in hibernate:
https://hibernate.onjira.com/browse/HB-1245
It will simply ignore the unique constraint name when the dialect supports creating the constraint in the same statement as create table.
I've checked SqlServer and Oracle dialects and they both support this way of creating the constraint, that will cause the bug you are experiencing.
There are two ways to workaround this bug:
1. The quick way:
Just extend the dialect and return false for supportsUniqueConstraintInCreateAlterTable() method:
public static class SQLServerDialectImproved extends SQLServerDialect {
#Override
public boolean supportsUniqueConstraintInCreateAlterTable() {
return false;
}
}
And set this class as your dialect in hibernate.dialect property of the persistence unit configuration (persistence.xml).
2. The right way:
Fix the hibernate code and recompile:
The bug is at org.hibernate.mapping.UniqueKey class, the method sqlConstraintString() will return unique (TAG_NAME, USERS_ID) for all dialects, even if they support constraint UQ_TAG_USER unique (TAG_NAME, USERS_ID).
But that is probably a larger change (need to support all kinds of dialects, etc.)
Under the hood:
If you use the original dialect, it will cause the following sql statement to be executed to create the table (added id column):
create table TAG (
id bigint not null,
TAG_NAME varchar(255),
USERS_ID varchar(255),
primary key (id),
unique (TAG_NAME, USERS_ID)
)
And after you apply the fix as stated in first option the following sql statements will be executed:
create table TAG (
id numeric(19,0) not null,
TAG_NAME varchar(255),
USERS_ID varchar(255),
primary key (id)
)
create unique index UQ_TAG_USER on TAG (TAG_NAME, USERS_ID)
which include the creation of the unique constraint with the chosen name (UQ_TAG_USER) in a separate statement.