DBIx-NinjaORM

 view release on metacpan or  search on metacpan

lib/DBIx/NinjaORM.pm  view on Meta::CPAN


	my $object = My::Model::Book->new();

=item * Retrieving a single object from the database.

	# Retrieve by ID.
	my $object = My::Model::Book->new( { id => 3 } )
		// die 'Book #3 does not exist';

	# Retrieve by unique field.
	my $object = My::Model::Book->new( { isbn => '9781449303587' } )
		// die 'Book with ISBN 9781449303587 does not exist';

=back

When retrieving a single object from the database, the first argument should be
a hashref containing the following information to select a single row:

=over 4

=item * id

The ID for the primary key on the underlying table. C<id> is an alias for the
primary key field name.

	my $object = My::Model::Book->new( { id => 3 } )
		// die 'Book #3 does not exist';

=item * A unique field

Allows passing a unique field and its value, in order to load the
corresponding object from the database.

	my $object = My::Model::Book->new( { isbn => '9781449303587' } )
		// die 'Book with ISBN 9781449303587 does not exist';

Note that unique fields need to be defined in C<static_class_info()>, in the
C<unique_fields> key.

=back

This method also supports the following optional arguments, passed in a hash
after the filtering criteria above-mentioned:

=over 4

=item * skip_cache (default: 0)

By default, if cache is enabled with C<object_cache_time()> in
C<static_class_info()>, then C<new> attempts to load the object from the cache
first. Setting C<skip_cache> to 1 forces the ORM to load the values from the
database.

	my $object = My::Model::Book->new(
		{ isbn => '9781449303587' },
		skip_cache => 1,
	) // die 'Book with ISBN 9781449303587 does not exist';

=item * lock (default: 0)

By default, the underlying row is not locked when retrieving an object via
C<new()>. Setting C<lock> to 1 forces the ORM to bypass the cache if any, and
to lock the rows in the database as it retrieves them.

	my $object = My::Model::Book->new(
		{ isbn => '9781449303587' },
		lock => 1,
	) // die 'Book with ISBN 9781449303587 does not exist';

=back

=cut

