Getting started
Pre-requisites
Install LibPQ.jl
(MyProject) pkg> add LibPQ
Install PostgresORM.jl
(MyProject) pkg> add PostgresORM
Example projects
You can look at the following projects to see how PostgresORM is used :
Concepts
Classes
PostgreSQL tables are mapped to mutable composite types that inherit the abstract type PostgresORM.IEntity.
For the sake of conciseness we call this particular type a class.
A class looks like this :
mutable struct Film <: IFilm
id::Union{Missing,Int32}
codeName::Union{Missing,String}
year::Union{Missing,Int16}
actorFilmAssos::Union{Missing,Vector{Model.IActorFilmAsso}}
Film(args::NamedTuple) = Film(;args...)
Film(;
id = missing,
codeName = missing,
year = missing,
actorFilmAssos = missing,
) = (
x = new(missing,missing,missing,missing,);
x.id = id;
x.codeName = codeName;
x.year = year;
x.actorFilmAssos = actorFilmAssos;
return x
)
end
Lets describe the key aspects of a class:
A class inherits an abstract type that inherits IEntity
mutable struct Film <: IFilm
where IFilm <: PostgresORM.IEntity
This allows us to avoid circular dependencies
Fields of a class are all Union of a Missing and something else
id::Union{Missing,Int32}
The 'something else' can be a lot of things including a IEntity
or a vector of IEntity
.
In this documentation we call:
A 'complex property', a property of type IEntity
. It is also named a "manyToOne" property and it resolves to a foreign key in the table of the class.
A 'property of IEntities', a property of type Vector{T} where T <: IEntity
. It is also named a "oneToMany" property and it is the counter part of a complex property in another class.
A class has two constructors
A first constructor that takes a NamedTuple and that is required by PostgresORM function. It calls the second constructor by splatting the NamedTuple.
A second constructor that takes optional named arguments with default values missing
and that assign the values to the matching properties.
Therefore:
Calling Film()
creates an instance of Film with all properties set to missing
Calling Film(id = 34, codeName = "cube")
creates an instance of Film with all properties set to missing
except id and codeName
ORM modules
An ORM module is a Julia module that tells PostgresORM how to handle a class. It contains the following:
data_type = Model.Film
: Assigns the module variable data_type
to the class associated with the ORM module
PostgresORM.get_orm(x::Model.Film) = return(ORM.FilmORM)
: Declares a new method of function PostgresORM.get_orm
, this function is used to tell PostgresORM which ORM module to use for a given class
get_schema_name() = "public"
: Returns the PostgreSQL schema name of the table associated with the class
get_table_name() = "film"
: Returns the table name associated with the class
get_columns_selection_and_mapping()
: Returns the mapping between julia fields and table columns. Note that a complex property can be mapped to an array of columns if the foreign key has multiple columns (i.e. if the class of the complexproperty_ has a composite id)
get_id_props()
: Returns the fields that make the id of the class. These fields can be complex properties
get_onetomany_counterparts()
: It gives for every property of IEntities the associated complex property (i.e. manyToOne property)
get_types_override()
: It gives for every oneToMany or manyToOne property the real type of the property
Some optional functions for the tracking of changes:
get_track_changes()
: Tells PostgresORM to record all the changes made to instances of the class
get_creator_property()
: Tells which property holds the reference of the user that created the instance. This property must inherit PostgresORM.AppUser
get_editor_property()
: Tells which property holds the reference of the user that last edited the instance. This property must inherit PostgresORM.AppUser
get_creation_time_property()
: Tells which property holds the creation time of the instance
get_update_time_property()
: Tells which property holds the last update time of the instance
Enums
Julia enums are the counterpart of PostgreSQL custom enumeration types.
LibPQ connection
Many PostgresORM functions expects a LibPQ.Connection
as one of the arguments. The developer is in charge of managing the connections and the transactions.
Design choices
Retrieval of complex properties
Methods retrieve_entity
and retrieve_one_entity
expects the argument retrieve_complex_props
to tell them if they need to make additional queries to retrieve the properties of the complex properties. If retrieve_complex_props == false
then the properties of a complexproperty_ will be set to missing except the properties used as IDs.
Retrieval of properties of IEntities
Reminder, methods retrieve_entity
and retrieve_one_entity
expects the argument retrieve_complex_props
to tell them if they need to make additional queries to retrieve the properties of the complex properties. There is no such thing for properties of IEntities, PostgresORM never loads them (the properties of will be equal to missing). It is up to the package user to enrich the instance if he wants to.
Update of a properties of IEntities
update_entity
does not update properties of type vector of IEntities. If the user wants to update a property of type vector of IEntities, he needs to use update_vector_property!
.
Beware! missing has two meanings
A property with value missing can mean that:
- the value is missing for this entity
- the value has not been loaded yet
We could have make use of Nothing
for the second case but we decided not to because the benefit was too small compare to the complexity it was adding. Nevertheless, the developer must be well aware of this when updating an instance. Here are two code snippets to show what is the risk:
# Load the film with retrieve_complex_props set to true
film = retrieve_one_entity(Film(codeName = "cube"),
true, # retrieve the complex props
dbconn)
@test ismissing(film.director.id) # false
@test ismissing(film.director.birthDate) # false
update_entity(film.director, dbconn) # This is OK
# Load the film with retrieve_complex_props set to false
film = retrieve_one_entity(Film(codeName = "cube"),
false, # do not retrieve the complex props
dbconn)
@test ismissing(film.director.id) # false
@test ismissing(film.director.birthDate) # true
update_entity(film.director, dbconn) # This is NOT OK! the director will loose
# its birthDate
Reverse engineer the database
The easiest way to get started is to ask PostgresORM to generate the classes, the ORM modules and the enums. Once done, you can copy the files in the src folder of the project and declare everything in the project (see how it's done in IMDBTestApp.jl).
Here is an example of script to reverse engineer a database:
out_dir = (@__DIR__) * "/out"
dbconn = begin
database = "imdbtestapp"
user = "imdbtestapp"
host = "127.0.0.1"
port = "5432"
password = "1234"
LibPQ.Connection("host=$(host)
port=$(port)
dbname=$(database)
user=$(user)
password=$(password)
"; throw_error=true)
end
PostgresORM.Tool.generate_julia_code(dbconn,out_dir)
close(dbconn)
How to record changes
Record the changes in the modified class itself
You can ask PostgresORM to record the following information inside the class itself :
- the creator of an instance,
- the last editor of an instance,
- the creation time of an instance,
- the last edition time of an instance
To do this you need to declare the following functions in the ORM of the class:
get_creator_property() = :your_creator_prop
get_editor_property() = :your_lastEditor_prop
get_creation_time_property() = :your_creationTime_prop
get_update_time_property() = :your_updateTime_prop
NOTE: You can decide to only declare some of these functions
Record the changes in a 'modification' table
You can also ask PostgresORM to record the details of the modifications of a class in a table.
To do this you need:
- Tell the ORM of the class that you want to record the changes
- A table where to write those modifications
Declare the following functions in the ORM of the class:
get_track_changes() = true
The Modification
table
PostgresORM ships a class Modification
that inherits from IEntity
. It also has the accompanying ORM mapping module PostgresORM.ModificationORM
. By default, this class is serialized to a table modification
in the schema public
.
Here is the SQL for the creation of the table if you want to use the default mapping of ModificationORM:
CREATE TABLE public.modification
(
id uuid NOT NULL DEFAULT uuid_generate_v4(),
entity_type character varying COLLATE pg_catalog."default",
entity_id character varying COLLATE pg_catalog."default",
attrname character varying COLLATE pg_catalog."default",
oldvalue text COLLATE pg_catalog."default",
user_id character varying COLLATE pg_catalog."default",
newvalue text COLLATE pg_catalog."default",
action_id uuid,
action_type character varying(10) COLLATE pg_catalog."default",
creation_time timestamp without time zone,
CONSTRAINT modification_pkey PRIMARY KEY (id)
)
Suppose the modification table is in a different schema, you can tell PostgresORM where it is by overwriting the functions of PostgresORM.ModificationORM
. Eg.
PostgresORM.ModificationORM.get_schema_name() = "my_schema"