Taking a datetime field into primary key throws fatal error - mysql

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

Related

NHibernate: How to insert C# [Guid] into MySQL [BINARY(16) DEFAULT (uuid_to_bin(uuid(),1))] column?

Environment: MySQL Server 8.0, .NET Core 3.1, MySql.Data 8.0.28, NHibernate 5.3.11
I have following table:
CREATE TABLE `Master` (
`Row_Id` char(36) NOT NULL DEFAULT (uuid()),
`Path` varchar(1000) NOT NULL,
PRIMARY KEY (`Row_Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
Following is entity definition and mapping:
public class MasterEntity
{
public virtual Guid RowId { get; set; }
public virtual string Path { get; set; }
}
internal sealed class MasterMap : ClassMapping<MasterEntity>
{
public MasterMap()
{
Table("Master");
Id
(
x => x.RowId,
map =>
{
map.Column("Row_Id");
map.Generator(Generators.GuidComb);
}
);
Property(x => x.Path, map => { map.Column("Path"); map.NotNullable(true); map.Type(TypeFactory.GetAnsiStringType(1000)); });
}
}
Following is how I INSERT this entity using NHibernate:
using(ISession session = SessionFactory.OpenSession())
{
MasterEntity entity = new MasterEntity();
entity.Path = "c:\whatever";
session.Save(entity);
session.Flush();
}
This inserts the record correctly. Up to here, everything is fine.
Now, I change the definition of Row_Id column as below:
`Row_Id` binary(16) NOT NULL DEFAULT (uuid_to_bin(uuid(),1)),
I do not change anything in my C# code. Now, the session.Flush(); call throws below exception:
NHibernate.Exceptions.GenericADOException: could not execute batch command.[SQL: SQL not available]
---> MySql.Data.MySqlClient.MySqlException (0x80004005): Data too long for column 'Row_Id' at row 1
The error looks obvious. The Guid in C# is 32 and column length is 16.
What changes I need to make in my mapping or entity definition (or other part of code) to insert C# Guid into BINARY(16) DEFAULT (uuid_to_bin(uuid(),1)) column?
By default, MySql.Data will store a Guid as CHAR(36). You can use BINARY(16) instead by specifying Old Guids = True; in your connection string.
From Connector/NET 8.0 Connection Options Reference:
The back-end representation of a GUID type was changed from BINARY(16) to CHAR(36). This was done to allow developers to use the server function UUID() to populate a GUID table - UUID() generates a 36-character string. Developers of older applications can add 'Old Guids=true' to the connection string to use a GUID of data type BINARY(16).
The way suggested in accepted answer works; but it has a problem.
The code in the question uses uuid_to_bin(uuid(),1); the second swap parameter is set to 1. With this, the INSERT works great; but when you SELECT the row, you get entirely different UUID. This is because, database drivers do not know whether the UUID is swapped or not.
Better solution is to use MySqlConnector instead of Oracle's Connector/NET (MySql.Data.dll).
For ADO.NET:
Configure it as explained here.
For .NET Core 2.1 or later, call DbProviderFactories.RegisterFactory("MySqlConnector", MySqlConnectorFactory.Instance) during application startup. This will register MySqlConnector’s DbProviderFactory implementation in the central DbProviderFactories registry.
My observation is that, call to DbProviderFactories.RegisterFactory is not needed. It just works by adding reference of MySqlConnector.dll and removing reference of MySql.Data.dll.
With MySqlConnector, the OldGuids=True; setting is available but obsolete; avoid it.
Use GuidFormat=TimeSwapBinary16; for uuid_to_bin(uuid(),1) (swap parameter set to 1).
Other possible values are mentioned here:
Determines which column type (if any) should be read as a System.Guid. The options include:
Char36:
All CHAR(36) columns are read/written as a Guid using lowercase hex with hyphens, which matches UUID().
Char32:
All CHAR(32) columns are read/written as a Guid using lowercase hex without hyphens.
Binary16:
All BINARY(16) columns are read/written as a Guid using big-endian byte order, which matches UUID_TO_BIN(x).
TimeSwapBinary16:
All BINARY(16) columns are read/written as a Guid using big-endian byte order with time parts swapped, which matches UUID_TO_BIN(x,1).
LittleEndianBinary16:
All BINARY(16) columns are read/written as a Guid using little-endian byte order, i.e. the byte order used by Guid.ToByteArray() and the Guid(byte[]) constructor.
None:
No column types are automatically read as a Guid.
Default:
Same as Char36 if OldGuids=False; same as LittleEndianBinary16 if OldGuids=True.
For NHibernate:
Install NHibernate.MySqlConnector from nuget package.
Add configuration.DataBaseIntegration(c => c.MySqlConnectorDriver()); in Session Factory configuration.
Set GuidFormat in connection string as explained above.
For other ORMs:
Please refer to this for usage with other ORMs.

Envers + MYSQL + List<String> = SQLSyntaxErrorException: Specified key was too long;

I am extending an existing application with Audit support using Envers. I annotated all #Entity classes and I got a bunch of Exception traces. When taking a look at them, it seems that they all relate to attribute definitions that have the following form
protected List<String> testActivities;
#ElementCollection
protected List<String> getTestActivities() {
return testActivities;
}
public void setTestActivities(List<String> testActivities) {
this.testActivities = FXCollections.observableList(testActivities);
}
All exceptions are List<String> attributes, and the getter method has a #ElementCollection annotation.
The Exception I am getting is always (here is the exception for the above testActivities attribute)
org.hibernate.tool.schema.spi.CommandAcceptanceException: Error executing DDL "create table TestCase_testActivities_AUD (REV integer not null, TestCase_id bigint not null, testActivities varchar(255) not null, REVTYPE tinyint, primary key (REV, TestCase_id, testActivities)) engine=MyISAM" via JDBC Statement
..
Caused by: java.sql.SQLSyntaxErrorException: Specified key was too long; max key length is 1000 bytes
..
I guess the issue is the primary key containing the testActivities?!
The testActivities attribute refers to a list of instructions that a user has to perform, so reducing the String length on the code side, as suggested on some StackOverflow pages related to the key length issue, is probably not an option?!
Currently all tables are created with DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci and I could probably save memory by using utf8 instead of utf8mb4, but is that a good and reliable solution?
How to fix this the right way? I am open to different views regarding the above two points.
I run MySQL Server 8.0.15, MyISAM, and
I am using Spring Boot which gives me Hibernate Envers 5.3.10
I forgot to mention that I am using #Access(AccessType.PROPERTY)on the class level. Anyway, I extended the related getter methods
#ElementCollection
#Column(length=175) // keep in sync with maxDBStringLength
public List<String> getEnvironmentalInterfaces() {
return environmentalInterfaces;
}
Thus actually does the trick. However, in order to not loose information, I also extended all methods to add an element to the list, like so
// Must be in sync with #Column(length=175) definitions
protected static int maxDBStringLength = Constants.maxDBStringLength;
public void addEnvironmentalInterfaces(String environmentalInterface) throws StringTooLongException {
if(environmentalInterface.length() > maxDBStringLength) {
throw new StringTooLongException(maxDBStringLength, environmentalInterface.length());
}
environmentalInterfaces.add(environmentalInterface);
}
Now all tables are created. Unfortunately I have now a NullPointer issue, which you find here Envers NullPointerException when creating test data - just in case you are going through the same learning curve.

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")

Check if a primary key exists in an SqlAlchemy Session

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

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;
}