sub new
{
	my ( $class, $filters, %args ) = @_;

	# If filters exist, they need to be a hashref.
	croak 'The first argument must be a hashref containing filtering criteria'
		if defined( $filters ) && !Data::Validate::Type::is_hashref( $filters );

	# Check if we have a unique identifier passed.
	# Note: passing an ID is a subcase of passing field defined as unique, but
	# unique_fields() doesn't include the primary key name.
	my $unique_field;
	foreach my $field ( 'id', @{ $class->get_info('unique_fields') // [] } )
	{
		next
			if ! exists( $filters->{ $field } );

		# If the field exists in the list of filters, it needs to be
		# defined. Being undefined probably indicates a problem in the calling code.
		croak "Called new() with '$field' declared but not defined"
			if ! defined( $filters->{ $field } );

		# Detect if we're passing two unique fields to retrieve the object. This is
		# obviously bad.
		croak "Called new() with the unique argument '$field', but already found another unique argument '$unique_field'"
			if defined( $unique_field );

		$unique_field = $field;
	}

	# Retrieve the object.
	my $self;
	if ( defined( $unique_field ) )
	{
		my $objects = $class->retrieve_list(
			{
				$unique_field => $filters->{ $unique_field },
			},
			skip_cache    => $args{'skip_cache'},
			lock          => $args{'lock'} ? 1 : 0,
		);

		my $objects_count = scalar( @$objects );
		if ( $objects_count == 0 )
		{
			# No row found.
			$self = undef;
		}

lib/DBIx/NinjaORM.pm  view on Meta::CPAN

		my $table_schema = $class->get_table_schema();
		croak "Failed to retrieve schema for table '$table_name'"
			if !defined( $table_schema );
		my $column_names = $table_schema->get_column_names();
		croak "Failed to retrieve column names for table '$table_name'"
			if !defined( $column_names );

		my @filtered_fields = ();
		if ( defined( $args{'exclude_fields'} ) && !defined( $args{'select_fields'} ) )
		{
			my %excluded_fields = map { $_ => 1 } @{ $args{'exclude_fields'} };
			foreach my $field ( @$column_names )
			{
				$excluded_fields{ $field }
					? delete( $excluded_fields{ $field } )
					: push( @filtered_fields, $field );
			}
			croak "The following excluded fields are not valid: " . join( ', ', keys %excluded_fields )
				if scalar( keys %excluded_fields ) != 0;
		}
		elsif ( !defined( $args{'exclude_fields'} ) && defined( $args{'select_fields'} ) )
		{
			my %selected_fields = map { $_ => 1 } @{ $args{'select_fields'} };
			croak 'The primary key must be in the list of selected fields'
				if defined( $primary_key_name ) && !$selected_fields{ $primary_key_name };

			foreach my $field ( @$column_names )
			{
				next if !$selected_fields{ $field };
				push( @filtered_fields, $field );
				delete( $selected_fields{ $field } );
			}

			croak "The following restricted fields are not valid: " . join( ', ', keys %selected_fields )
				if scalar( keys %selected_fields ) != 0;
		}
		else
		{
			croak "The 'exclude_fields' and 'select_fields' options are not compatible, use one or the other";
		}

		croak "No fields left after filtering out the excluded/restricted fields"
			if scalar( @filtered_fields ) == 0;

		$fields = join(
			', ',
			map { "$quoted_table_name.$_" } @filtered_fields
		);
	}
	else
	{
		$fields = $quoted_table_name . '.*';
	}

	$fields .= ', ' . $args{'query_extensions'}->{'joined_fields'}
		if defined( $args{'query_extensions'}->{'joined_fields'} );

	# We need to make an exception for lock=1 when using SQLite, since
	# SQLite doesn't support FOR UPDATE.
	# Per http://sqlite.org/cvstrac/wiki?p=UnsupportedSql, the entire
	# database is locked when updating any bit of it, so we can simply
	# ignore the locking request here.
	my $lock = '';
	if ( $args{'lock'} )
	{
		my $database_type = $dbh->{'Driver'}->{'Name'} || '';
		if ( $database_type eq 'SQLite' )
		{
			$log->info(
				'SQLite does not support locking since only one process at a time is ',
				'allowed to update a given SQLite database, so lock=1 is ignored.',
			);
		}
		else
		{
			$lock = 'FOR UPDATE';
		}
	}

	# Check if we need to paginate.
	my $pagination_info = {};
	if ( defined( $args{'pagination'} ) )
	{
		# Allow for pagination => 1 as a shortcut to get all the defaults.
		$args{'pagination'} = {}
			if !Data::Validate::Type::is_hashref( $args{'pagination'} ) && ( $args{'pagination'} eq '1' );

		# Set defaults.
		$pagination_info->{'per_page'} = ( $args{'pagination'}->{'per_page'} || '' ) =~ m/^\d+$/
			? $args{'pagination'}->{'per_page'}
			: 20;

		# Count the total number of results.
		my $count_data = $dbh->selectrow_arrayref(
			sprintf(
				q|
					SELECT COUNT(*)
					FROM %s
					%s
					%s
				|,
				$quoted_table_name,
				$joins,
				$where,
			),
			{},
			map { @$_ } @$where_values,
		);
		$pagination_info->{'total_count'} = defined( $count_data ) && scalar( @$count_data ) != 0
			? $count_data->[0]
			: undef;

		# Calculate what the max page can be.
		$pagination_info->{'page_max'} = int( ( $pagination_info->{'total_count'} - 1 ) / $pagination_info->{'per_page'} ) + 1;

		# Determine what the current page is.
		$pagination_info->{'page'} = ( ( $args{'pagination'}->{'page'} || '' ) =~ m/^\d+$/ ) && ( $args{'pagination'}->{'page'} > 0 )
			? $pagination_info->{'page_max'} < $args{'pagination'}->{'page'}
				? $pagination_info->{'page_max'}
				: $args{'pagination'}->{'page'}
			: 1;

		# Set LIMIT and OFFSET.
		$limit = "LIMIT $pagination_info->{'per_page'} "
			. 'OFFSET ' . ( ( $pagination_info->{'page'} - 1 ) * $pagination_info->{'per_page'} );
	}

	# If we need to lock the rows and there's joins, let's do this in two steps:
	# 1) Lock the rows without join.
	# 2) Using the IDs found, do another select to retrieve the data with the joins.
	if ( ( $lock ne '' ) && ( $joins ne '' ) )
	{
		my $query = sprintf(
			q|
				SELECT %s
				FROM %s
				%s
				ORDER BY %s ASC
				%s
				%s
			|,
			$quoted_primary_key_name,
			$quoted_table_name,
			$where,
			$quoted_primary_key_name,
			$limit,
			$lock,
		);

		my @query_values = map { @$_ } @$where_values;
		$log->debugf(
			"Performing pre-locking query:\n%s\nValues:\n%s",
			$query,
			\@query_values,
		) if $args{'show_queries'};

		my $locked_ids;
		try
		{
			local $dbh->{'RaiseError'} = 1;
			$locked_ids = $dbh->selectall_arrayref(
				$query,
				{
					Columns => [ 1 ],
				},
				@query_values
			);
		}
		catch
		{
			$log->fatalf(
				"Could not select rows in pre-locking query: %s\nQuery: %s\nValues:\n%s",
				$_,
				$query,
				\@query_values,
			);
			croak "Failed select: $_";
		};

		if ( !defined( $locked_ids ) || ( scalar( @$locked_ids ) == 0 ) )
		{
			return [];
		}

		$where = sprintf(
			'WHERE %s.%s IN ( %s )',
			$quoted_table_name,
			$quoted_primary_key_name,
			join( ', ', ( ('?') x scalar( @$locked_ids ) ) ),
		);
		$where_values = [ [ map { $_->[0] } @$locked_ids ] ];
		$lock = '';
	}

	# Prepare the query elements.
	my $query = sprintf(
		q|
			SELECT %s
			FROM %s
			%s %s %s %s %s
		|,
		$fields,
		$quoted_table_name,
		$joins,
		$where,
		$order_by,
		$limit,
		$lock,
	);
	my @query_values = map { @$_ } @$where_values;
	$log->debugf(
		"Performing query:\n%s\nValues:\n%s",
		$query,
		\@query_values,
	) if $args{'show_queries'};

	# Retrieve the objects.
	my $sth;
	try
	{
		local $dbh->{'RaiseError'} = 1;
		$sth = $dbh->prepare( $query );
		$sth->execute( @query_values );
	}
	catch
	{
		$log->fatalf(
			"Could not select rows: %s\nQuery: %s\nValues: %s",
			$_,
			$query,
			\@query_values,
		);
		croak "Failed select: $_";
	};

	my $object_list = [];
	while ( my $ref = $sth->fetchrow_hashref() )
	{
		my $object = Storable::dclone( $ref );
		bless( $object, $class );

		$object->reorganize_non_native_fields();

		# Add a flag to distinguish objects that were populated via
		# retrieve_list_nocache(), as those objects are known for sure to contain
		# all the keys for columns that exist in the database. We also won't have to
		# worry about missing defaults, like insert() would have to.
		$object->{'_populated_by_retrieve_list'} = 1;

		# Add cache debugging information.
		$object->{'_debug'}->{'list_cache_used'} = 0;



( run in 1.490 second using v1.01-cache-2.11-cpan-5b529ec07f3 )